跳到主要内容

WSL 网络设置

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

wsl 默认使用的是 NAT 模式的网络,无法直接访问外部网络,可以通过修改 wsl 的网络设置,将其设置为 mirrored,使其能够直接访问外部网络。

C:\Users\用户名 目录下创建 .wslconfig 文件,内容如下:

[wsl2]
networkingMode=mirrored

wsl 中查看网络配置:

ifconfig

如果 ip 地址已经和主机在同一个网段,那么网络设置已经生效。

重启 wsl 使设置生效:

wsl --shutdown

代理可能需要重新设置,或者重启代理软件或者主机

如果要在 NAT 模式下使用 clash 代理,可以参考 获取 Windows 在 wsl 中的 ip 这篇文章

vi ~/.bashrc

~/.bashrc 文件中添加以下内容:

export http_proxy=http://172.30.160.1:7890
export https_proxy=http://172.30.160.1:7890
export all_proxy=http://172.30.160.1:7890
export HTTP_PROXY=http://172.30.160.1:7890
export HTTPS_PROXY=http://172.30.160.1:7890
export ALL_PROXY=http://172.30.160.1:7890

重启终端,或者执行 source ~/.bashrc 使设置生效。

发布后同步到 npm 镜像

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

在发布 npm 包后,可以立即在 npm 上看到最新的版本,然而 npmmirror 等镜像站却有一定的延迟。如果想要立即同步到镜像站,可以使用以下方法:

  1. 在项目根目录创建 scripts 文件夹,创建 sync.mjs 文件

    // @ts-check

    import { readFile } from "fs/promises"

    /**
    * 将浏览器中直接复制的 headers 转换为对象
    * @param {string} str 复制的 headers
    * @returns {Headers} headers 对象
    */
    function getHeaders(str) {
    const reg = /^(.+?):$\n^(.+?)$/gm
    const reg2 = new RegExp(reg.source, "m")
    const headers = new Headers()
    const match = str.match(reg)
    if (!match) throw new Error("headers 格式错误")

    Array.from(match).forEach(item => {
    const match2 = item.match(reg2)
    headers.set(match2[1], match2[2])
    })

    return headers
    }

    const headers = getHeaders(`accept:
    */*
    accept-encoding:
    gzip, deflate, br, zstd
    accept-language:
    zh-CN,zh;q=0.9,en;q=0.8
    content-length:
    0
    dnt:
    1
    origin:
    https://npmmirror.com
    priority:
    u=1, i
    referer:
    https://npmmirror.com/
    sec-ch-ua:
    "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
    sec-ch-ua-mobile:
    ?0
    sec-ch-ua-platform:
    "Windows"
    sec-fetch-dest:
    empty
    sec-fetch-mode:
    cors
    sec-fetch-site:
    same-site
    user-agent:
    Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36`)

    /**
    * 同步包
    * @param {string} packageName 包名
    */
    function syncPackage(packageName) {
    return fetch(`https://registry-direct.npmmirror.com/-/package/${packageName}/syncs`, {
    headers,
    referrer: "https://npmmirror.com/",
    referrerPolicy: "strict-origin-when-cross-origin",
    method: "PUT",
    mode: "cors",
    credentials: "omit",
    })
    }

    async function main() {
    const packageJson = JSON.parse(await readFile("package.json", "utf-8"))
    await syncPackage(packageJson.name)
    }

    main()
  2. package.json 中添加 sync 脚本

    {
    "scripts": {
    "postpublish": "node scripts/sync.mjs"
    }
    }

useQuery 中的 gcTime 和 staleTime

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

gcTime 表示所有缓存数据回收时间。默认为 5 分钟。如果设置为 Infinity, 则表示缓存数据永不过期。如果存在多个 gcTime 值,则取缓存周期中的最大值。如果所有引用这个 queryKeyhooks 都被销毁了,那么这个 queryKey 的缓存数据也会被销毁。如果在销毁之后,又有新的 hooks 使用了这个 queryKey,分为两种情况:

  1. 如果还在 gcTime 时间内,那么会直接使用缓存数据,且新的 gcTime 会加入到这个缓存周期中,即使这个 gcTime 比之前的 gcTime 小,也会取最大值
  2. 如果超过了 gcTime 时间,那么会重新请求数据,会生成一个新的缓存周期

staleTime 表示缓存数据新鲜时间。默认为 0。在该时间间隔内,认为数据是新鲜的,不会重新发请求。如果设置为 Infinity,则表示数据永远新鲜。

之前也分析过 ahooks 中的 useRequest(参考useRequest 中的 cacheTime 和 staleTime),规则复杂,心智负担严重,且在严格模式下数据不一致。@tanstack/query 中的 useQuery 明显更加优雅,且设计合理。

移除 Ant Design 中默认的 a 元素样式

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

Ant Design 总是会给 a 元素设置默认样式,这样会导致在使用 a 元素时,样式不符合我们的预期。比如在 a 元素中设置了 colorred,但是在 Ant Design 中,a 元素的默认样式是 blue,这样就会导致我们的样式被覆盖。可以用以下方式去除默认样式:

a[href],
a[href]:active,
a[href]:hover {
color: inherit;
}

使用 void 操作符返回

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

有的时候,某些函数类的参数类型是 () => (undefined | SomeType),如果我们使用箭头函数的话,就会出现返回类型不匹配的情况,比如:

import { useEffect } from "react"

useEffect(() => setTimeout(() => console.log("Hello"), 1000), [])

这时候,编辑器会给我们报错 不能将类型“Timeout”分配给类型“void | Destructor”,我们可以使用 {}console.log("Hello") 包裹起来:

import { useEffect } from "react"

useEffect(
() =>
setTimeout(() => {
console.log("Hello")
}, 1000),
[],
)

但是这样的话,代码会变得很臃肿,我们可以使用 void 操作符来解决这个问题:

import { useEffect } from "react"

useEffect(() => void setTimeout(() => console.log("Hello"), 1000), [])

大功告成!

在 SSR 中按需提取 Ant Design CSS

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

使用方法

react router(或者 remix)中使用 Ant Design 时,如果不对 css 进行处理,会导致首屏样式丢失的问题。之前介绍过整体导入的解决方案 在 Remix 中使用 Ant Design,但是这种方式会导致打包出来的 css 文件很大。本文介绍一种更优雅的解决方案:在 SSR 中按需提取 Ant Designcss

  1. 需要在项目中暴露 entry.client.tsxentry.server.tsx,如果已经暴露了,可以跳过这一步:

    npx react-router reveal
  2. 安装相应依赖:

    npm i @ant-design/cssinjs @ant-design/static-style-extract
  3. root.tsx 中放入 __ANTD_STYLE_PLACEHOLDER__

    const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"

    const isDev = process.env.NODE_ENV === "development"

    export const Layout: FC<PropsWithChildren> = ({ children }) => (
    <html lang="zh">
    <head>
    <meta charSet="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <Meta />
    <Links />
    {/** 如果需要在开发环境开启,去除 !isDev */}
    {!isBrowser && !isDev && "__ANTD_STYLE_PLACEHOLDER__"}
    </head>
    <body>
    <ConfigProvider
    theme={{
    token: {
    colorPrimary: "#FF0000",
    },
    }}
    >
    {children}
    </ConfigProvider>
    <ScrollRestoration />
    <Scripts />
    </body>
    </html>
    )
  4. 修改 entry.client.tsx

    import { startTransition, StrictMode } from "react"
    import { hydrateRoot } from "react-dom/client"

    import { legacyLogicalPropertiesTransformer, StyleProvider } from "@ant-design/cssinjs"
    import { HydratedRouter } from "react-router/dom"

    startTransition(() => {
    hydrateRoot(
    document,
    <StrictMode>
    <StyleProvider transformers={[legacyLogicalPropertiesTransformer]} hashPriority="high">
    <HydratedRouter />
    </StyleProvider>
    </StrictMode>,
    )
    })
  5. 修改 entry.server.tsx

    import { type RenderToPipeableStreamOptions, renderToPipeableStream } from "react-dom/server"

    import { PassThrough } from "node:stream"

    import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs"
    import { createReadableStreamFromReadable } from "@react-router/node"
    import { isbot } from "isbot"
    import { type AppLoadContext, type EntryContext, ServerRouter } from "react-router"

    const ABORT_DELAY = 5_000

    export default function handleRequest(
    request: Request,
    responseStatusCode: number,
    responseHeaders: Headers,
    routerContext: EntryContext,
    loadContext: AppLoadContext,
    ) {
    return new Promise((resolve, reject) => {
    let shellRendered = false
    const userAgent = request.headers.get("user-agent")

    const fromBot = !!userAgent && isbot(userAgent)

    // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
    // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
    const readyOption: keyof RenderToPipeableStreamOptions = (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady"

    const cache = createCache()

    const { pipe, abort } = renderToPipeableStream(
    <StyleProvider cache={cache}>
    <ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />
    </StyleProvider>,
    {
    [readyOption]() {
    shellRendered = true

    const body = new PassThrough({
    transform(chunk, encoding, callback) {
    chunk = String(chunk).replace("__ANTD_STYLE_PLACEHOLDER__", fromBot ? "" : extractStyle(cache))
    callback(null, chunk)
    },
    })

    const stream = createReadableStreamFromReadable(body)

    responseHeaders.set("Content-Type", "text/html")

    resolve(
    new Response(stream, {
    headers: responseHeaders,
    status: responseStatusCode,
    }),
    )

    pipe(body)
    },
    onShellError(error: unknown) {
    reject(error)
    },
    onError(error: unknown) {
    responseStatusCode = 500

    // Log streaming rendering errors from inside the shell. Don't log
    // errors encountered during initial shell rendering since they'll
    // reject and get logged in handleDocumentRequest.
    if (shellRendered) console.error(error)
    },
    },
    )

    setTimeout(abort, ABORT_DELAY)
    })
    }

原理分析

  1. 首先是在服务端的 HTML 代码中插入了 __ANTD_STYLE_PLACEHOLDER__

    !isBrowser && !isDev && "__ANTD_STYLE_PLACEHOLDER__"
  2. 然后是将 antd 的样式抽取为 style 标签

    const cache = createCache()

    renderToPipeableStream(
    <StyleProvider cache={cache}>
    <ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />
    </StyleProvider>,
    )

    const css = extractStyle(cache)
  3. __ANTD_STYLE_PLACEHOLDER__ 替换为抽取的 style 标签

    chunk = String(chunk).replace("__ANTD_STYLE_PLACEHOLDER__", fromBot ? "" : extractStyle(cache))

HTML 流式传输

· 阅读需 1 分钟
1adybug
子虚伊人
import { createServer } from "http"

const server = createServer(async (request, response) => {
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Transfer-Encoding": "chunked",
})

response.write(`
<!DOCTYPE html>
<html>
<body>
<h2 id="loading">Loading...</h2>
`)

const response2 = await fetch("https://dog.ceo/api/breeds/image/random")
const data = await response2.json()

response.write(`
<img src="${data.message}" alt="random dog" />
<script>
document.getElementById("loading").remove()
</script>
</body>
</html>
`)

response.end()
})

server.listen(3000, () => console.log("Server is running on http://localhost:3000"))

:last-child 与 :last-of-type 的区别

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

:last-child:last-of-type 的区别:

  1. :last-child 选择器

    • 选择父元素中最后一个子元素
    • 无论这个子元素是什么类型
    • 如果最后一个子元素不匹配选择器,则不会被选中
  2. :last-of-type 选择器

    • 选择父元素中最后一个指定类型的子元素
    • 只关注特定类型的元素
    • 即使是最后一个,但不是指定类型也不会被选中

举个例子:

<div>
<p>第一段</p>
<p>第二段</p>
<span>一个span</span>
</div>
  • div p:last-child 不会选中任何元素(因为最后一个子元素是 <span>
  • div p:last-of-type 会选中第二个 <p> 元素
  • div span:last-of-type 会选中最后的 <span>

简单来说:

  • :last-child 更严格,要求是最后一个子元素
  • :last-of-type 更灵活,只要是同类型的最后一个元素
提示

:last-of-type 只关注元素类型,不关注类名、ID 等属性,所以不会受到这些属性的影响。

比如:

<div>
<p class="red">第一段</p>
<p>第二段</p>
<p>第三段</p>
</div>

div p.red:last-of-type 不会选中任何元素,因为 p:last-of-type 已经选中最后一个 <p> 元素,但是这个元素没有 red 类名。

调整导入顺序

· 阅读需 2 分钟
1adybug
子虚伊人
  1. 安装插件

    npm i @ianvs/prettier-plugin-sort-imports glob -D
  2. 写入配置文件 prettier.config.mjs

    // @ts-check

    import { readFileSync } from "fs"
    import { parse } from "path"

    import { globSync } from "glob"

    /**
    * 数组去重
    * @template T - 数组的元素类型
    * @param {T[]} array - 输入的数组
    * @return {T[]} 新数组
    */
    function unique(array) {
    return Array.from(new Set(array))
    }

    const jsExts = [".js", ".jsx", ".ts", ".tsx", ".cjs", ".mjs", ".cts", ".mts", ".vue"]

    const assetExts = unique(
    globSync("**/*", {
    ignore: ["node_modules/**"],
    withFileTypes: true,
    cwd: import.meta.dirname,
    })
    .filter(path => path.isFile() && !jsExts.some(ext => path.name.toLowerCase().endsWith(ext)))
    .map(path => parse(path.name).ext.slice(1))
    .filter(ext => ext !== ""),
    )

    const assetExtsRegStr = `\\.(${assetExts.join("|")}|${assetExts.join("|").toUpperCase()})`

    const assetQueryRegStr = "(\\?[a-zA-Z0-9]+)?"

    const namespaces = unique(
    unique(
    globSync("**/package.json", {
    withFileTypes: true,
    cwd: import.meta.dirname,
    })
    .filter(path => path.isFile())
    .map(path => path.fullpath()),
    )
    .map(path => JSON.parse(readFileSync(path, "utf8")))
    .map(json =>
    Object.keys({
    ...json.dependencies,
    ...json.devDependencies,
    ...json.peerDependencies,
    ...json.optionalDependencies,
    }))
    .flat()
    .filter(dep => dep.startsWith("@"))
    .map(dep => dep.split("/")[0].slice(1)),
    )

    /**
    * @type {import("prettier").Options}
    */
    const config = {
    semi: false,
    tabWidth: 4,
    arrowParens: "avoid",
    printWidth: 160,
    plugins: ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
    importOrder: [
    "<BUILTIN_MODULES>",
    `^@(${namespaces.join("|")})/`,
    "<THIRD_PARTY_MODULES>",
    "",
    `^@.+?(?<!${assetExtsRegStr}${assetQueryRegStr})$`,
    `^\\.{1,2}/.+?(?<!${assetExtsRegStr}${assetQueryRegStr})$`,
    "",
    `^@.+?${assetExtsRegStr}${assetQueryRegStr}$`,
    `^\\.{1,2}/.+?${assetExtsRegStr}${assetQueryRegStr}$`,
    ],
    importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
    importOrderTypeScriptVersion: "5.0.0",
    importOrderCaseSensitive: true,
    }

    export default config
    注意

    prettier-plugin-tailwindcss 插件必须放置在 @ianvs/prettier-plugin-sort-imports 的后面

  3. 配置 package.json

    {
    "scripts": {
    "lint": "prettier --write ."
    }
    }
  4. 运行 npm run lint 即可调整导入顺序。

body 设置为 overflow:scroll 时,antd Modal 打开后会被改为 overflow:hidden

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

在某个项目中,为了让每个页面的宽度一致,不因滚动条而发生突变,设置 bodyoverflow: scroll

然而发现 antdModal 组件打开后,body 会被改为 overflow: hidden,导致页面滚动条消失。这是因为 antdModal 组件会在打开时给 body 添加一个 overflow: hidden 的样式,关闭时再移除,这会导致页面的宽度发生变化,观感不好

但是某些页面的 Modal 打开后又会自动添加 padding-right,不会让页面宽度发生变化,这是因为 antdModal 组件在打开时会判断页面是否有滚动条,如果有滚动条,会给 body 添加一个 padding-right,这样页面的宽度就不会发生变化

获取页面滚动条的宽度也很简单:

const width = document.documentElement.offsetWidth - document.documentElement.clientWidth