引言

为了将之前业务开发的组件进行对立保护以及便于后续在其余我的项目复用,以此为目标而搭建组件库。因为之前开发的我的项目是基于 React 实现,通过调研,决定选用较为广泛应用的 Dumi 作为组件库文档工具,Father 作为组件库打包工具。

  • Dumi:https://d.umijs.org/zh-CN
  • Father:https://github.com/umijs/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: /componentsgroup:  path: /components---# RankList 排行榜排行榜组件用于繁难排行榜业务场景。## 根底应用<code src="./demos/index.tsx" /><API></API>

/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"]    },  },  // ...}

成果


基于 Antd 封装的组件

以开发一个倒计时按钮 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: /componentsgroup:  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,    // ...  },  //...}

执行打包操作
npm run build

公布

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"  }}

重点关注以下字段:

  • main:指定包的入口文件,此处指定为 lib/index.js 。
  • module:指定包的基于 ESM 标准的入口文件,此处指定为 es/index.esm.js 。
  • typings:指定包的类型申明文件,此处指定为 lib/index.d.ts 。
  • files:指定须要推送至 npm 的文件,此处指定为 ["es","lib"] 。
  • peerDependencies:指定应用包的我的项目所须要依赖的模块,因为我的项目是 React 我的项目,并且存在依赖于 Ant-Design 组件库的组件,因而此处指定为 react、react-dom、antd。

公布包
npm publish

  • 如果是第一次公布包,则须要先通过 npm adduser 增加用户,如果未登录,则须要先应用 npm login 登录,之后即可执行 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 的状况产生.  },  // ...});
  • 设置 base、publicPath、history,便于后续部署至 Github Pages 。

生成组件文档
npm run docs:build

勾销疏忽 /docs-dist 目录,后续 Github Pages 须要应用到此目录。同时将打包的产物(lib、es)增加到疏忽文件中(这两个目录无需推送至 Git 仓库)。
.gitignore

#/docs-dist/lib/es

提交更改,推送至 GitHub 仓库。

配置 Github Pages

拜访 https://hwjfqr.github.io/component-lib-demo/docs-dist/#/ ,当页面如下显示时,示意功败垂成!

更多

除以上根本应用之外, Dumi 与 Father 还有更多个性化的配置操作,具体可参考相干官网文档。

  • Dumi:https://d.umijs.org/zh-CN
  • Father:https://github.com/umijs/father

本文所演示我的项目的源码地址为 https://github.com/hwjfqr/component-lib-demo ,有任何疑难欢送评论或提 issue,如果感觉本文对本人有所帮忙,无妨点个赞再走,谢谢!

同时最初安利一波自己基于 Ant Design 封装的业务组件库:https://github.com/hwjfqr/ant-design-power,欢送应用。