跳到主要内容

7 篇博文 含有标签「antd」

查看所有标签

移除 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;
}

在 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))

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

Antd Design 的 Form 中自定义受控组件的使用

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

自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定:

  1. 提供受控属性 value 或其它与 valuePropName 的值同名的属性。

  2. 提供 onChange 事件或 trigger 的值同名的事件。

  3. 转发 ref 或者传递 id 属性到 dom 以支持 scrollToField 方法。

import { ChangeEvent, FC, forwardRef } from "react"

import { Button, Form, Input } from "antd"
import { useForm } from "antd/es/form/Form"
import FormItem from "antd/es/form/FormItem"
import { useInputState } from "soda-hooks"

type Info = {
name?: string
id?: string
}

type InfoItemProps = {
value?: Info
onChange?: (value: Info) => void
}

const InfoItem = forwardRef<HTMLDivElement, InfoItemProps>((props, ref) => {
const { value, onChange } = props

// 推荐使用 soda-hooks 的 useInputState
const [name, setName] = useInputState(value?.name)

const [id, setId] = useInputState(value?.id)

function onNameChange(e: ChangeEvent<HTMLInputElement>) {
setName(e.target.value)
// 将变化之后的 value 传递给外部,优先级为 state < props < 最新的变化的 value
onChange?.({ id, ...value, name: e.target.value })
}

function onIdChange(e: ChangeEvent<HTMLInputElement>) {
setId(e.target.value)
onChange?.({ name, ...value, id: e.target.value })
}

return (
<div ref={ref}>
<Input value={name} onChange={onNameChange} />
<Input value={id} onChange={onIdChange} />
</div>
)
})

type FormData = {
info: Info
}

const App: FC = () => {
const [form] = useForm<FormData>()

return (
<Form<FormData> form={form} onFinish={console.dir}>
<FormItem<FormData> name="info">
<InfoItem />
</FormItem>
<FormItem<FormData>>
<Button onClick={() => form.setFieldsValue({ info: undefined })}>Reset</Button>
</FormItem>
<FormItem<FormData>>
<Button htmlType="submit">Submit</Button>
</FormItem>
</Form>
)
}

export default App
提示
  1. onChange 是会一直变化的,所以需要获取到最新值
  2. onChangeform.setFieldsValue 一样,都是同步的

Ant Design 的 Form 中 requiredMark 的使用

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

之前一直都是使用 css 来实现必填组件的 * 号的隐藏,没想到 Ant Design 官方提供了修改的方法:

import React, { FC, Fragment, useState } from "react"

import { InfoCircleOutlined } from "@ant-design/icons"
import { Button, Form, Input, Radio, Tag } from "antd"
import { useForm } from "antd/es/form/Form"
import FormItem from "antd/es/form/FormItem"

type RequiredMark = boolean | "optional" | "customize"

const customizeRequiredMark = (label: React.ReactNode, { required }: { required: boolean }) => (
<Fragment>
{required ? <Tag color="error">Required</Tag> : <Tag color="warning">optional</Tag>}
{label}
</Fragment>
)

const App: FC = () => {
const [form] = useForm()
const [requiredMark, setRequiredMarkType] = useState<RequiredMark>("optional")

const onRequiredTypeChange = ({ requiredMarkValue }: { requiredMarkValue: RequiredMark }) => {
setRequiredMarkType(requiredMarkValue)
}

return (
<Form
form={form}
layout="vertical"
initialValues={{ requiredMarkValue: requiredMark }}
onValuesChange={onRequiredTypeChange}
requiredMark={requiredMark === "customize" ? customizeRequiredMark : requiredMark}
>
<FormItem label="Required Mark" name="requiredMarkValue">
<Radio.Group>
<Radio.Button value>Default</Radio.Button>
<Radio.Button value="optional">Optional</Radio.Button>
<Radio.Button value={false}>Hidden</Radio.Button>
<Radio.Button value="customize">Customize</Radio.Button>
</Radio.Group>
</FormItem>
<FormItem label="Field A" required tooltip="This is a required field">
<Input placeholder="input placeholder" />
</FormItem>
<FormItem
label="Field B"
tooltip={{
title: "Tooltip with customize icon",
icon: <InfoCircleOutlined />,
}}
>
<Input placeholder="input placeholder" />
</FormItem>
<FormItem>
<Button type="primary">Submit</Button>
</FormItem>
</Form>
)
}

export default App

requiredMark 有四种可以传递的值:

  1. true 默认值

  2. false 不显示是否必填

  3. "optional" 在可选表单项的 label 后面添加 (可选)

  4. (label: ReactNode, { required }: { required: boolean }) => ReactNode 自定义渲染函数

在 Remix 中使用 Ant Design

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

Remix.js 中使用 Ant Design 会出现首次渲染样式丢失的问题,参考 Ant Design 官方的解决方案 整体导出

npm i @ant-design/static-style-extract
npm i cross-env tsx -D
{
"scripts": {
"predev": "tsx ./scripts/genAntdCss.tsx",
"prebuild": "cross-env NODE_ENV=production tsx ./scripts/genAntdCss.tsx"
},
"dependencies": {
"@ant-design/static-style-extract": "^1.0.2"
},
"devDependencies": {
"cross-env": "^7.0.3",
"tsx": "^4.15.6"
}
}
import fs from "fs"

import { extractStyle } from "@ant-design/static-style-extract"
import { ConfigProvider } from "antd"

const outputPath = "./app/antd.min.css"

const css = extractStyle(node => <ConfigProvider theme={{ token: { colorPrimary: "red" } }}>{node}</ConfigProvider>)

fs.writeFileSync(outputPath, css)
import "./antd.min.css"

如果有自定义主题的需求,只需要传递给 ConfigProvider 相应的配置即可:

const css = extractStyle(node => <ConfigProvider theme={{ token: { colorPrimary: "red" } }}>{node}</ConfigProvider>)
注意

这种办法只能是妥协之计,打包出来的 css 文件很大。具体的优化还需要官方实现

Ant Design 与 Tailwind 的样式冲突问题

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

参考 Ant Design 官网:样式兼容

Ant DesignCSS-in-JS 默认通过 :where 选择器降低 CSS Selector 优先级,以减少用户升级时额外调整自定义样式的成本,不过 :where 语法的兼容性在低版本浏览器比较差。在某些场景下你如果需要支持旧版浏览器(或与 TailwindCSS 优先级冲突),你可以使用 @ant-design/cssinjs 取消默认的降权操作(请注意版本保持与 antd 一致):

import { FC } from "react"

import { StyleProvider } from "@ant-design/cssinjs"

// `hashPriority` 默认为 `low`,配置为 `high` 后,
// 会移除 `:where` 选择器封装
const App: FC = () => (
<StyleProvider hashPriority="high">
<MyApp />
</StyleProvider>
)

export default App

切换后,样式将从 :where 切换为类选择器:

:where(.css-bAMboO).ant-btn {
}

.css-bAMboO.ant-btn {
color: #fff;
}