一、技术选型dumi
直接引用官网的介绍
dumi,中文发音嘟米 ,是一款为组件开发场景而生的静态站点框架,与 father 一起为开发者提供一站式的组件开发体验,father 负责组件源码构建,而 dumi 负责组件开发及组件文档生成。
简单来说dumi 是一个开发组件库和函数库的框架,可以方便的开发组件和示例文档,最终把示例文档 打包为静态站点部署,并且把组件源码打包为最终发布的npm包,我们可以专注于组件开发,节省了大量配置的时间。
什么时候会用到?
搭建自己的函数库和组件库通常是为了提高代码的复用性和开发效率,特别适合在以下情况下使用:
- 多个项目共用的函数或组件。如果你有多个项目需要使用同样的函数或组件,可以将其封装成库来提高复用性和开发效率。这样做可以避免重复编写代码,同时也方便后续维护和更新。
- 团队协作开发。如果你在一个团队中进行协作开发,搭建自己的函数库和组件库可以方便不同成员之间的代码共享和协作。这样做可以避免不同成员之间重复编写代码,提高开发效率和代码质量。
- 提高代码可维护性。搭建自己的函数库和组件库可以方便后续的代码维护和更新。这样做可以避免代码中存在大量的冗余代码和重复代码,提高代码的可读性和可维护性。
二、安装dumi初始化ui-components子工程
2.1 环境准备
node 版本为 14+ 即可
node -v
v16.20.2
2.2 创建项目
①创建目录
bash
mkdir ui-components&& cd ui-components
②通过官方工具创建项目,选择你需要的模板
lua
npx create-dumi
dumi2目前模版有三种
- Static Site # 用于构建网站
- React Library # 用于构建组件库,有组件例子
- Theme Package # 主题包开发脚手架,用于开发主题包
在进行模版类型的时候我们选择React Library模版,可以开发组件和生成静态站点文档,其他的是选择pnpm管理工具,包名称,描述和邮箱,按自己情况来输入就好了,这里包名称输入的是@yyfe/boc-ui-components。
等待安装完依赖后启动项目:
sql
npm start
2.3 目录结构
python
.
├── docs # 组件库文档目录
│ ├── index.md # 组件库文档首页
│ ├── guide # 组件库其他文档路由表(示意)
│ │ ├── index.md
│ │ └── help.md
├── src # 组件库源码目录
│ ├── Button # 单个组件
│ │ ├── index.tsx # 组件源码
│ │ ├── index.less # 组件样式
│ │ └── index.md # 组件文档
│ └── index.ts # 组件库入口文件
├── .dumirc.ts # dumi文档的配置文件
└── .fatherrc.ts # 组件库打包npm包的配置文件
2.4 完善工作
①名称样式处理
arduino
import { defineConfig } from 'dumi';
export default defineConfig({
// ...
styles: [
`.dumi-default-header-left {
width: 400px !important;
}`,
],
});
②Foo修改为组件库选项 && git link url 修改
php
import { defineConfig } from 'dumi';
export default defineConfig({
// ...
themeConfig: {
name: 'dumi2-demo',
nav: [
{ title: '介绍', link: '/guide' },
{ title: '组件', link: '/components/Foo' }, // components会默认自动对应到src文件夹
],
socialLinks: {
github:
'https://dev.sankuai.com/code/repo-detail/health/medicine_brand_operation_center_monorepo/file/list?path=ui-components&branch=refs%2Fheads%2Ffeature%2Fyyfe-boc-ui-components%2Fdingxiaoxue02',
},
},
// ...
});
三、开发基础组件
3.1 添加组件源代码
①新增简单的Button组件
css
mkdir src/Button && touch src/Button/index.tsx
②在文件中新增一个简单的Button组件代码:
typescript
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);
③添加less样式和变量
scss
touch src/variables.less
@dumi-primary: #ffdf24; // 主题颜色
在Button组件库下面新建styles文件夹,里面新建index.less文件
less
mkdir src/Button/styles && touch src/Button/styles/index.less
@import '../../variables.less';
.dumi-btn {
font-size: 14px;
height: 32px;
padding: 4px 15px;
border-radius: 6px;
transition: all 0.3s;
cursor: pointer;
}
.dumi-btn-default {
background: #fff;
color: #333;
border: 1px solid #d9d9d9;
&:hover {
color: @dumi-primary;
border-color: @dumi-primary;
}
}
.dumi-btn-primary {
color: #fff;
background: @dumi-primary;
border: 1px solid @dumi-primary;
}
④组件源代码添加好后,需要在src/index.ts中引入后暴露一下:
javascript
export { default as Button } from './Button';
在这里引入并暴露出去以后,就可以在项目中通过import { Button } from '@yyfe/boc-ui-components';来引入了。
3.2 添加demo示例
每一个组件加一个demo示例,方便使用者能更方便的使用。Demo 配置
在Button目录下新建一个demo文件夹,内建一个基础演示base.tsx文件:
bash
mkdir src/Button/demo && touch src/Button/demo/base.tsx
然后添加组件的演示代码:
javascript
import React from 'react';
import { Button } from '@yyfe/boc-ui-components';
export default () => {
return (
<>
<Button type="default">默认按钮</Button>
<Button type="primary">主要按钮</Button>
</>
);
}
3.3 添加组件文档
再在该文件同目录新建一个index.md文件作为文档说明,这也是生成静态文档站点所需要的。Markdown 配置
yaml
touch src/Button/index.md
---
category: Components
title: Button 按钮 # 组件的标题,会在菜单侧边栏展示
toc: content # 在页面右侧展示锚点链接
group: # 分组
title: 基础组件 # 所在分组的名称
order: 1 # 分组排序,值越小越靠前
---
<!--Markdown 配置: https://d.umijs.org/config/markdown -->
# Button 按钮
## 介绍
基础的按钮组件 Button。
## 示例
<!-- 可以通过code加载示例代码,dumi会帮我们做解析 -->
<code src="./demo/base.tsx">基础用法</code>
## APi
<!-- 会生成api表格 -->
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| ---- | ---------------------- | -------- | ---- | ---- |
| type | 'primary' | 'default' | 'default | false | 按钮类型 |
3.4 添加单元测试
添加好基础组件后,需要给组件添加测试代码,来保障组件的健壮性。
①测试框架采用react最常用的jest工具,再配合react配套的单元测试库,安装依赖:
dart
pnpm i jest @testing-library/react @types/jest ts-jest jest-environment-jsdom jest-less-loader typescript -D
- @testing-library/react :
- 这是 React 应用的测试库,提供了一组工具来测试 React 组件的渲染和交互。
- @types/jest :
- 为 Jest 提供 TypeScript 类型支持,使 TypeScript 能够理解 Jest 的 API,从而提供更好的自动补全和类型检查。
- ts-jest :
- Jest 的一个预处理器,用于处理 TypeScript 文件,并将其转换为 JavaScript 以便进行测试。
- jest-environment-jsdom :
- 提供了一个 JSDOM 环境,让你能够在测试中模拟浏览器环境,适用于前端应用程序。
- jest-less-loader :
- 这是一个用于支持 LESS 文件的 Jest 加载器,允许你在测试中直接加载 LESS 样式。
- typescript :
- TypeScript 编程语言的核心包,提供了类型系统和编译器。
②在项目根目录新建jest的配置文件jest.config.js
arduino
touch jest.config.js
module.exports = {
preset: 'ts-jest', // 使用ts-jest预设,支持用ts写单元测试
testEnvironment: 'jsdom', // 设置测试环境为jsdom环境
roots: ['./src'], // 查找src目录中的文件
collectCoverage: true, // 统计覆盖率
coverageDirectory: 'coverage', // 覆盖率结果输出的文件夹
transform: {
'\.(less|css)$': 'jest-less-loader', // 支持less
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
// 单元覆盖率统计的文件
collectCoverageFrom: ['src/**/*.tsx', 'src/**/*.ts', '!src/index.ts', '!src/**/demo/*'],
};
③编写单元测试
在Button目录下新建一个__tests__文件夹放置单元测试代码,在里面新建index.test.tsx
bash
mkdir src/Button/__tests__ && touch src/Button/__tests__/index.test.tsx
在index.test.tsx文件中编写单元测试代码
ini
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Button from '..';
describe('Button组件', () => {
it('能够正确渲染按钮文字', () => {
const buttonText = '按钮文字';
const { getByRole } = render(<Button>{buttonText}</Button>);
const buttonElement = getByRole('button');
expect(buttonElement.innerHTML).toBe(buttonText);
});
it('能够正确渲染默认样式的按钮', () => {
const { getByRole } = render(<Button>默认按钮</Button>);
const buttonElement = getByRole('button');
expect(buttonElement.classList.contains('dumi-btn')).toBe(true);
});
it('能够正确渲染主要样式的按钮', () => {
const { getByRole } = render(<Button type="primary">主要按钮</Button>);
const buttonElement = getByRole('button');
expect(buttonElement.classList.contains('dumi-btn-primary')).toBe(true);
});
it('能够触发点击事件', () => {
const handleClick = jest.fn();
const { getByRole } = render(<Button type="primary" onClick={handleClick}>点击按钮</Button>);
const buttonElement = getByRole('button');
fireEvent.click(buttonElement);
// 断言函数被调用了一次。
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
单元测试代码分别测试了按钮基础渲染,渲染默认样式,渲染主要样式以及点击测试功能。
保存后在命令行执行npx jest执行一下单元测试:
可以看到执行了Button组件的单元测试,并且最后生成了测试报告,标注了测试了哪些文件,每个文件的覆盖率,测试情况。
并且会在项目根目录下生成测试报告的静态站点coverage文件夹,在浏览器打开coverage/lcov-report/index.html文件,即可看到测试报告,通过对每个文件的分析,可以看到有哪些单元测试没覆盖到的地方,可以帮助我们更好的写单元测试。
四、基于roo套装做二次开发
可直接复制已有组件,进行参考
五、 jest单元测试优化
5.1 全量单元测试
json
"scripts": {
"test:all": "jest --coverage --bail"
}
jest的命令参数:
- --bail: 遇到测试用例失败时立即停止测试,不再执行剩余的测试用例,可以节省时间。
- --coverage:生成单元测试覆盖率报告。
- --findRelatedTests: 可以指定要执行的单元测试文件。
再次执行npm run test:all就可以进行单元测试了。
5.2 按需单元测试
六、打包部署
本地研发时候的流程如下:
6.1 打包静态站点
打包静态站点dumi2在创建项目时已经配置好了命令,只需要在控制台执行
pnpm un docs:build
6.2 优化静态站点打包
在静态站点默认配置下,会把每一个组件或者函数单独打包一份静态文件在components文件夹下,在上图我们也可以看到,但实际上一般是不需要再单独生成一份的,可以修改打包配置,取消打包单个静态资源。
修改.dumirc.ts文件
arduino
import { defineConfig } from 'dumi';
export default defineConfig({
// ...
// 取消打包静态单个组件库和函数工具
exportStatic: false
});
6.3 打包npm源码包
arduino
pnpm run build
布的时候除了dist文件之外,还需要在package.json里面做配置:
把antd添加到,表示使用该组件库,必须要先安装roo对应版本。
perl
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"@roo/roo": ">=1.17.9"
},
6.4 优化npm源码打包
在上面打包源码包图中,我们可以看到在函数源码下面依然有demo文件夹,但实际使用过程中是不会用到的,可以通过配置在打包npm源码包的时候把demo文件夹过滤掉。
因为打包npm源码是用father来打包的,所以我们要修改.fatherrc.ts配置文件:
arduino
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/**/demo/**', // 避免打包demo文件到npm包里面
],
},
});
6.5 解决roo打包npm后没有样式
javascript
pnpm i babel-plugin-import -D
import { defineConfig } from 'father'
export default defineConfig({
// 打包的时候自动引入roo的样式链接
// roo 的组件不统一,有的组件没有样式文件,会报错。已经在最外层统一引入了roo的全局样式,并设置了 boc-ui 统一前缀。
extraBabelPlugins: [
[
'babel-plugin-import',
{
libraryName: '@roo/roo',
libraryDirectory: './',
styleLibraryDirectory: '',
style: true,
},
],
],
})
6.6 解决组件不能按需引入
告诉构建工具这个npm包没有副作用可以进行tree-shaking
json
{
// ...
"slideEffects": false,
}
6.7 打包优化:关于peerDependencies、devDependencies、dependencies 和 externals、extraExternals
1.依赖类型的区别
- dependencies :
- 这些是生产环境所需的依赖,会被打包到最终的输出文件中。正常情况下,添加新的 dependencies 应该会增加打包后的文件大小。
- devDependencies :
- 这些是开发环境所需的依赖,不会被打包到最终的输出中。它们主要用于开发和测试。
- peerDependencies :
- 这些是对某些依赖的声明,表明该包需要某个特定版本的依赖,但不会直接安装这些依赖。它通常用于库的开发,以确保用户在使用该库时有正确版本的依赖。
2. externals 和 extraExternals
- externals :
- 通过配置 externals,可以指定某些依赖不被打包,而是期望在运行时由外部提供。这有助于减小打包文件的大小,尤其是像 React、Lodash 等大型库。
- extraExternals :
- 这是 father 提供的额外配置项,允许你在打包过程中处理特定的外部依赖。
6.8 发布流水线
可根据自己公司发布平台进行配置