基于monorepo的antd二次开发调试方案
最近在做 Ant Design 的二次开发,需要深度定制一些组件,甚至要改底层的 rc-* 组件。这个过程并不是很顺利,遇到了很多流程上的问题。
本文将介绍一种基于monorepo 二开antd代码的方案,解决antd 二开遇到的一些问题。
Ant Design 的架构设计
先简单介绍一下 Ant Design 的整体架构。
Ant Design 采用了分层设计,将组件拆分为两层:
- 上层组件:
antd包,提供完整的 UI 组件,包含样式、主题、国际化等 - 底层组件:
rc-*系列包(如rc-menu、rc-tooltip、rc-dropdown等),提供核心的交互逻辑和状态管理
这种设计的好处是职责分离:rc-* 组件专注于交互逻辑,antd 组件负责样式和用户体验。但问题在于,这些包都是独立的 Git 仓库和 npm 包:
ant-design/ant-design # antd 主仓库
react-component/menu # rc-menu 仓库
react-component/tooltip # rc-tooltip 仓库
react-component/dropdown # rc-dropdown 仓库
react-component/... # 还有几十个 rc-* 仓库每个包都有自己的:
- 独立的 Git 仓库和版本管理
- 独立的 npm 发布流程
- 独立的构建配置和依赖关系
举个例子,一个简单的 Dropdown 组件,实际上涉及多个包的协作:
antd/dropdown
└── rc-dropdown
├── rc-trigger
└── rc-menu
└── rc-motion二次开发的痛点
当我们需要对 Ant Design 进行二次开发时,这种架构就带来了不少麻烦。
场景 1: 修改上层组件
如果只是修改 antd 的样式或配置,看起来还算简单:
- Fork
ant-design/ant-design仓库 - 修改代码
- 构建发布
但实际上,Ant Design 内置了一套基于 dumi 的文档系统,使用 MDX 格式编写组件文档和示例。这套系统在编写文档时确实很方便,但在开发调试时就比较麻烦了。
举个例子,Button 组件的文档页面大概是这样的:
# Button 按钮
## 代码演示
<code src="./demos/basic.tsx">基础按钮</code>
<code src="./demos/type.tsx">按钮类型</code>
<code src="./demos/size.tsx">按钮尺寸</code>
<code src="./demos/loading.tsx">加载状态</code>
<code src="./demos/disabled.tsx">禁用状态</code>
<!-- ... 还有十几个 demo -->一个组件页面会同时渲染十几个 demo,这就带来了几个问题:
日志混乱:所有 demo 的
console.log都输出到同一个控制台,调试时很难分辨是哪个 demo 的日志热更新慢:修改一个 demo 的代码,整个页面的所有 demo 都要重新渲染,等待时间长
状态干扰:多个 demo 可能会相互影响,特别是涉及全局状态或事件监听时
性能问题:页面加载时要同时初始化十几个组件实例,首次加载很慢
难以聚焦:想专注测试某个特定功能时,其他 demo 会分散注意力
比如我要调试一个 Button 的 loading 状态问题,打开页面后:
- 要滚动到对应的 demo 位置
- 控制台里混杂着其他 demo 的日志
- 改完代码后,整个页面的 20 个 Button demo 都要重新渲染
- 等了 3-5 秒才能看到效果
这种体验在快速迭代和调试时真的很影响效率。
场景 2: 修改底层组件
但如果需要修改底层的 rc-* 组件,就麻烦了。比如我遇到的一个问题:Tooltip 在全屏模式下渲染位置不对,需要修改 rc-tooltip 的源码。
传统的开发流程是这样的:
- Fork
react-component/tooltip仓库 - 克隆到本地,修改代码
- 在 rc-tooltip 目录执行
npm link - 在 antd 目录执行
npm link rc-tooltip - 构建 rc-tooltip
- 构建 antd
- 在业务项目中
npm link antd - 启动业务项目测试
每次修改代码后,都要重复步骤 5-8。如果涉及多个 rc-* 组件,link 关系会更复杂:
# 在各个包的目录中执行
cd rc-tooltip && npm link
cd rc-dropdown && npm link
cd rc-menu && npm link
# 在 antd 目录中链接
cd antd
npm link rc-tooltip
npm link rc-dropdown
npm link rc-menu
# 在业务项目中链接
cd my-project
npm link antd这种方式存在几个问题:
- 每次改完代码都要
npm link来链接本地包 - 修改源码后必须重新构建才能看到效果
- 调试多个相互依赖的包时,link 来 link 去容易出错
- 热更新基本不可用,改一行代码要等半天
- 版本管理混乱,不知道改了哪些包的哪些代码
- 团队协作困难,每个人都要在本地配置一遍 link
场景 3: 追踪上游更新
更麻烦的是,当上游发布新版本时,我们需要:
- 在每个 fork 的仓库中手动同步上游更新
- 解决合并冲突
- 重新构建和测试
- 确保各个包的版本兼容性
如果 fork 了 5 个仓库,就要重复 5 次这个流程。
解决方案
为了解决这些痛点,探究了一种基于 pnpm workspace + Vite + git subtree 搭建antd开发调试环境。核心思路是:
- 用 git subtree 将所有上游代码引入到一个 monorepo 中,统一管理
- 用 pnpm workspace 自动处理包之间的依赖关系,无需手动 link
- 用 Vite 提供极速的开发服务器,支持 HMR
现在改代码就像写普通项目一样丝滑,热更新响应时间不到 100ms,再也不用等构建了。
这篇文章主要整理一下这套方案的实现思路和使用方式。
monorepo结构
先看看项目结构:
antd-monorepo/
├── packages/
│ ├── antd/ # 主组件库(基于 Ant Design 二次开发)
│ │ └── components/ # 通过 git subtree 从 ant-design/ant-design 引入
│ └── rc-xxx/ # rc 组件源码(通过 git subtree 管理)
│ └── src/ # 通过 git subtree 从 react-component/xxx 引入
├── demo/ # 调试项目(本方案核心)
│ ├── src/
│ │ ├── routes/ # 按组件组织的演示页面
│ │ │ ├── button/
│ │ │ │ └── demos/ # Button 组件的各种用例
│ │ │ └── splitter/
│ │ │ └── demos/ # Splitter 组件的各种用例
│ │ └── main.tsx
│ ├── vite.config.ts # Vite 配置(核心)
│ └── package.json
└── pnpm-workspace.yaml整个方案的核心思路是:
- 用 pnpm workspace 管理 monorepo,自动处理包之间的依赖关系
- 用 git subtree 引入上游源码,既能追踪版本历史,又能方便地合并更新
- 用 Vite 提供极速的开发服务器,支持 HMR
- 在 demo 项目中按组件组织测试用例,每个 demo 都是一个独立的 React 组件
这套方案用下来,确实解决了不少痛点:
- 无需手动
npm link,pnpm workspace 自动处理依赖关系,支持多层级依赖 - Vite 原生 ESM 支持,启动速度快,HMR 响应时间 < 100ms
- 每个 demo 独立运行,互不干扰,可以针对特定 bug 创建最小复现用例
- TypeScript 类型提示完整,支持 CSS Modules / UnoCSS,可以直接使用 React DevTools 调试
- 使用相同的构建工具链和模块解析规则,减少"本地能跑,发布后出问题"的情况
- git subtree 完整的版本历史追踪,清晰记录每次二开修改,支持选择性合并上游更新
- 所有代码在一个仓库,团队成员克隆一次即可获得完整代码
下面介绍一下核心的思路
git subtree
传统的组件库二次开发通常会 fork 仓库,但这样做有几个问题:
- 上游更新很难合并,经常要手动解决一堆冲突
- 无法在一个仓库中统一管理多个依赖包
- 代码对比和冲突解决都很麻烦
我们采用 git subtree 方案,将上游仓库的代码作为子目录引入。这样做的好处是:
- 所有代码在一个 monorepo 中统一管理
- 保留完整的 git 历史,方便追溯变更
- 支持双向同步:既能拉取上游更新,也能推送修改回上游(如果需要贡献代码)
- 清晰的二开代码对比和版本管理
看起来学习成本会高一点,但用起来确实比 fork 方便多了。
仓库映射
对应的目录与仓库映射关系
| 本地路径 | 上游仓库 | 用途 |
|---|---|---|
packages/antd/components | ant-design/ant-design | Ant Design 组件源码 |
packages/rc-menu | react-component/menu | Menu 底层实现 |
packages/rc-tooltip | react-component/tooltip | Tooltip 底层实现 |
packages/rc-dropdown | react-component/dropdown | Dropdown 底层实现 |
packages/rc-* | react-component/* | 其他 rc 组件 |
这里只列举了需要二开的rc-*组件,如果没有定制需求,则使用npm官方源默认的版本即可。
vite 开发调试
前面提到,Ant Design 自带的 dumi 文档系统更适合编写文档,但在开发和调试源码时存在不少问题。那么为什么选择 Vite 来替代呢?
dumi 的局限性
dumi 作为一个文档工具,它的设计目标是展示组件的各种用法,而不是为了快速开发调试。具体来说:
文档导向的架构
- dumi 需要解析 MDX 文件,处理文档元数据、目录结构等
- 每个组件页面会同时加载十几个 demo,这对文档展示很友好,但对开发调试来说是负担
- 启动时间长,通常需要 10-20 秒才能看到页面
构建流程复杂
- dumi 基于 Umi 框架,有自己的一套构建流程和约定
- 需要处理路由生成、主题配置、插件系统等
- 热更新需要重新编译整个文档站点,速度慢
调试体验差
- 一个页面渲染多个 demo,日志混乱
- 修改代码后,所有 demo 都要重新渲染
- 难以针对单个功能点进行快速验证
Vite 的优势
相比之下,Vite 作为一个纯粹的开发工具,更适合我们的场景:
极速冷启动
- Vite 利用浏览器原生 ESM,无需打包即可启动
- 启动时间通常在 1-2 秒内
- 按需编译,只处理当前访问的文件
快速热更新
- 基于 ESM 的 HMR,只更新修改的模块
- 响应时间 < 100ms,几乎是即时的
- 不会影响其他未修改的代码
简单直接
- 没有复杂的文档系统,就是一个普通的 React 应用
- 可以用熟悉的 React Router 组织路由
- 配置简单,专注于开发体验
灵活的 demo 组织
- 每个 demo 是独立的路由页面,互不干扰
- 可以针对特定功能创建最小复现用例
- 日志清晰,调试方便
为什么不直接改造 dumi
你可能会问,为什么不直接优化 dumi 的开发体验呢?主要有几个原因:
- 设计目标不同:dumi 是为了生成文档站点,而我们需要的是开发调试环境
- 改造成本高:要改造 dumi 的构建流程和渲染逻辑,工作量很大
- 维护负担:改造后的 dumi 需要持续维护,跟上游版本同步会很麻烦
- 灵活性差:dumi 有很多约定和限制,不如从零搭建一个简单的 Vite 项目灵活
所以最终选择了用 Vite 单独搭建一个开发调试环境,保持 dumi 用于文档生成,两者各司其职。
Git Subtree 使用指南
初次引入上游代码
第一次引入上游代码时,使用 git subtree add 命令:
# 引入 Ant Design 源码(指定版本 5.x.x)
git subtree add --prefix=packages/antd \
https://github.com/ant-design/ant-design.git v5.22.0 --squash
# 引入 rc-menu 源码
git subtree add --prefix=packages/rc-menu \
https://github.com/react-component/menu.git v9.15.0 --squash
# 引入 rc-tooltip 源码
git subtree add --prefix=packages/rc-tooltip \
https://github.com/react-component/tooltip.git v6.2.0 --squash这里有几个参数需要注意:
--prefix: 指定代码存放的本地目录--squash: 压缩上游的所有提交为一个,保持本地历史清晰- 版本号: 使用 tag 或 branch 名称,建议锁定稳定版本
更新上游代码
当上游发布新版本时,可以选择性合并(由于是内部的二开组件库,除了非常严重的bug,一般不会轻易升级):
# 更新 Ant Design 到 5.23.0
git subtree pull --prefix=packages/antd \
https://github.com/ant-design/ant-design.git v5.23.0 --squash
# 更新 rc-menu 到最新版本
git subtree pull --prefix=packages/rc-menu \
https://github.com/react-component/menu.git v9.16.0 --squashgit 会自动对比本地修改和上游更新,如果有冲突会提示手动解决。解决冲突后提交,就完成合并了。
Vite 本地开发
由于antd自带的dumi,更适合用来编写文档,在开发和调试源码方面,并不合适。
接下来看看 Vite 这边的配置。
Workspace 依赖配置
在 demo/package.json 中通过 workspace:* 协议引用本地包:
{
"dependencies": {
"antd": "workspace:*"
}
}这样 pnpm 会自动将 packages/antd 软链接到 demo/node_modules/antd,无需手动 link。
路径转换插件
这里有个坑:rc-* 组件在发布时使用 /lib/ 目录,但vite开发时需要使用 /es/ 目录(ESM 格式)。
我们在 Vite 中添加了自定义插件来处理:
{
name: 'replace-lib-to-es',
transform(code, id) {
if (/\.(j|t)sx?$/.test(id)) {
const transformed = code.replace(
/from\s+(['"])([^'"]*\/rc-[^'"]+)\/lib\//g,
(_, quote, path) => `from ${quote}${path}/es/`
)
if (transformed !== code) {
return { code: transformed, map: null }
}
}
return null
}
}这个插件会自动将所有 rc-*/lib/ 的导入路径转换为 rc-*/es/,确保 Vite 能正确处理 ESM 模块,同时避免了修改源码的lib写法。
按组件组织演示用例
采用约定式路由结构,每个组件独立管理其演示用例:
routes/
├── button/
│ ├── demos/
│ │ ├── Basic.tsx # 基础用法
│ │ ├── Type.tsx # 按钮类型
│ │ └── Size.tsx # 按钮尺寸
│ └── routes.tsx # 路由配置
└── splitter/
├── demos/
│ └── fullscreen.tsx # 全屏功能演示
└── routes.tsx每个 demo 文件都是一个独立的 React 组件,可以专注于测试特定功能点。
启动和调试
整个流程非常简单:
启动开发服务器
bashcd demo pnpm dev修改组件源码
- 编辑
packages/antd/components/xxx/index.tsx - 或编辑
packages/rc-xxx/src/xxx.tsx
- 编辑
实时预览效果
- Vite 自动检测文件变化
- HMR 即时更新浏览器页面
- 无需手动刷新或重新构建
添加新的测试用例
- 在
demo/src/routes/xxx/demos/下创建新文件 - 在路由配置中注册
- 立即可访问测试
- 在
一个实际案例
举个例子,我之前遇到 Splitter 组件在全屏模式下 Tooltip 位置不对的问题。下面是测试用例:
// demo/src/routes/splitter/demos/fullscreen.tsx
export function Component() {
const [isFullscreen, setIsFullscreen] = useState(false);
const leftPanelRef = useRef<HTMLDivElement>(null);
const handleFullscreen = async () => {
if (!isFullscreen) {
await leftPanelRef.current?.requestFullscreen();
setIsFullscreen(true);
} else {
await document.exitFullscreen();
setIsFullscreen(false);
}
};
return (
<ConfigProvider getPopupContainer={() => document.fullscreenElement || document.body}>
<Splitter layout="vertical">
<Splitter.Panel>
<div ref={leftPanelRef}>
<Button onClick={handleFullscreen}>
{isFullscreen ? '退出全屏' : '全屏'}
</Button>
<Tooltip title="测试内容" placement="top">
<Button>编辑</Button>
</Tooltip>
</div>
</Splitter.Panel>
</Splitter>
</ConfigProvider>
);
}这个用例专门测试全屏模式下 Tooltip 的渲染容器问题,可以快速验证修复方案。改完源码,浏览器立刻就能看到效果,非常方便。
小结
通过 git subtree + pnpm workspace + Vite 的组合,我们搭建了一个高效的 Ant Design 二次开发调试环境。
这套方案的核心价值在于:
- Vite 的极速 HMR,修改即生效(< 100ms),无需 npm link,workspace 自动关联,一键启动即可调试所有组件
- git subtree 保留完整历史,清晰追踪二开代码,支持选择性合并上游更新,可对比任意版本差异
- Monorepo 统一管理,依赖关系清晰,标准化的 commit 规范和 PR 流程,所有代码在一个仓库
- 按需添加测试用例,支持复杂场景模拟,易于集成测试框架、文档工具等
与传统的 fork 仓库或 npm link 方案相比,这套方案在上游更新、版本对比、多包管理、历史追踪等方面都有明显优势。虽然学习成本稍高,但用起来确实比较方便。
这套方案比较适合需要深度定制 Ant Design 或 rc-* 组件、需要长期维护自己的组件库版本、团队规模 > 3 人需要协作开发、需要追踪和对比二开代码的场景。
如果只是简单使用 Ant Design 不需要修改源码,或者临时性的小项目,就没必要搞这么复杂了。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
