基于Antd+Dumi的React组件库封装

0 阅读12分钟

基于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,最后输入组件库名称和描述完成初始化。

image-20241027125028900

这里要注意的是我们的组件库名称:是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命令,启动项目,即可看到如下页面:

image-20241030222507553

这里因为我们设置的组件库名称较长,对导航有些遮挡,后面可以对这里调整。

到这里,我们组件库的初始化就完成了,下面我们开始具体的搭建过程。

二,搭建组件库

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配置文件后,我们刷新页面即可看到新的内容:

image-20241030231843763

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开发,提供完整的类型定义文件
---

image-20241030233838202

  • 设置基础指南的内容:
// 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
```

image-20241101213811246

这里只演示安装的例子,其他的文档内容可以自行创建。

  • 设置组件的文档内容:
// components/index.md
---
category: Components
title: 概述
group:
  title: 组件总览
  order: 1
---

# 概述

`@zhangxiaofan/antd@1.0` 为公司应用提供了丰富的业务组件,大大提升了团队的开发效率。

image-20241031000658546

以后在src/components目录下封装组件,并书写对应的md文档,就可以将组件案例及API渲染到这里。

  • 设置更新记录的文档内容:
// changelogs/index.md
---
title: 更新日志
toc: content
order: 1
---

# 更新日志

- 2024/10/30 `@zhangxiaofan/antd@1.0`  v1.0.0 正式发布。

image-20241031001325404

关于更新记录可以根据自己的需求进行不同的展示,比如按年份时间线,或者表格等等。

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组件的样式。

image-20241101224920130

还可以查看相关的示例代码:

image-20241101225032608

底部是组件的API属性内容:

image-20241101225158650

到此,一个组件的封装就基本完成了,后续其他的组件封装操作也是一样处理。

四,构建发布

当我们的组件库封装完成之后,最后一步就是需要将组件库进行打包构建:

# 执行打包命令
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中会被作为组织的包,所以使用个人账号无法直接发布,需要先创建一个【组织】这里选择免费计划即可,在创建组织时预期的名称被占用了,所以改为了zhangxiaofan1package中包的名称和源码中所有地方都需要同步修改。

最简单的处理方法就是包的名称不使用@前缀,这样发布包的流程就很简单了。

在修改完包名称之后,执行发布命令:

npm publish --access=public

image-20241102152711485

发布成功后,我们可以在 npm 网站 搜索 @zhangxiaofan1/antd 来验证包是否已经成功发布。

image-20241102154109795

扩展: 在组件库发布之后,我们可以创建一个测试项目并安装组件库,来检查组件库能否正常使用。

在本地搭建一个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>
  );
}

image-20241102160514978

最后我们可以看到组件使用成功,到此我们的组件库封装流程就全部完成。

五,总结

以上就是封装一个React组件库的全部流程了,整个搭建流程还是比较流畅的。

对于一个业务组件库而言,它实际上类似于一个完整的项目,需要经过长期的迭代和更新,才能逐步成熟并成为坚实的基础设施。从而帮助提升团队整体的开发效率,为公司的业务发展提供高质量的保障。

组件库源码已同步到 @zhangxiaofan1/antd 包里,感兴趣的可以自行下载了解。