跳到主要内容

闭包

· 阅读需 1 分钟
1adybug
子虚伊人

在 JavaScript 中,很多时候我们总是认为:一个变量如果无法被访问到了,那么它就会被垃圾回收,然而事实却并非如此:

function bar() {
const a = 1
const b = 2

function log() {
console.log(a)
}

return log
}

const log = bar()
console.dir(log)
提示

使用 console.dir 可以避免控制台只打印变量的字符串形式

可以看到,闭包如下:

01

但是如果我们再创建一个函数 log2,并且引用了变量 b

function bar() {
const a = 1
const b = 2

function log() {
console.log(a)
}

function log2() {
console.log(b)
}

return log
}

const log = bar()
console.dir(log)

此时再查看闭包:

02

可以得出结论:只要产生了闭包,而上下文中的其他变量又被其他函数所引用了,即使引用的函数无法被访问,被引用的变量也不会被回收

HTML 元素 props

· 阅读需 1 分钟
1adybug
子虚伊人

很多时候我们会有元素标签上显示的属性来获取元素的 props

import { HTMLAttributes } from "react"

type MyDivProps = HTMLAttributes<HTMLDivElement>

但其实,React 内置更为方便的泛型工具 ComponentProps

import { ComponentProps } from "react"

type MyDivProps = ComponentProps<"div">

HTMLAttributes<HTMLDivElement>ComponentProps<"div"> 的区别在于,后者包含了 refkey 属性

前端守则

· 阅读需 3 分钟
1adybug
子虚伊人

技术栈

类型要求
编辑器Visual Studio Code
浏览器Chrome
语言TypeScript
UI 库React
组件库Ant Design / Deepsea Components
脚手架Rsbuild / Next
路由React Router
包管理器Yarn
时间处理Day.js
CSSTailwind / Emotion
hooksahooks
状态共享ahooks / Soda Hooks / React Soda
网络请求fetch
数据验证Zod
数据库管理Prisma
移动端Capacitor
移动端组件库Ionic
代码格式化Prettier

要求

  1. 所有 React 组件必须使用以下形式书写:

    const Component: FC = () => <div></div>

    export default Component

    // 有参数组件
    import { FC } from "react"

    export type ComponentProps = {}

    const Component: FC<ComponentProps> = props => {
    const {} = props

    return <div></div>
    }

    export default Component
  2. 全局共享状态优先使用 ahooks 实现,其次 React Soda

    import { useRequest } from "ahooks"

    export function useCount() {
    return useRequest(() => Promise.resolve(Math.random()), {
    cacheKey: "abc",
    })
    }
    注意

    ahooksStrictMode 下有 Bug,需要关闭严格模式

    import createStore from "react-soda"

    export const useCount = createStore(0)
  3. CSS 样式优先使用 Tailwind 实现,其次 Emotion 或者行内样式实现。

    注意
    • Tailwind 不支持动态属性,如 `bg-${color}`
    • 尽量不要在 Emotion 中书写动态样式,Emotion 生成的样式不会被自动清除。如果有动态需求,可以使用 CSS 变量 var(--name) 搭配行内样式实现
  4. 对于 Ant Design 的样式修改尽量通过修改主题来实现

  5. 对于表单查询的参数使用 Soda Hooks 中的 useQueryState 来存储和控制

    import { useQueryState } from "soda-hooks"

    const [query, setQuery] = useQueryState({
    keys: ["name", "unit"],
    parse: {
    age: value => (value ? Number(value) : undefined),
    },
    })
  6. 在项目根目录配置 Prettier

    // prettier.config.cjs
    module.exports = {
    // tailwind 格式化插件
    plugins: ["prettier-plugin-tailwindcss"],
    semi: false,
    tabWidth: 4,
    arrowParens: "avoid",
    printWidth: 800,
    trailingComma: "none",
    }
  7. 命令规范

在开发环境中使用 https

· 阅读需 1 分钟
1adybug
子虚伊人

现在很多环境下的网络请求要求必须是 https 请求,但是在开发或者内网环境下,无法使用 CA 机构颁发的证书,这时候可以使用 OpenSSL 提供的自签证书功能:

安装

在 windows 系统下,可以使用 winget 来安装第三方分发版本:

winget search openssl
# 选择一个版本较新的分发版本
winget install FireDaemon.OpenSSL

重启终端

生成证书

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout your-key.pem -out your-cert.pem

使用

import { readFileSync } from "fs"
import { createServer } from "https"

import express from "express"

const app = express()

const server = createServer(
{
key: readFileSync("your-key.pem"),
cert: readFileSync("your-cert.pem"),
},
app,
)

server.listen(3000)

在 Node.js 中使用 fetch 请求接口可能会报错:

TypeError: fetch failed
at Object.fetch (node:internal/deps/undici/undici:11730:11)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: Error: self-signed certificate
at TLSSocket.onConnectSecure (node:_tls_wrap:1674:34)
at TLSSocket.emit (node:events:514:28)
at TLSSocket._finishInit (node:_tls_wrap:1085:8)
at ssl.onhandshakedone (node:_tls_wrap:871:12) {
code: 'DEPTH_ZERO_SELF_SIGNED_CERT'
}
}

指定环境变量 NODE_TLS_REJECT_UNAUTHORIZED='0' 即可

npx cross-env NODE_TLS_REJECT_UNAUTHORIZED='0' node index.js

在 leaflet 中使用百度地图瓦片图

· 阅读需 1 分钟
1adybug
子虚伊人

安装

安装 leaflet, proj4, proj4leaflet

npm i leaflet proj4 proj4leaflet
npm i @types/leaflet @types/proj4 @types/proj4leaflet -D

使用

import L from "leaflet"

import "leaflet/dist/leaflet.css"
import "proj4leaflet"

import "./style.css"

const resolutions = Array(20)
.fill(0)
.map((_, i) => Math.pow(2, 18 - i))

const baiduCrs = new L.Proj.CRS(
"EPSG:900913",
"+proj=merc +a=6378206 +b=6356584.314245179 +lat_ts=0.0 +lon_0=0.0 +x_0=0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs",
{
resolutions,
origin: [0, 0],
bounds: L.bounds([20037508.342789244, 0], [-20037508.342789244, 20037508.342789244]),
},
)

const map = new L.Map(document.getElementById("app")!, {
center: [33.60478, 119.05186],
zoom: 19,
crs: baiduCrs,
minZoom: 3,
// 必须设置最大缩放为 19
maxZoom: 19,
})

L.tileLayer("/wanda-baidu/{z}/{x}/{y}.png", {
tms: true,
attribution: 'Map data &copy; <a href="https://map.baidu.com/">百度地图</a>',
minZoom: 3,
// 必须设置最大缩放为 19
maxZoom: 19,
}).addTo(map)

L.marker([33.60503, 119.045273]).addTo(map)

Node.js 中 child_process 模块中的 exec 和 spawn

· 阅读需 4 分钟
1adybug
子虚伊人

Node.js 的 child_process 模块允许你从 Node.js 应用程序内部运行其他程序和命令。它是一个非常强大的功能,可以用来执行操作系统级别的任务,例如启动一个新的进程来处理 CPU 密集型工作或者运行系统命令。

child_process 模块是 Node.js 中一个强大的模块,它被用于各种场景,如:

  • 在后台运行定时任务 (比如,备份数据库)
  • 利用多核 CPU 优势来提高应用性能
  • 运行系统命令,进行文件操作
  • 实现基本的并行处理

运用这个模块可以帮助你的 Node.js 应用与操作系统更紧密地交互,从而执行一些复杂的任务。不过,也需要注意,错误地使用 child_process 模块可能会导致安全问题,比如如果用户输入没有得到恰当的处理,就可能会引发命令注入攻击。因此,在使用它时必须小心谨慎。

这里有几个主要的函数和类,你可以通过 child_process 模块使用:

exec

exec 用于执行一个命令并且将结果以回调函数的形式返回。它适合用于那些产生少量输出的情况

const { exec } = require("child_process")

exec("ls", (error, stdout, stderr) => {
if (error) {
console.error(`执行的错误: ${error}`)
return
}

console.log(`stdout: ${stdout}`)
console.error(`stderr: ${stderr}`)
})

在这个例子中,我们使用了 exec 函数来运行 ls 命令,这个命令会列出当前文件夹中的所有文件。如果执行成功,它的输出会被打印到控制台。

调用方法:exec(command[, options][, callback])

参数解释:

  1. command (必须): 你想要执行的命令字符串。
  2. options (可选): 一个对象,可以用来定制操作的各种设置,例如:
    • cwd:指定子进程的当前工作目录。
    • env:环境变量键值对。
    • encoding:输出的编码。
    • timeout:超时时间,过了这个时间子进程会被杀掉。
    • shell:要使用的 shell,如果不指定,默认在 UNIX 上是 /bin/sh,在 Windows 上是 cmd.exe
  3. callback (可选): 当进程终止或有错误发生时调用的回调函数,其参数包括:
    • error:错误对象或者 null
    • stdout:子进程的标准输出。
    • stderr:子进程的标准错误输出。

execFile

在计算机中,进程(Process)是正在运行的程序的实例。Node.js 提供了一个 child_process 模块,使得它可以从 Node.js 程序中创建和控制子进程。execFile 函数是这个模块中的一个非常有用的方法,用于创建一个新的子进程来执行一个指定的程序文件,并且可以传递参数给这个程序

调用方法:execFile(file[, args][, options][, callback])

参数解释:

  1. file (必须):这是你想要执行的可执行文件的名称或者路径。如果这个文件在系统的 PATH 环境变量里定义的目录中,你可以直接提供文件名;否则,你需要提供完整的路径。

  2. args (可选):这是一个可选参数,是一个数组,包含所有你想要传递给程序的命令行参数。

  3. options (可选):同 exec 中的 options

  4. callback (可选):同 exec 中的 callback

spawn

exec 相比,spawn 会返回一个流(Stream),这使得它更适用于需要处理大量数据的情况。

const { spawn } = require("child_process")

const child = spawn("find", ["."])

child.stdout.on("data", data => {
console.log(`stdout: ${data}`)
})

child.stderr.on("data", data => {
console.error(`stderr: ${data}`)
})

child.on("close", code => {
console.log(`子进程退出码:${code}`)
})

fork

这个函数是特别为 Node.js 模块设计的。它允许你创建一个 Node.js 进程,并运行一个模块。这对于在后台执行一个任务特别有用,而不必担心阻塞主事件循环。

const { fork } = require("child_process")

const child = fork("some-module.js")

child.on("message", message => {
console.log("收到消息:", message)
})

child.send({ hello: "world" })

在这个例子中,fork 创建了一个新的 Node.js 进程来运行 some-module.js 文件。父进程通过 .send() 方法发送消息给子进程,并通过监听 message 事件来接受子进程发送的消息。

...todo

检查依赖中的包

· 阅读需 2 分钟
1adybug
子虚伊人

有时候,项目中可能会存在某个隐藏的依赖,又或者存在同一个依赖的多个版本,这时候我们可以通过以下命令来检查依赖来源:

# 推荐:npm
npm ls @types/react
# yarn
yarn why @types/react

当项目中存在多个版本的 @types/react 时,可能或报错:

不能用作 JSX 组件, 不是有效的 JSX 元素

这时我们可以在 package.json 中配置 resolutions 统一 @types/react 版本:

{
"resolutions": {
"@types/react": "^18.2.79"
}
}

以下是实际遇到的案例,在启动某项目时,总是报以下错误:

错误

去 github 一搜,发现是项目中存在多个版本 string-width 依赖(实际上只会安装最新的依赖)的问题,使用 npm ls string-width 查看:

之前

将依赖项 string-width 版本改为 ^4

{
"resolutions": {
"string-width": "^4"
}
}

再使用 npm ls string-width 查看:

之前

问题解决了。

注意
  1. 尽量使用多个版本中最低 major 版本的依赖,新版本可能是移除了某些特性
  2. 解决这一个依赖的版本问题后,再次启动项目,可能还会报错,不要慌张,仔细看一下报错信息,有可能是另一个依赖的版本问题

从 git 提交记录中删除文件或者文件夹

· 阅读需 1 分钟
1adybug
子虚伊人

有的时候我们会不小心误操作,将某些文件(夹)加入了 git 的提交记录,这并不是我们想要的,即使再添加进 gitignore 也无济于事,以下是解决办法:

# 将 secrets.txt 替换为要删除的文件
git filter-branch --force --index-filter "git rm --cached --ignore-unmatch secrets.txt" --prune-empty --tag-name-filter cat -- --all
# 或者添加 -r 参数,删除文件夹
git filter-branch --force --index-filter "git rm -r --cached --ignore-unmatch logs" --prune-empty --tag-name-filter cat -- --all
# 或者使用 zixulu 工具
npx zixulu rm-git secrets.txt
# 添加 -r 参数,删除文件夹
npx zixulu rm-git logs -r
注意

文件(夹)会被全部删除,包括当前分支,请提前做好备份

安全切割字符串

· 阅读需 2 分钟
1adybug
子虚伊人

JavaScript 中,由于某些 emoji 表情是使用两个 Unicode 码点组成的,也就是说它们是由一对代理对(surrogate pair)表示的。这意味着这些 emoji 表情在 JavaScript 字符串中实际上会占用两个字符的位置。如果直接使用 slice() 方法来截取包含这类 emoji 的字符串,可能会导致 emoji 被错误地切割,导致无法正确显示。

为了解决这个问题,可以通过将字符串转换为一个能够正确处理 Unicode 扩展字符的形式(如使用 Array.from() 或者扩展运算符 ... 将字符串转换为数组),然后对数组进行操作,最后再将其转换回字符串。这样做可以确保即使是占用两个字符位置的 emoji 也能被正确处理。

以下是一个示例代码,展示了如何安全地截取包含 emoji 的字符串:

// 原始字符串,其中包含占用两个字符位置的 emoji
const originalString = "Hello 👋 World 🌍!"

// 使用 Array.from() 将字符串转换为数组,以正确处理每个 Unicode 字符
const characters = Array.from(originalString)

// 安全地截取字符串,这里以截取前 8 个 Unicode 字符为例
const sliced = characters.slice(0, 8).join("")

console.log(sliced) // 输出结果应为 "Hello 👋"

在这个例子中,Array.from(originalString) 生成了一个数组,每个元素都是 originalString 的一个 Unicode 字符,包括那些占用两个字符位置的 emoji。然后,你可以像处理普通数组一样处理这个字符数组,例如使用 slice() 方法。最后,使用 join("") 方法将数组元素合并成一个新的字符串。

这种方法可以确保即使是复杂的 Unicode 字符(如那些由代理对组成的 emoji)也能被正确处理,避免了因直接使用 slice() 方法而导致的潜在问题。

从 node-fetch 的响应中获取 readable

· 阅读需 1 分钟
1adybug
子虚伊人

node-fetchresponse.body 虽然类型标记为 ReadableStream,但实际上并不是,被 Readable.fromweb 调用时会报错,此时改为使用 Readable.from 即可成功。

import fetch from "node-fetch"

const response = await fetch("http://somewhere.com")

// ❌ 会报错
const readable = Readable.fromweb(response.body!)

// ✅ 不会报错
const readable = Readable.from(response.body!)