React组件库搭建

702 阅读12分钟

一、概述

基于dumi2搭建React组件库和函数库,部署文档站点以及发布npm包。

二、dumi

dumi,是一款为组件开发场景而生的静态站点框架,与 father 一起为开发者提供一站式的组件开发体验,father 负责组件源码构建,而 dumi 负责组件开发及组件文档生成

2.1 环境准备

确保正确安装 Node.js 且版本为 14+ 即可。

$ node -v
v14.19.1
2.2 脚手架
# 先找个地方建个空目录。
$ mkdir myapp && cd myapp


# 通过官方工具创建项目,选择你需要的模板
$ npx create-dumi


# 选择一个模板
$ ? Pick template type › - Use arrow-keys. Return to submit.
$ ❯   Static Site # 用于构建网站
$     React Library # 用于构建组件库,有组件例子
$     Theme Package # 主题包开发脚手架,用于开发主题包


# 安装依赖后启动项目
$ npm start
2.3 目录结构
├── docs               # 组件库文档目录
│   ├── index.md       # 组件库文档首页
│   ├── guide          # 组件库其他文档路由表(示意)
│   │   ├── index.md
│   │   └── help.md
├── src                # 组件库源码目录
│   ├── Foo            # 单个组件
│   │   ├── index.tsx  # 组件源码
│   │   ├── index.less # 组件样式
│   │   └── index.md   # 组件文档
│   └── index.ts       # 组件库入口文件
├── .dumirc.ts         # dumi文档的配置文件
└── .fatherrc.ts       # 组件库打包npm包的配置文件
2.4 站点配置

修改项目名称,定义菜单项

在.dumirc.ts文件中添加配置:

import { defineConfig } from 'dumi';

export default defineConfig({
  outputPath: 'docs-dist',
  themeConfig: {
    name: 'demo-ui',
    footer: false,
    nav: [
      { title: '指南', link: '/guide' },
      { title: '工具', link: '/utils' },
      { title: '组件', link: '/components' },
    ],
  },
});

三、开发基础组件

3.1 添加组件源代码

这里先新增一个简单的Button组件,在src下面新增Button文件内写入index.tsx文件。

mkdir src/Button && touch src/Button/index.tsx

在文件中新增一个简单的Button组件代码:

import React, { memo } from 'react';
import './styles/index.less' // 引入样式
export interface ButtonProps {
  /** 按钮类型 */
  type?: 'primary' | 'default';
  /** 按钮文字 */
  children?: React.ReactNode;
  onClick?: React.MouseEventHandler<HTMLButtonElement>
}

/** 按钮组件 */
const Button: React.FC<ButtonProps> = (props) => {
  const { type = 'default', children, onClick } = props
  return (
    <button
      type='button'
      className={`dumi-btn ${type ? 'dumi-btn-' + type : ''}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

export default memo(Button);

3.2 添加less样式和变量

在Button组件库下面新建styles文件夹,里面新建index.less文件

mkdir src/Button/styles && touch src/Button/styles/index.less

添加样式文件


.dumi-btn {
  font-size: 14px;
  height: 32px;
  padding: 4px 15px;
  border-radius: 6px;
  transition: all .3s;
  cursor: pointer;
}

.dumi-btn-default {
  background: #fff;
  color: #333;
  border: 1px solid #d9d9d9;

  &:hover {
    color: #ff7d4c;
    border-color: #ff7d4c;
  }
}

.dumi-btn-primary {
  color: #fff;
  background: #ff7d4c;
  border: 1px solid #ff7d4c;
}

组件源代码添加好后,需要在src/index.ts中引入后暴露一下:

export { default as Button } from './Button';

在这里引入并暴露出去以后,就可以在项目中通过import { Button } from 'demo-ui';来引入了。

3.3 添加demo示例

每一个组件我们可以加一个demo示例,方便使用者能更方便的使用。

在Button目录下新建一个demo文件夹,内建一个基础演示base.tsx文件:

mkdir src/Button/demo && touch src/Button/demo/base.tsx

然后添加组件的演示代码:

// src/Button/demo/base.tsx

import React from 'react';
import { Button } from 'demo-ui';

export default () => {

  return (
    <>
      <Button type="default">默认按钮</Button> &nbsp;
      <Button type="primary">主要按钮</Button>
    </>
  );
}

3.4 添加组件文档

再在该文件同目录新建一个index.md文件作为文档说明,这也是生成静态文档站点所需要的。

touch src/Button/index.md

添加文档内容,具体内容描述可以看官网MakeDown配置项,这里只在注释里面讲一下用到的配置。

---
category: Components
title: Button 按钮 # 组件的标题,会在菜单侧边栏展示
toc: content # 在页面右侧展示锚点链接
group: # 分组
  title: 基础组件 # 所在分组的名称
  order: 1 # 分组排序,值越小越靠前
---

# Button 按钮

## 介绍

基础的按钮组件 Button。

## 示例 

<!-- 可以通过code加载示例代码,dumi会帮我们做解析 -->
<code src="./demo/base.tsx">基础用法</code>

## APi

<!-- 会生成api表格 -->
| 属性 | 类型                   | 默认值   | 必填 | 说明 |
| ---- | ---------------------- | -------- | ---- | ---- |
| type | 'primary' | 'default' | 'default |  false  | 按钮类型 |

全部配置好后,需要重启一下dumi2项目,重启后就可以在浏览器看到效果了。

image.png

四. 高阶组件开发

有时候除了从0封装基础组件之外,还会基于antd等组件库进行二次开发,方式和开发基础组件是一样的,只是要在打包package包时注意css的引入。

4.1 添加组件代码

先安装antd库,安装到开发依赖,后面会添加到peerDependencies依赖中。

npm i antd -D

先做一个基础的例子,二次封装一下antd的按钮组件, 让它是primary风格的组件。

在src下新建一个PrimaryButton文件夹,内建index.tsx

mkdir src/PrimaryButton && touch src/PrimaryButton/index.tsx

在index.tsx里面编写代码:

// src/PrimaryButton/index.tsx

import React, { memo } from "react";
import { Button, ButtonProps } from "antd";

type IPrimaryButtonProps = Omit<ButtonProps, 'type'>

const PrimaryButton: React.FC<IPrimaryButtonProps> = (props) => {

  const { children, ...rest } = props

  return (
    <Button {...rest} type='primary'>
      {children}
    </Button>
  );
};

export default memo(PrimaryButton);

组件源代码添加好后,需要在src/index.ts中引入后暴露一下:

// src/index.ts
export { default as PrimaryButton } from './PrimaryButton';

在这里引入并暴露出去以后,dumi会帮我们放在包名称demo-ui里面,就可以在项目中通过

import { PrimaryButton } from 'demo-ui';来引入了。

4.2 添加demo示例

在PrimaryButton目录下新建一个demo文件夹,内建一个基础演示base.tsx文件。

mkdir src/PrimaryButton/demo && touch src/PrimaryButton/demo/base.tsx

添加组件示例代码:

// src/Button/demo/base.tsx

import React from 'react';
import { PrimaryButton } from 'demo-ui';

export default () => {

  return (
    <PrimaryButton>默认按钮</PrimaryButton>
  );
}

4.3 添加组件文档

再在该文件同目录新建一个index.md文件作为文档说明,这也是生成静态文档站点所需要的。

touch src/PrimaryButton/index.md

添加文档内容,具体内容描述可以看官网MakeDown配置项,这里只在注释里面讲一下用到的配置。

---
category: Components
title: PrimaryButton # 组件的标题,会在菜单侧边栏展示
toc: content # 在页面右侧展示锚点链接
group: # 分组
  title: 二次封装组件 # 所在分组的名称
  order: 2 # 分组排序,值越小越靠前
---

# PrimaryButton 按钮

## 介绍

基础的按钮组件 PrimaryButton。

## 示例 

<!-- 可以通过code加载示例代码,dumi会帮我们做解析 -->
<code src="./demo/base.tsx">基础用法</code>

## APi

<!-- 会生成api表格 -->
| 属性 | 类型                   | 默认值   | 必填 | 说明 |
| ---- | ---------------------- | -------- | ---- | ---- |
| size | 'small' | 'midlle' | 'large |  false  | 按钮大小 |

然后重启一下dumi2项目,可以看到页面上已经有最新添加的组件了。

image.png

五. 开发工具函数

dum2除了开发组件库之外,也能开发函数库,开发函数库要比组件库简单很多,而且不限制前端框架,vue,react里面都能使用。

5.1 添加工具函数代码

写一个时间格式化的工具函数,在src下新建一个formatTime文件夹,新增index.ts。

mkdir src/formatTime && touch src/formatTime/index.ts

在index.ts里面编写代码:

// src/formatTime/index.ts

/**
  格式化时间戳
  @param timestamp 时间戳,单位为毫秒
  @param format 时间格式,如YYYY-MM-DD hh:mm:ss
  @returns 返回格式化后的时间字符串
*/
function formatTime(timestamp: number, format='YYYY-MM-DD hh:mm:ss'): string {
  const date = new Date(timestamp);
  const year = date.getFullYear();
  const month = ('0' + (date.getMonth() + 1)).slice(-2);
  const day = ('0' + date.getDate()).slice(-2);
  const hours = ('0' + date.getHours()).slice(-2);
  const minutes = ('0' + date.getMinutes()).slice(-2);
  const seconds = ('0' + date.getSeconds()).slice(-2);
  const map: { [key: string]: string } = {
    YYYY: String(year),
    MM: month,
    DD: day,
    hh: hours,
    mm: minutes,
    ss: seconds,
  };
  return format.replace(/YYYY|MM|DD|hh|mm|ss/g, (matched) => map[matched]);
}

export default formatTime;

组件源代码添加好后,需要在src/index.ts中引入后暴露一下:

// src/index.ts
export { default as formatTime } from './formatTime';

在这里引入并暴露出去以后,dumi会帮我们放在包名称demo-ui里面,就可以在项目中通过

import { formatTime } from 'demo-ui';来引入了。

5.2 添加demo示例

在formatTime目录下新建一个demo文件夹,内建一个基础演示base.tsx文件:

import React, { useEffect, useState } from 'react';
import { formatTime } from 'demo-ui';

const App: React.FC = () => {
  const [currentDate, setCurrentDate] = useState(formatTime(Date.now(), 'YYYY年MM月DD日 hh:mm:ss'));
  const [siteDate, setSiteDate] = useState<string>();

  useEffect(() => {
    // 指定时间戳时间
    const timestamp=1673850986000 //2023-01-16 14:36:26
    const siteStr: string = formatTime(timestamp);
    setSiteDate(siteStr);
  }, []);

  useEffect(() => {
    // 每秒更新一次时间
    const timer = setInterval(() => {
      const date = Date.now();
      const dateStr = formatTime(date, 'YYYY年MM月DD日 hh:mm:ss');
      setCurrentDate(dateStr);
    }, 1000);
    return () => {
      clearInterval(timer);
    }
  }, []);

  const inputRef = React.createRef<HTMLInputElement>();
  const onFormatData = () => {
    const value = inputRef.current?.value;
    if (value) {
      const dateStr = formatTime(Number(value), 'YYYY年MM月DD日 hh:mm:ss');
      setSiteDate(dateStr);
    }
  };

  return (
    <>
      当前时间:{currentDate}
      <hr />
      指定时间转换:
      <input type="number" ref={inputRef} defaultValue={1673850986000} />
      &nbsp;<button type='button' onClick={onFormatData}>转换</button>&nbsp;
      {siteDate}
    </>
  );
};

export default App;

5.3 添加工具文档

在formatTime目录下新建一个index.md文件:

---
category: Components
title: 时间格式化 # 组件的标题,会在菜单侧边栏展示
toc: content # 在页面右侧展示锚点链接
group: # 分组
  title: 工具函数 # 所在分组的名称
  order: 3 # 分组排序,值越小越靠前
---

### formatTime

将时间戳格式化成指定的日期时间格式。

#### 示例

<!-- 可以通过code加载示例代码,dumi会帮我们做解析 -->

<code src="./demo/base.tsx">基础用法</code>

### 参数

| 参数名    | 类型   | 是否必填 | 默认值                  | 说明                                                       |
| --------- | ------ | -------- | ----------------------- | ---------------------------------------------------------- |
| timestamp | number | 是       | -                       | 要格式化的时间戳,单位为毫秒                               |
| format    | string | 否       | `'YYYY-MM-DD hh:mm:ss'` | 要格式化成的日期时间格式,默认为 `'YYYY-MM-DD hh:mm:ss'`。 |

#### 返回值

类型:string
格式化后的日期时间字符串。

然后重启一下dumi项目,可以看到页面上已经有最新添加的formatTime函数了。

image.png

六、打包部署

在组件或者工具函数开发完成后,就需要进行部署操作了,部署分为两部分部署:

一是打包文档静态站点文档,让用户可以通过域名访问到文档站点,方便其使用。

二是打包组件库源码,部署到npm仓库上面,让其他人可以通过npm安装使用。

6.1 打包静态站点

打包静态站点dumi2在创建项目时已经配置好了命令,只需要在控制台执行

npm run docs:build

打包完成后会在项目中生成docs-dist文件夹,该文件夹就是部署静态文档站点的静态资源。

在本地可以借助serve起一个服务托管静态进行测试一下,全局安装serve:

npm i serve -g

安装完成后在项目根目录执行命令托管文档静态站点:

serve -s docs-dist

打开提示的服务访问链接http://localhost:3000/,就可以在浏览器访问到打包后的静态站点,实际情况下需要部署到服务器上面,可以借助nginx来部署。

6.2 优化静态站点打包

在静态站点默认配置下,会把每一个组件或者函数单独打包一份静态文件在components文件夹下,在上图我们也可以看到,但实际上一般是不需要再单独生成一份的,可以修改打包配置,取消打包单个静态资源。

修改.dumirc.ts文件

import { defineConfig } from 'dumi';

export default defineConfig({
  // ...
  // 取消打包静态单个组件库和函数工具
  exportStatic: false
});

6.3 打包npm源码包

静态站点打包好后,就需要打包组件和函数库最终的npm包了,dumi2也在创建项目时就提供了npm包打包的命令,直接执行:

npm run build

打包完成后会在项目中生成dist文件夹,该dist文件夹就是最终要发布到npm仓库上的源码。

发布的时候除了dist文件之外,还需要在package.json里面做配置:

把antd添加到,表示使用该组件库,必须要先安装antd对应版本。

"peerDependencies": {
  "react": ">=16.9.0",
  "react-dom": ">=16.9.0",
  "antd": ">=5.4.2"
},

package.json的name字段对应npm包的版本号,第一次发可以0.0.1,后面再发就需要修改版本号。

其他的字段files和module,types在项目初始化的时候dumi就帮我们设置好了。

然后去npm官网注册账号,在命令行通过npm login进行登录,最后回到项目目录,打开命令行,输入

npm publish

就可以发布到npm仓库了。

6.4 优化npm源码打包

在上面打包源码包图中,我们可以看到在函数源码下面依然有demo文件夹,但实际使用过程中是不会用到的,可以通过配置在打包npm源码包的时候把demo文件夹过滤掉。

因为打包npm源码是用father来打包的,所以我们要修改.fatherrc.ts配置文件:

import { defineConfig } from 'father';

export default defineConfig({
  esm: {
    // ...
    ignores: [
      'src/**/demo/**', // 避免打包demo文件到npm包里面
    ],
  },
  // ...
});

再一次打包就会发现demo文件夹不会出现在最终的npm包dist文件夹里面了。

6.5 解决antd打包npm后没有样式

上面虽然基于antd封装好了按钮,在文档预览没有问题,但是在把npm包引入使用的时候会出现样式丢失的问题,有三个常见的解决方案。

  1. 直接全局引入antd的样式,但这样虽然可以解决问题,但是会引入很多不必要的css资源,增加项目体积,所以不推荐。
  2. 在二次封装的组件内手段引入对应antd组件的样式,这样虽然可以解决样式丢失问题,并且支持按需引入,但需要手动加比较麻烦。
  3. 可以在npm包打包配置.fatherrc.ts添加antd按需引入css的配置,安装按需引入依赖:
npm i babel-plugin-import -D

然后在.fatherrc.ts添加extraBabelPlugins配置

import { defineConfig } from 'father'

export default defineConfig({
  // ...
  // 打包的时候自动引入antd的样式链接
  extraBabelPlugins: [
    [
      'babel-plugin-import',
      {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
      },
    ],
  ],
})

添加配置后打包npm包就会添加上antd的样式了。

6.6 解决组件不能按需引入

在发布到npm上面后本地安装使用时发现组件库没有tree-shaking效果。

npm i demo-ui -S

import { PrimaryButton } from 'demo-ui'

问题原因是由样式less文件引起的,构建工具认为样式有副作用,所以没有进行tree-shaking操作,解决方案只需要在dmi2组件代码的package.json里面加上sideEffet,告诉构建工具这个npm包没有副作用可以进行tree-shaking。

修改组件库的package.json,添加:

{
  // ...
  "slideEffects": false,
}

修改后使用组件就会按需加载,有tree-shking效果了。