Skip to content

Commit 6546f27

Browse files
author
ma.jinyun
committed
update post
1 parent 010ad73 commit 6546f27

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
---
2+
title: 基于 Antd 二次开发,打造符合业务场景的 React 组件库
3+
date: 2025-04-29 19:49:58
4+
tags: ["React", "Ant Design", "Component Library", "DXP", "Frontend"]
5+
series: ["前端开发"]
6+
category: ["blog", "前端"]
7+
featured: true
8+
9+
---
10+
11+
基于 Ant Design 5 二次封装,打造更符合业务场景的 React 组件库 @digitalc/dxp-ui
12+
13+
## 前言
14+
在现代前端开发中,组件库扮演着至关重要的角色,它能显著提升开发效率、保证 UI 一致性。Ant Design 无疑是国内 React 生态中最受欢迎的组件库之一。但在公司产品和项目开发中,我们常常需要针对具体的业务场景下, 进行组件库UI方面的调整或定制。所以决定对其进行二次封装,以更好地满足公司产品特定需求、统一团队产品的风格。
15+
16+
本文将介绍我们基于 Ant Design 5 打造的二次封装组件库`@digitalc/dxp-ui` 。探讨其设计理念、核心特性,特别是怎么样实现动态 Token 注入、处理 Ant Design v4/v5 兼容性问题,以及在开发过程中的一些思考和实践。
17+
18+
## 为什么需要二次封装?
19+
Ant Design 提供了丰富的基础组件和强大的主题定制能力。但在大型项目或多业务线的场景下,直接使用 Ant Design 可能会遇到以下挑战:
20+
21+
1. 业务场景契合度 :某些业务场景需要更特定、更复杂的组件交互或样式,直接使用基础组件可能需要编写大量重复的样式,导致代码冗余。
22+
2. 设计规范统一 :团队内部通常有统一的设计规范(Design System),需要将这些规范融入组件库,确保所有产品的视觉风格保持一致,提高用户体验和品牌感。
23+
3. 主题管理复杂度 :虽然 Ant Design 提供了 Token 定制,但在多主题(如不同品牌、不同产品线)切换和管理上,需要更便捷、更中心化、更快速、更简单的管理方式(领导想要的方式)。
24+
4. 技术栈升级兼容 :当项目需要在不同版本的 Ant Design(如 v4 和 v5)之间过渡或共存时,需要组件库层面提供良好的兼容性支持。
25+
基于以上考虑,我们开始了`@digitalc/dxp-ui` 项目,旨在提供一套更贴合我们业务场景、易于维护和扩展的 React 组件库。
26+
27+
## @digitalc/dxp-ui 核心特性
28+
29+
`@digitalc/dxp-ui` 不仅仅是 Ant Design 的简单 token 包装,还包含了对业务需求痛点的深度思考和解决方案:
30+
31+
- 基于 Ant Design 5 :享受 Ant Design 5 带来的性能提升、CSS-in-JS 支持和更灵活的 Token 系统。
32+
- 业务场景组件 :除了对基础组件进行样式和行为的定制,还封装了更贴合业务流程的复合组件(例如,在关联包`packages/business` 中规划)。
33+
- 动态 Token 注入 :这是`@digitalc/dxp-ui` 的核心亮点之一。我们设计了一套机制,允许在运行时动态注入自定义的 Token JSON 数据,轻松实现全局或局部主题的切换,满足一套产品,面对不同客户、品牌、项目的视觉需求。
34+
- Ant Design v4/v5 兼容 :考虑到存量项目,按功能模块逐个升级, 可能仍在会同时使用 Ant Design v4,我们提供了详细的兼容处理方案,使得`@digitalc/dxp-ui` 可以在 v4 项目中平稳运行。
35+
36+
## 技术实现细节
37+
38+
### 1. 动态注入自定义 Token JSON
39+
Ant Design 5 的 Token 系统本身已经很强大了,但产品要求:希望实现更灵活的主题切换(虽然 Ant Design v5 官方提供了ConfigProvider机制,但并是很不适合我们,原因也在我们自己,因为 UI 设计前期是按客户需求定制,并且没有遵循antd的风格和规范做UI设计,是现有现有产品,后面才选择antd组件库,这样就导致我们的token 与antd token的映射关系不统一,无法直接粗暴的传入token即可改变主题)。为了解决这个问题,我们设计了`tokenManager` 模块实现了这一目标,它负责加载和应用自定义的 Token JSON 数据( 并且做了与 antd token的值映射关系)。
40+
41+
```tsx
42+
import { tokenManager } from '@digitalc/dxp-ui';
43+
44+
// 假设 customTokenJSON 是从 API 获取或本地加载的 Token 数据
45+
const customTokenJSON = {
46+
colorPrimary: '#00b96b',
47+
colorLink: '#00b96b',
48+
// ... 其他 DXP 或 Ant Design Token
49+
};
50+
51+
// 在应用初始化或需要切换主题时调用
52+
tokenManager.loadExternalToken(customTokenJSON);
53+
```
54+
55+
其内部实现原理大致如下:
56+
57+
- `tokenManager` : 维护一个全局的`tokenRef` 对象,存储当前的 Token 数据。`loadExternalToken` 方法会更新这个`tokenRef` 并触发一个自定义事件(如`xxx-token-updated` )。
58+
- `TokenContext` : 提供一个 React Context (`TokenContext` ) 和对应的 Provider (`GlobalTokenProvider` )。Provider 内部监听`xxx-token-updated` 事件,当事件触发时,更新 Context 的值(通常是一个版本号或时间戳)。
59+
- `useDynamicTokens` Hook : 组件内部通过此 Hook 订阅`TokenContext` 。当 Context 值变化时,Hook 会重新执行,并调用`tokenManager` 中更新后的`getToken` 方法来获取最新的 Token 值。
60+
- 组件应用 : 在组件(如`Button` )的样式计算或`designTokens.ts` 中,使用`useDynamicTokens` 获取 Token 值,并将其传递给 Ant Design 的`ConfigProvider` 或直接应用于组件样式。
61+
使得主题切换无需重新加载页面,实现了真正的动态响应。
62+
63+
```tsx
64+
// designTokens.js
65+
import { useDynamicTokens } from '@/utils';
66+
/*
67+
这是 dxp 的 UI token,缓存在本地的内置变量;
68+
通过antd 提供的 ConfigProvider 注入了组件级 token 变量来实现 gomo 风格的UI组件;
69+
70+
理论上可以通过2种方式来实现 antd 的样式定制:
71+
1. 直接写 /components/Button/style/button.less 文件通过 less 覆盖antd的样式,可控细节更多(相当于把 dxp 的 UI token 通过 less 的方式覆盖antd的样式);
72+
2. 通过 ConfigProvider 注入组件级别的 token 变量,这种不用写 less 文件,但是可控细节更少,只能antd 提供了哪些 token,才能覆盖对应的值;
73+
目前优先选择第二种,因为第一种需要写 less 文件,要知道对应组件的dom结构,才能覆盖;而第二种只需要对齐 变量值,就能覆盖;
74+
*/
75+
76+
const useDesignTokens = () => {
77+
const getToken = useDynamicTokens();
78+
return {
79+
// 将UI token 值,通过 useDynamicTokens 钩子获取,并映射给 antd 的 token
80+
colorText: getToken('colorStepperTextInactive'),
81+
colorTextDescription: getToken('colorStepperTextInactive'),
82+
colorTextLightSolid: getToken('colorStepperTextActive'),
83+
};
84+
};
85+
86+
export { useDesignTokens };
87+
88+
```
89+
90+
然后在渲染组件中使用这个钩子,获取设计 token,并传递给组件,并且自定义了prefixCls,以实现组件样式的定制,并且不冲突antd本身,如果不够用,就要用 cssinjs的方案。
91+
92+
```tsx
93+
import { designTokens } from './designTokens';
94+
95+
<ConfigProvider
96+
prefixCls={cssClasses.PREFIX}
97+
theme={{
98+
components: {
99+
Steps: {
100+
...designTokens,
101+
},
102+
},
103+
}}
104+
>
105+
<Steps
106+
{...props}
107+
prefixCls={prefixCls}
108+
style={{ ...otherDesignTokens, ...style }}
109+
className={className}
110+
items={processedItems}
111+
current={current}
112+
labelPlacement={labelPlacement}
113+
>
114+
{processedChildren}
115+
</Steps>
116+
</ConfigProvider>
117+
```
118+
cssinjs的方案,可以参考antd的官方文档.
119+
120+
```tsx
121+
import { useStyleRegister } from '@ant-design/cssinjs';
122+
import { designTokens } from './designTokens';
123+
124+
// 使用 useStyleRegister 注册自定义样式
125+
const useCustomToastStyle = () => {
126+
const {
127+
colorText,
128+
contentBg,
129+
contentPadding,
130+
borderRadiusLG,
131+
} = designTokens;
132+
133+
const hashId = useStyleRegister(
134+
{
135+
theme: theme,
136+
token: {},
137+
path: [prefixCls + '-toast'],
138+
},
139+
() => `
140+
div[class*="-message"] .${prefixCls} div[class*="-message-notice-content"] {
141+
background-color: ${contentBg};
142+
color: ${colorText};
143+
border-radius: ${borderRadiusLG}px;
144+
padding: ${contentPadding};
145+
}
146+
`,
147+
);
148+
return hashId;
149+
};
150+
151+
const hashId = useCustomToastStyle();
152+
153+
// 返回 Toast 组件,并设置 hashId style
154+
return <Toast prefixCls={prefixCls} hashId={hashId} {...props} />;
155+
156+
```
157+
158+
159+
### 2. Ant Design v4 和 v5 兼容性处理
160+
在 Ant Design v4 项目中使用基于 v5 的`@digitalc/dxp-ui` 是一个常见的痛点。在`README.md` 中提供了详细的使用方案,核心思路是利用 npm alias 和 Ant Design v5 的`prefixCls` 特性:
161+
162+
1. 安装依赖别名 : 同时安装`antd@5``antd@4` (使用别名如`antd4` )。
163+
```json
164+
{
165+
"dependencies": {
166+
"antd": "^5.x.x",
167+
"antd4": "npm:antd@^4.x.x"
168+
}
169+
}
170+
```
171+
172+
2. 配置构建工具 (以 Umi 3 脚手架为例) :
173+
- 关闭 Umi 对 antd 的默认处理 (`antd: false` )。
174+
- 可能需要配置`webpack-chain` 来处理`@digitalc/dxp-ui` 内部的 Less 文件编译(如果构建工具不支持自动处理 Less 时)。
175+
176+
3. 使用独立的`ConfigProvider` : 在应用根部或`@digitalc/dxp-ui` 组件外层包裹 Ant Design v5 的`ConfigProvider` ,并设置`prefixCls` (如`ant5` ),以隔离 v4 和 v5 的样式。
177+
```tsx
178+
import { ConfigProvider as Antd5ConfigProvider } from 'antd';
179+
180+
<Antd5ConfigProvider prefixCls="ant5">
181+
{/* 应用或 @digitalc/dxp-ui 组件 */}
182+
</Antd5ConfigProvider>
183+
```
184+
4. 按需引入 : 在代码中明确区分导入`antd` (v5) 和`antd4` 。
185+
有效避免版本冲突和样式污染,实现 v4 和 v5 的和谐共存。
186+
187+
### 3. 整体流程与项目结构
188+
`@digitalc/dxp-ui` 采用了 Monorepo 的结构(虽然当前示例只有一个`base components` 包),便于未来扩展业务组件库 (`business components` )。
189+
190+
- `packages/base` : 存放基础组件、工具函数和核心逻辑。
191+
- `src/components` : 各个基础组件的实现。
192+
- `src/utils` : 存放工具函数,如`tokenManager` ,`TokenContext` , 设备类型判断等。
193+
- `src/styles` : 全局样式或基础样式。
194+
- `.dumirc.ts` : dumi 的配置文件,用于生成组件文档和演示。
195+
- `.fatherrc.ts` : father 的配置文件,用于组件库的构建打包。
196+
- `packages/business` (规划中) : 存放基于`base` 组件封装的业务组件。
197+
开发流程遵循标准的 npm 组件库开发模式:
198+
199+
1. 组件开发 : 在`src/components` 中创建或修改组件,编写 TypeScript 代码和样式文件 (推荐使用 Less 并利用 Ant Design Token)。
200+
2. Token 定义 : 在组件的`designTokens.ts` 文件中,使用`useDynamicTokens` 将设计规范映射到 Ant Design Token。
201+
3. 文档编写 : 使用 Markdown 编写组件的 API 文档和使用示例 (`docs/demo` )。
202+
4. 本地调试 : 运行 dumi (`npm start` ) 进行本地预览和调试。
203+
5. 单元测试 : 编写 Jest 测试用例,确保组件功能正确。
204+
6. 构建打包 : 运行 (`npm run build` ) 生成适用于不同环境(ESM, UMD, Lib)的产物。
205+
7. 发布版本 : 使用 npm 发布到私有或公共仓库。
206+
8. 发布文档 : 使用 dumi 构建并发布到 GitHub Pages 或其他静态网站托管平台,供用户查看和下载。vercel 就是个不错的方案。
207+
208+
## 简单小结
209+
`@digitalc/dxp-ui` 是我们在 Ant Design 基础上,结合自身业务需求进行二次封装的一次成功实践。通过动态 Token 注入、精心的 v4/v5 兼容处理以及清晰的项目结构,我们构建了一个灵活、健壮且易于维护的组件库,理论上如果时间允许,建议还是纯自己开发,而不是使用第三方组件库是更好的方案,因为有一些dom结构,我们无法通过样式覆盖的方式来修改,只能通过js代码来修改,这样会增加开发成本和维护成本,目前也需要精心的v4/v5兼容处理和细心的Token注入处理差异点。
210+
211+
后续将继续完善迭代基础组件,并丰富基于组成组件拼装的业务组件库,持续优化开发体验和组件性能,使其更好地服务于我们的产品开发。
212+
213+
希望本文能对同样在进行组件库建设或 Ant Design 二次开发的同学有所启发。

0 commit comments

Comments
 (0)