一、概述
基于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>
<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项目,重启后就可以在浏览器看到效果了。
四. 高阶组件开发
有时候除了从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项目,可以看到页面上已经有最新添加的组件了。
五. 开发工具函数
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} />
<button type='button' onClick={onFormatData}>转换</button>
{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函数了。
六、打包部署
在组件或者工具函数开发完成后,就需要进行部署操作了,部署分为两部分部署:
一是打包文档静态站点文档,让用户可以通过域名访问到文档站点,方便其使用。
二是打包组件库源码,部署到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包引入使用的时候会出现样式丢失的问题,有三个常见的解决方案。
- 直接全局引入antd的样式,但这样虽然可以解决问题,但是会引入很多不必要的css资源,增加项目体积,所以不推荐。
- 在二次封装的组件内手段引入对应antd组件的样式,这样虽然可以解决样式丢失问题,并且支持按需引入,但需要手动加比较麻烦。
- 可以在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效果了。