为了将之前业务开发的组件进行统一维护以及便于后续在其他项目复用,以此为目的而搭建组件库。由于之前开发的项目是基于 React 实现,经过调研,决定选用较为普遍使用的 Dumi 作为组件库文档工具,Father 作为组件库打包工具。
mkdir component-lib-demo
cd component-lib-demo
npx @umijs/create-dumi-lib --site —— 初始化一个站点模式的组件库开发脚手架
初始目录如下,其中,.fatherrc.ts 为打包配置文件,.umirc.ts 为组件库文档配置文件。
以开发一个排行榜 RankList 组件为例。
/src/RankList/index.tsx
import React from 'react';
import './index.less';
interface RankListProps {
data: { label: string; value: string | number }[];
}
function RankList({ data }: RankListProps) {
return (
<div className="rank-list">
{data.length ? (
<ul>
{data
.filter((_, index) => index < 10)
.map(({ label, value }, index) => (
<li key={label}>
<div
className="rank"
style={{
backgroundColor: index + 1 < 4 ? '#27478d' : '#fafafa',
color: index + 1 < 4 ? '#fff' : 'rgba(0, 0, 0, 0.65)',
}}
>
{index + 1}
</div>
<div className="name">
<span title={label || '--'}>{label || '--'}</span>
</div>
<div className="num">{value}</div>
</li>
))}
</ul>
) : (
<div className="empty">暂无数据</div>
)}
</div>
);
}
export default RankList;
/src/RankList/index.less
.rank-list {
position: relative;
height: 100%;
ul {
margin: 0px;
padding: 0px;
li {
height: 24px;
margin: 16px 0px;
display: flex;
align-items: center;
.rank {
flex-shrink: 0;
display: flex;
justify-content: center;
width: 20px;
height: 20px;
line-height: 20px;
margin-right: 6px;
border-radius: 50%;
}
.name {
flex: 1;
}
}
}
.empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
/src/index.ts
export { default as RankList } from './RankList';
/src/RankList/index.md
---
title: RankList 排行榜
nav:
title: 组件
path: /components
group:
path: /components
---
# RankList 排行榜
排行榜组件用于简易排行榜业务场景。
## 基础使用
/src/RankList/demos/index.tsx
import React from 'react';
import { RankList } from 'component-lib-demo';
function RankListDemo() {
const data = Array.from(new Array(10)).map((_, idx) => ({
label: `选项${idx + 1}`,
value: 10 - idx,
}));
return (
<div>
<RankList data={data}></RankList>
</div>
);
}
export default RankListDemo;
配置别名,防止在 demo 中,通过项目名引入组件时显示编译错误。
/tsconfig.json
{
"compilerOptions": {
// ...
"paths": {
"@/*": ["src/*"],
"@@/*": ["src/.umi/*"],
"component-lib-demo": ["src/index.ts"]
},
},
// ...
}
以开发一个倒计时按钮 CountdownButton 组件为例。
安装 antd 相关依赖
npm i -D antd babel-plugin-import
配置按需加载
/.umirc.ts
export default defineConfig({
// ...
extraBabelPlugins: [
[
'babel-plugin-import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
],
],
// ...
});
/src/CountdownButton/index.tsx
import React, { useState, useEffect } from 'react';
import { Button } from 'antd';
import { ButtonProps } from 'antd/es/button';
const MAX_SECOND_NUM = 60;
interface CountdownButtonType
extends Omit<ButtonProps, 'disabled' | 'onClick'> {
/**
* 最大秒数
*/
maxSecondNum?: number;
/**
* 按钮默认文本
*/
txt?: string;
/**
* 加载时按钮文本
*/
loadingTxt?: string;
/**
* 禁用时按钮文本
*/
disabledTxt?: (s: number) => string;
/**
* 点击按钮时触发的函数,其参数 completeCallback 需要在接口请求完毕后调用,用于告知组件接口请求已完成。
*/
onClick: (completeCallback: () => void) => void;
}
function CountdownButton({
maxSecondNum = MAX_SECOND_NUM,
txt = '获取验证码',
loadingTxt = '发送中',
disabledTxt = (s) => `${s} 秒后重试`,
onClick = (completeCallback) => {
completeCallback();
},
...rest
}: CountdownButtonType) {
const [authCodeArgs, setAuthCodeArgs] = useState({
timing: false,
count: maxSecondNum,
});
useEffect(() => {
let timer: number | undefined = undefined;
if (authCodeArgs.timing) {
timer = window.setInterval(() => {
setAuthCodeArgs((pre) => {
const { count, timing } = pre;
if (count === 1) {
window.clearInterval(timer);
return { timing: false, count: maxSecondNum };
}
return { timing, count: count - 1 };
});
}, 1000);
}
return () => window.clearInterval(timer);
}, [authCodeArgs.timing]);
const completeCallback = () => {
setAuthCodeArgs({
...authCodeArgs,
timing: true,
});
};
let buttonText;
if (rest.loading) {
buttonText = loadingTxt;
} else if (authCodeArgs.timing) {
buttonText = disabledTxt(authCodeArgs.count);
} else {
buttonText = txt;
}
return (
<Button
disabled={authCodeArgs.timing}
style={{ minWidth: 100, ...(rest.style || {}) }}
onClick={() => {
onClick && onClick(completeCallback);
}}
{...rest}
>
{buttonText}
</Button>
);
}
export default CountdownButton;
/src/index.ts
export { default as RankList } from './RankList';
export { default as CountdownButton } from './CountdownButton';
/src/CountdownButton/index.md
---
title: CountdownButton 倒计时按钮
nav:
title: 组件
path: /components
group:
path: /components
---
# CountdownButton 倒计时按钮
倒计时按钮常应用于获取手机、邮箱验证码等业务场景。
## 基础使用
<code src="./demos/index.tsx" />
<API></API>
除以上 API 外,倒计时按钮还支持 Button 组件(Ant Design)的所有 API 。
/src/CountdownButton/demos/index.tsx
import React, { useState } from 'react';
import { CountdownButton } from 'component-lib-demo';
function CountdownButtonDemo() {
const [loading, setLoading] = useState<boolean>(false);
const getCode = async () => {
setLoading(true);
try {
return await new Promise((resolve) =>
setTimeout(() => {
resolve(123);
}, 1000),
);
} catch (err) {
throw new Error('failed');
} finally {
setLoading(false);
}
};
return (
<CountdownButton
loading={loading}
onClick={async (completeCallback) => {
const code = await getCode();
console.log(`验证码:${code}`);
completeCallback();
}}
>
获取验证码
</CountdownButton>
);
}
export default CountdownButtonDemo;
修改 .umirc.ts 配置,过滤掉 antd 组件自带的接口属性,防止最终生成的 API 文档包含 antd 组件的自带属性。
import { defineConfig } from 'dumi';
export default defineConfig({
// ...
extraBabelPlugins: [
[
'babel-plugin-import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
],
],
apiParser: {
// 自定义属性过滤配置,也可以是一个函数,用法参考:https://github.com/styleguidist/react-docgen-typescript/#propfilter
propFilter: {
// 是否忽略从 node_modules 继承的属性,默认值为 false
skipNodeModules: true,
},
},
// ...
});
打包配置
/src/.fatherrc.ts
export default {
esm: 'babel', // 通过 babel 编译相关组件即可,而无需打包在一个文件中,实现在使用时可按需加载。
cjs: 'babel',
lessInBabelMode: true, // less 转 css
// 打包的产物若需引入 antd ,则通过按需加载形式引入。
extraBabelPlugins: [
[
'babel-plugin-import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
],
],
};
为防止打包时可能出现的类型报错,建议安装 @types/react、@types/react-dom 等相关依赖:
npm i -D @types/react @types/react-dom
配置 tsconfig.json 的 compilerOptions 字段的 declaration 选项为 true ,使打包时生成对应的类型声明文件。
/tsconfig.json
{
"compilerOptions": {
// ...
"declaration": true,
// ...
},
//...
}
package.json 配置
{
"name": "component-lib-demo",
"version": "1.0.0",
"scripts": {
"start": "dumi dev",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"build": "father-build",
"deploy": "npm run docs:build && npm run docs:deploy",
"release": "npm run build && npm publish",
"prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
"main": "lib/index.js",
"module": "es/index.esm.js",
"typings": "lib/index.d.ts",
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
},
"files":["es","lib"],
"peerDependencies": {
"antd": ">=4.0.0",
"react":">=16.9.0",
"react-dom":">=16.9.0"
},
"dependencies": {
"react": "^16.12.0 || ^17.0.0"
},
"devDependencies": {
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"@umijs/test": "^3.0.5",
"antd": "^4.17.2",
"babel-plugin-import": "^1.13.3",
"dumi": "^1.0.17",
"father-build": "^1.17.2",
"gh-pages": "^3.0.0",
"lint-staged": "^10.0.7",
"prettier": "^2.2.1",
"yorkie": "^2.0.0"
}
}
重点关注以下字段:
发布包
npm publish
.umirc.ts
import { defineConfig } from 'dumi';
export default defineConfig({
// ...
base: '/component-lib-demo/docs-dist/',
publicPath: '/component-lib-demo/docs-dist/',
history: {
type: 'hash', // 设置路由模式为 hash 模式,防止部署至 GitHub Pages 后刷新网页后出现 404 的情况发生.
},
// ...
});
取消忽略 /docs-dist 目录,后续 Github Pages 需要使用到此目录。同时将打包的产物(lib、es)添加到忽略文件中(这两个目录无需推送至 Git 仓库)。
.gitignore
#/docs-dist
/lib
/es
访问 https://hwjfqr.github.io/component-lib-demo/docs-dist/#/ ,当页面如下显示时,表示大功告成!
除以上基本使用之外, Dumi 与 Father 还有更多个性化的配置操作,具体可参考相关官方文档。
本文所演示项目的源码地址为 https://github.com/hwjfqr/component-lib-demo ,有任何疑问欢迎评论或提 issue,如果觉得本文对自己有所帮助,不妨点个赞再走,谢谢!
同时最后安利一波本人基于 Ant Design 封装的业务组件库:https://github.com/hwjfqr/ant-design-power,欢迎使用。