移除 Ant Design 中默认的 a 元素样式
Ant Design 总是会给 a 元素设置默认样式,这样会导致在使用 a 元素时,样式不符合我们的预期。比如在 a 元素中设置了 color 为 red,但是在 Ant Design 中,a 元素的默认样式是 blue,这样就会导致我们的样式被覆盖。可以用以下方式去除默认样式:
a[href],
a[href]:active,
a[href]:hover {
color: inherit;
}
Ant Design 总是会给 a 元素设置默认样式,这样会导致在使用 a 元素时,样式不符合我们的预期。比如在 a 元素中设置了 color 为 red,但是在 Ant Design 中,a 元素的默认样式是 blue,这样就会导致我们的样式被覆盖。可以用以下方式去除默认样式:
a[href],
a[href]:active,
a[href]:hover {
color: inherit;
}
有的时候,某些函数类的参数类型是 () => (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), [])
大功告成!
在 react router(或者 remix)中使用 Ant Design 时,如果不对 css 进行处理,会导致首屏样式丢失的问题。之前介绍过整体导入的解决方案 在 Remix 中使用 Ant Design,但是这种方式会导致打包出来的 css 文件很大。本文介绍一种更优雅的解决方案:在 SSR 中按需提取 Ant Design 的 css:
需要在项目中暴露 entry.client.tsx 和 entry.server.tsx,如果已经暴露了,可以跳过这一步:
npx react-router reveal
bunx react-router reveal
pnpx react-router reveal
yarn dlx react-router reveal
安装相应依赖:
npm i @ant-design/cssinjs @ant-design/static-style-extract
bun i @ant-design/cssinjs @ant-design/static-style-extract
pnpm i @ant-design/cssinjs @ant-design/static-style-extract
yarn add @ant-design/cssinjs @ant-design/static-style-extract
在 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>
)
修改 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>,
)
})
修改 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)
})
}
首先是在服务端的 HTML 代码中插入了 __ANTD_STYLE_PLACEHOLDER__
!isBrowser && !isDev && "__ANTD_STYLE_PLACEHOLDER__"
然后是将 antd 的样式抽取为 style 标签
const cache = createCache()
renderToPipeableStream(
<StyleProvider cache={cache}>
<ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />
</StyleProvider>,
)
const css = extractStyle(cache)
将 __ANTD_STYLE_PLACEHOLDER__ 替换为抽取的 style 标签
chunk = String(chunk).replace("__ANTD_STYLE_PLACEHOLDER__", fromBot ? "" : extractStyle(cache))
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 的区别:
:last-child 选择器
: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 类名。
安装插件
npm i @ianvs/prettier-plugin-sort-imports glob -D
bun i @ianvs/prettier-plugin-sort-imports glob -D
pnpm i @ianvs/prettier-plugin-sort-imports glob -D
yarn add @ianvs/prettier-plugin-sort-imports glob -D
写入配置文件 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 的后面
配置 package.json:
{
"scripts": {
"lint": "prettier --write ."
}
}
运行 npm run lint 即可调整导入顺序。
在某个项目中,为了让每个页面的宽度一致,不因滚动条而发生突变,设置 body 为 overflow: scroll 时
然而发现 antd 的 Modal 组件打开后,body 会被改为 overflow: hidden,导致页面滚动条消失。这是因为 antd 的 Modal 组件会在打开时给 body 添加一个 overflow: hidden 的样式,关闭时再移除,这会导致页面的宽度发生变化,观感不好
但是某些页面的 Modal 打开后又会自动添加 padding-right,不会让页面宽度发生变化,这是因为 antd 的 Modal 组件在打开时会判断页面是否有滚动条,如果有滚动条,会给 body 添加一个 padding-right,这样页面的宽度就不会发生变化
获取页面滚动条的宽度也很简单:
const width = document.documentElement.offsetWidth - document.documentElement.clientWidth
clientWidth 总是返回 0例如一个元素样式如下:
.element {
width: 100px;
padding: 10px;
border: 5px solid black;
}
则 clientWidth = 120px (content 100px + padding 20px)
继续上面的例子:
offsetWidth = 130px (content 100px + padding 20px + border 10px)
clientWidth举个例子:
// 如果一个容器宽度为 200px,但内容实际宽度为 300px
const container = document.querySelector(".container")
console.log(container.clientWidth) // 200px
console.log(container.scrollWidth) // 300px
这些属性的主要应用场景:
clientWidth:计算元素的可视内容区域offsetWidth:获取元素实际占用的布局空间scrollWidth:检测内容是否溢出,实现横向滚动功能在实际开发中如何选择:
scrollWidth 和 clientWidthoffsetWidthclientWidth需要注意的是,这些值都是只读的,如果需要修改元素尺寸,应该使用 CSS 的 width、padding 等属性。
仅限 windows 版,一共两种方法:
在 nvm 的安装路径下,找到 settings.txt 文件,设置 node_mirror 与 npm_mirror 为国内镜像地址,在文件末尾加入:
阿里云镜像
node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: https://npmmirror.com/mirrors/npm/
腾讯云镜像
node_mirror: http://mirrors.cloud.tencent.com/npm/
npm_mirror: http://mirrors.cloud.tencent.com/nodejs-release/
阿里云镜像
nvm npm_mirror https://npmmirror.com/mirrors/npm/
nvm node_mirror https://npmmirror.com/mirrors/node/
腾讯云镜像
nvm npm_mirror http://mirrors.cloud.tencent.com/npm/
nvm node_mirror http://mirrors.cloud.tencent.com/nodejs-release/
新建文本文档,将以下内容复制到文本文档中:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Directory\Background\shell\wt]
@="在此处打开 Windows Terminal"
"Icon"="C:\\Users\\lenovo\\Pictures\\terminal.ico"
[HKEY_CLASSES_ROOT\Directory\Background\shell\wt\command]
@="wt.exe -d \"%V\""
[HKEY_CLASSES_ROOT\Directory\shell\wt]
@="在此处打开 Windows Terminal"
"Icon"="C:\\Users\\lenovo\\Pictures\\terminal.ico"
[HKEY_CLASSES_ROOT\Directory\shell\wt\command]
@="wt.exe -d \"%V\""
将 C:\\Users\\lenovo\\Pictures\\terminal.ico 替换为你的 Windows Terminal 程序或者 .ico 文件图标路径。
由于 Windows Terminal 经常会升级,所以如果使用 Windows Terminal 的绝对路径作为图标,可能会导致图标失效。
使用 UTF-16 LE 编码格式保存文件,将文件后缀名改为 .reg,双击运行即可。
新建文本文档,将以下内容复制到文本文档中:
Windows Registry Editor Version 5.00
[-HKEY_CLASSES_ROOT\Directory\Background\shell\wt]
[-HKEY_CLASSES_ROOT\Directory\shell\wt]
同样使用 UTF-16 LE 编码格式保存文件,将文件后缀名改为 .reg,双击运行即可。