基于Antd+Dumi的React组件库封装
在很多的公司里,随着业务的发展,前端研发团队都会基于公司业务的需求搭建一套公司内部的组件库,这不仅可以规范团队的开发标准,还可以降低研发的成本,提升整体的研发效率。全自研组件库是一个成本较大的选择,而且对于研发团队的水平也有很高的要求,更加友好的选择则是基于Antd这样的优秀开源组件库进行二次封装,搭建属于公司业务风格的内部组件库。
本文主要介绍基于Antd+Dumi的React组件库封装流程,Antd就不用过多介绍了,而Dumi是一套专门用于组件库研发的工具,整合了组件库开发与文档生成,并且使用起来也是非常容易上手。
一,初始化
首先创建一个组件库目录my-antd
:
$ mkdir my-antd && cd my-antd
然后使用dumi
创建工具,开始初始化搭建:
$ npx create-dumi
这里我们选择模板为React Library
,即组件库研发,npm客户端选择yarn
,最后输入组件库名称和描述完成初始化。
这里要注意的是我们的组件库名称:是package文件中的name,并不是组件库目录名。
生成的目录结构如下:
├── .dumi
├── .husky
├── docs # 组件库文档
├── node_modules
├── src # 组件库源码
├── .dumirc.ts # dumi配置文件
├── .editorconfig
├── .eslintrc.cjs
├── .fatherrc.ts # 组件库打包配置
├── .gitignore
├── .prettierignore
├── .prettierrc
├── README.md
├── package.json
├── tsconfig.json
└── yarn.lock
最后我们执行yarn dev
命令,启动项目,即可看到如下页面:
这里因为我们设置的组件库名称较长,对导航有些遮挡,后面可以对这里调整。
到这里,我们组件库的初始化就完成了,下面我们开始具体的搭建过程。
二,搭建组件库
1,dumirc配置
首先设置.dumirc.ts
配置文件:
import { defineConfig } from 'dumi';
import path from 'path'
export default defineConfig({
outputPath: 'docs-dist', // 文档构建输出目录
// 配置别名:在封装组件时,可以使用import { xxx } from '@zhangxiaofan/antd'
alias: {
'@zhangxiaofan/antd': path.join(__dirname, 'src'),
},
themeConfig: {
// 配置左上角logo名,默认是创建时配置的package的name,在这里可以自定义修改
name: 'MyAntd',
// 底部的信息栏
footer: 'Copyright © 2024 | zhangxiaofan',
// 配置导航栏上的内容,不配置时默认为约定式导航
nav: [
{
title: '指南',
link: '/guide/install',
},
{
title: '组件',
link: '/components',
},
{
title: '更新记录',
link: '/changelogs',
},
],
},
// 用于配置 Markdown 解析相关的行为
resolve: {
// 配置(组件、函数、工具等)Markdown 的解析目录。
atomDirs: [
{ type: 'component', dir: 'src/components' }, // 默认值
// { type: 'component', dir: 'src/utils' },
// { type: 'component', dir: 'src/hooks' },
],
},
});
重点说明以下几个配置:
alias
:关于别名配置,dumi
存在默认的配置就是将组件库名指向src
,所以这里我们其实可以不用配置也拥有相同的效果。nav
:关于导航栏,我们只需要配置顶层的路径即可,剩下的二级目录dumi
会自动查找对应目录下的md
文件进行文档生成。atomDirs
:配置原子资产(例如组件、函数、工具等)Markdown
的解析目录。它的默认值就包含src/components
,所以我们在组件中书写的md
案例内容可以被自动渲染到页面中。在有需要的情况下,我们还可以添加src/utils, src/hooks
等文档内容,这样这些公共方法和hook
的案例内容就可以渲染到组件库文档中,团队的其他成员就可以根据文档知道如何使用了。
修改.dumirc.ts
配置文件后,我们刷新页面即可看到新的内容:
2,fatherrc配置
.fatherrc.ts
配置文件:
import { defineConfig } from 'father';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: { output: 'dist', ignores: ['src/components/*/__demo__/**/*'] },
});
该配置文件主要用于组件库源码的构建配置,我们可以构建出不同格式的源文件,以及打包到指定的目录。一般我们只需要构建出ESM
格式资源即可,同时还需要加上ignores
配置,在进行组件库源码构建时排除组件中的demo
文件。
3,docs文档资源
docs
目录存储的就是组件库文档的内容,前面我们配置了nav
导航栏的内容,这里我们就需要建立对应的文档内容:
├── docs # 组件库文档资源
│ └── changelogs # 更新日志
│ ├── index.md
│ └── components # 组件文档
│ ├── index.md
│ └── guide # 基础指南
│ ├── develop.md # 参与贡献
│ ├── install.md # 安装
│ ├── standard.md # 开发规范
│ └── index.md # 文档首页
一般来说,基础指南,组件文档,更新日志。这三个模块为组件库文档最基本的内容,无论哪个组件库都应该建立这三部分的内容,当然如果还有其他需要的文档内容也可以继续添加。
以下文档内容仅为参考,在搭建组件库时可以自行调整。
- 首先设置文档首页的内容:
// docs/index.md
---
hero:
title: MyAntd
description: 一个基于Antd二次封装的组件库
actions:
- text: 快速上手
link: /guide/install
- text: gitlab
link: https://gitlab.com
features:
- title: 快速开发
emoji: 🚀
description: 让开发更快速、简单
- title: 'Ant Design'
emoji: 💎
description: 在 Ant Design基础上进行的封装,无缝对接 antd 项目
- title: TypeScript
emoji: 🌈
description: 使用TypeScript开发,提供完整的类型定义文件
---
- 设置基础指南的内容:
// guide/install.md
---
title: 安装
toc: content
group:
title: 快速上手
order: 1
---
# 安装
`@zhangxiaofan/antd@1.0` 是基于 `antd@4.x.x` 开发的。
## 前置依赖
请先检查项目中是否已安装以下依赖:
```
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"antd": ">=4.24.16"
```
## 镜像仓库
`@zhangxiaofan/antd@1.0` 应当发布到公司的npm私服中。
```bash
npm config set registry http://npm.xxx.com
# or
yarn config set registry http://npm.xxx.com
```
## 安装
```bash
npm install @zhangxiaofan/antd
# or
yarn add @zhangxiaofan/antd
```
这里只演示安装的例子,其他的文档内容可以自行创建。
- 设置组件的文档内容:
// components/index.md
---
category: Components
title: 概述
group:
title: 组件总览
order: 1
---
# 概述
`@zhangxiaofan/antd@1.0` 为公司应用提供了丰富的业务组件,大大提升了团队的开发效率。
以后在src/components
目录下封装组件,并书写对应的md
文档,就可以将组件案例及API
渲染到这里。
- 设置更新记录的文档内容:
// changelogs/index.md
---
title: 更新日志
toc: content
order: 1
---
# 更新日志
- 2024/10/30 `@zhangxiaofan/antd@1.0` v1.0.0 正式发布。
关于更新记录可以根据自己的需求进行不同的展示,比如按年份时间线,或者表格等等。
4,public目录
可以在根目录下创建public
,存放组件库文档需要的一些图片等静态资源。
5,scripts目录
可以在根目录下创建scripts
目录,创建一些脚本文件【比如同步更新组件库文档或者发送版本更新通知】。
6,src目录
src
目录下存放的是我们组件库源码,一般可以创建如下目录结构:
├── src
│ └── assets # 图标或者字体资源
│ └── components # 封装的组件源码
│ └── hooks # 封装的hook
│ └── utils # 封装的公共方法
│ └── ...
│ └── index.ts # 统一导出
// src/index.ts
export * from './components/index';
export * from './hooks/index';
export * from './utils/index';
在src
的入口文件中,将所有的资源全部暴露出去,然后我们就可以在组件中通过@zhangxiaofan/antd
引入任意资源并使用。
三,封装组件
上面的操作完成之后,我们的组件库框架基本就搭建完成了,下面我们开始组件的封装。
本次演示一个【操作按钮组:MyActions
】的封装。
1,编写组件
首先创建所需的目录及文件:
├── components
│ ├── MyActions
│ ├── __demo__ # 组件案例
│ ├── __test__ # 单元测试
│ └── ActionButton.tsx
│ └── Action.tsx
│ └── index.css
│ └── index.md # 组件文档
│ └── index.tsx # 组件入口
│ └── type.ts # 组件类型
│ └── index.ts # 统一导出【导出组件及类型】
- 封装的组件源码:
// Action.tsx
import { DownOutlined, EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, Space } from 'antd';
import React from 'react';
import ActionButton from './ActionButton';
import './index.css';
import { ItemProps, MyActionsProps } from './type';
const Actions: React.FC<MyActionsProps> = (props) => {
const { actionsType = 'link', limit = 2, items } = props;
// 默认显示与隐藏的按钮组【超过limit的按钮会被加入到hiddenItems】
let showItems: ItemProps[] = [];
let hiddenItems: ItemProps[] = [];
// 按钮处理
if (Array.isArray(items) && items.length) {
// 移除被隐藏的按钮
let results = items.filter((item) => !item.hidden);
results = results.map((item, index) => {
let disabled: boolean | undefined = false;
let content: string | undefined = '';
let actionType = actionsType;
// 对超过limit的按钮转换处理
if (actionsType === 'button') {
if (index >= limit) {
actionType = 'text';
}
}
// 禁用处理
if (Array.isArray(item.disabled)) {
// 找到首个disabled为true的禁用项
const firstDisable = item.disabled.find((item) => item.disabled);
if (firstDisable) {
disabled = firstDisable.disabled;
content = firstDisable.content;
}
} else {
disabled = item.disabled;
content = item.content;
}
return {
...item,
label: (
<ActionButton
key={index}
{...item} // 透传antd Button参数
actionType={actionType}
disabled={disabled}
content={content}
popover={{ placement: index >= limit ? 'left' : 'top' }}
>
{item.label}
</ActionButton>
),
};
});
// 设置数据
showItems = results.slice(0, limit);
hiddenItems = results.slice(limit);
}
return (
<Space>
{showItems.map((item) => {
return item.label;
})}
{hiddenItems.length > 0 && (
<Dropdown
menu={{
rootClassName: 'my-actions-menu-root',
items: hiddenItems.map((item, index) => {
return { key: String(index), label: item.label };
}),
}}
>
{actionsType === 'link' ? (
<a>
<EllipsisOutlined />
</a>
) : (
<Button>
更多
<DownOutlined />
</Button>
)}
</Dropdown>
)}
</Space>
);
};
export default Actions;
// ActionButton.tsx
import { Button, ButtonProps, Popover } from 'antd';
import React from 'react';
import { MyActionButton } from './type';
const ActionButton: React.FC<MyActionButton & ButtonProps> = (props) => {
const { children, content, popover, actionType, ...buttonProps } = props;
let myButton = null;
if (actionType === 'link') {
myButton = (
<Button
size="small"
type="link"
{...buttonProps}
style={{ ...buttonProps.style }}
>
{children}
</Button>
);
} else if (actionType === 'text') {
myButton = (
<Button
size="small"
type="text"
{...buttonProps}
style={{ ...buttonProps.style }}
>
{children}
</Button>
);
} else {
// 自定义按钮
myButton = <Button {...buttonProps}>{children}</Button>;
}
// 对禁用的按钮添加popover提示
if (content && buttonProps?.disabled) {
return (
<Popover
content={content}
trigger="hover"
placement="bottom"
{...popover}
>
{myButton}
</Popover>
);
}
return myButton;
};
export default ActionButton;
- 编写组件所需的
Typescript
类型:
// type.ts
import { ButtonProps, PopoverProps } from 'antd';
// 按钮禁用disabled属性的props
export type DisabledProps = {
disabled: boolean;
content?: string;
};
export type ItemProps = {
label: React.ReactNode;
disabled?: boolean | DisabledProps[];
content?: string;
hidden?: boolean;
onClick?: () => void;
// 加上ButtonProps,但需移除disabled的类型
} & Omit<ButtonProps, 'disabled'>;
export interface MyActionButton {
children?: React.ReactNode;
content?: string;
popover?: PopoverProps;
actionType?: 'button' | 'link' | 'text';
}
export interface MyActionsProps {
actionsType?: 'button' | 'link' | 'text';
limit?: number;
items: ItemProps[];
}
- 最后导出组件及类型:
// index.tsx
import { ComponentType } from 'react';
import ActionButton from './ActionButton';
import Actions from './Actions';
import { MyActionsProps } from './type';
type MyActionsComponent = ComponentType<MyActionsProps> & {
Button: typeof ActionButton;
};
const MyActions = Actions as MyActionsComponent;
MyActions.Button = ActionButton;
export default MyActions;
export type { MyActionsProps } from './type';
2,编写案例
在我们的组件封装完成之后,下一步应该编写组件使用的demo
。
我们可以在__demo__
目录下编写多个案例,每个案例最好使用单独的文件。
├── __demo__
│ └── basic.tsx
│ └── button.tsx
│ └── disable.tsx
│ └── table.tsx
│ └── type.tsx
│ └── ...
/**
* title: 基本使用
*/
// basic.tsx
import { MyActions } from '@zhangxiaofan/antd';
import React from 'react';
export default function Index() {
return (
<MyActions
items={[
{ label: '按钮1' },
{ label: '按钮2' },
{ label: '按钮3' },
{ label: '按钮4' },
{ label: '按钮5' },
]}
/>
);
}
实际开发中我们可以在封装组件时,同时编写案例进行验证。
3,编写API文档
在demo
编写完成之后,需要引入到当前组件的index.md
文件中,最后我们还需要在文档底部加上组件的API
属性内容:
---
title: MyActions - 操作
toc: content
group:
title: 操作
demo:
cols: 2
---
# MyActions - 操作按钮组
## 何时使用
MyActions:表格的操作列,表格批量操作按钮。
MyActions.Button: 根据业务场景选择性使用。
## 代码演示
<code src="./__demo__/basic.tsx"></code>
<code src="./__demo__/button.tsx"></code>
<code src="./__demo__/type.tsx"></code>
<code src="./__demo__/disable.tsx"></code>
<code src="./__demo__/table.tsx"></code>
## MyActions
| 参数 | 说明 | 类型 | 默认值 |
| ----------- | -------------------------------- | ----------------- | ------ |
| limit | 菜单显示几个按钮,其他隐藏到 ... | `number` | 2 |
| actionsType | 内置默认样式 | `link \| button ` | link |
| items | 按钮数据项 | `ItemsProps[]` | - |
### ItemsProps
| 参数 | 说明 | 类型 | 默认值 |
| ----------------------------------------------------------------- | ------------------------------------------- | ---------------------------- | ------ |
| label | 菜单数据 | `ReactNode` | - |
| disabled | 是否禁用 | `boolean \| DisabledProps[]` | - |
| content | 按钮禁用时的提示内容,disabled 为 true 可用 | `ReactNode` | - |
| hidden | 是否隐藏 | `boolean` | - |
| onClick | 单击事件 | `() => void` | - |
| [...ButtonProps](https://4x.ant.design/components/button-cn/#API) | 支持透传 antd Button 参数 | `ButtonProps` | - |
### DisabledProps
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ------------------------------------------- | ----------- | ------ |
| disabled | 是否禁用 | `boolean` | - |
| content | 按钮禁用时的提示内容,disabled 为 true 可用 | `ReactNode` | - |
## MyActionButton
| 参数 | 说明 | 类型 | 默认值 |
| ----------------------------------------------------------------- | ------------------------------------------- | -------------- | ------ |
| popover | 按钮提示透传,ItemsProps 暂未支持 | `PopoverProps` | - |
| disabled | 是否禁用 | `boolean` | - |
| content | 按钮禁用时的提示内容,disabled 为 true 可用 | `ReactNode` | - |
| [...ButtonProps](https://4x.ant.design/components/button-cn/#API) | 支持透传 antd Button 参数 | `ButtonProps` | - |
刷新页面,即可看到MyActions
组件的使用介绍:
注意:我们需要在
.dumi
目录下创建一个global.css
文件,并引入@import '~antd/dist/antd.css'
,这样组件库文档才能正确渲染antd组件的样式。
还可以查看相关的示例代码:
底部是组件的API
属性内容:
到此,一个组件的封装就基本完成了,后续其他的组件封装操作也是一样处理。
四,构建发布
当我们的组件库封装完成之后,最后一步就是需要将组件库进行打包构建:
# 执行打包命令
yarn build && yarn docs:build
在我们的组件库打包完成之后,即可进行发布:
# 发布
npm publish
这里要注意: 基于安全和可控性的考虑,用于公司内部的业务组件库我们应该发布到公司的npm
私服。
关于搭建npm私服可以参考《Linux系统使用Verdaccio搭建Npm私服》
所以在发布之前需要将npm
的默认地址切换到公司的私有地址,然后执行发布命令即可完成。
npm config set registry http://npm.xxx.com
# or
yarn config set registry http://npm.xxx.com
我这里为演示应用,就直接发布到npm仓库了。
# 切换到npm公共仓库
npm config set registry https://registry.npmjs.org
# 添加用户
npm adduser
运行上述命令后,npm
会提示你输入用户名、密码和电子邮件地址,按照提示完成登录过程。
我们可以使用以下命令来验证是否已经成功登录:
npm whoami
最后执行发布命令,即可将打包完成的组件库发布到npm
仓库中。
这里有个小插曲:@
符开头的包在npm中会被作为组织的包,所以使用个人账号无法直接发布,需要先创建一个【组织】这里选择免费计划即可,在创建组织时预期的名称被占用了,所以改为了zhangxiaofan1
,package
中包的名称和源码中所有地方都需要同步修改。
最简单的处理方法就是包的名称不使用@
前缀,这样发布包的流程就很简单了。
在修改完包名称之后,执行发布命令:
npm publish --access=public
发布成功后,我们可以在 npm 网站 搜索 @zhangxiaofan1/antd
来验证包是否已经成功发布。
扩展: 在组件库发布之后,我们可以创建一个测试项目并安装组件库,来检查组件库能否正常使用。
在本地搭建一个react-test
项目,然后添加组件库依赖包:
# 安装前置依赖
$ yarn add antd @ant-design/icons
# 安装组件库
$ yarn add @zhangxiaofan1/antd
编写一个使用案例,然后启动项目:
// App.tsx
import React from "react";
// 可以从中引入组件,hook和方法
import { MyActions, useCounter, sum } from "@zhangxiaofan1/antd";
export default function App() {
return (
<div className="App">
<div style={{fontWeight: 800}}>组件库使用测试</div>
<MyActions
limit={3}
items={[
{ label: "正常" },
{ label: "禁用", disabled: true, content: "按钮被禁用" },
{ label: "危险", danger: true },
{ label: "按钮4" },
{ label: "按钮5" }
]}
></MyActions>
</div>
);
}
最后我们可以看到组件使用成功,到此我们的组件库封装流程就全部完成。
五,总结
以上就是封装一个React组件库的全部流程了,整个搭建流程还是比较流畅的。
对于一个业务组件库而言,它实际上类似于一个完整的项目,需要经过长期的迭代和更新,才能逐步成熟并成为坚实的基础设施。从而帮助提升团队整体的开发效率,为公司的业务发展提供高质量的保障。
组件库源码已同步到 @zhangxiaofan1/antd 包里,感兴趣的可以自行下载了解。