从零开始搭建 React 组件库

3,135 阅读5分钟

引言

为了将之前业务开发的组件进行统一维护以及便于后续在其他项目复用,以此为目的而搭建组件库。由于之前开发的项目是基于 React 实现,经过调研,决定选用较为普遍使用的 Dumi 作为组件库文档工具,Father 作为组件库打包工具。

项目搭建

mkdir component-lib-demo cd component-lib-demo npx @umijs/create-dumi-lib --site —— 初始化一个站点模式的组件库开发脚手架

初始目录如下,其中,.fatherrc.ts 为打包配置文件,.umirc.ts 为组件库文档配置文件。 image.png

组件编写

普通组件

以开发一个排行榜 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 排行榜

排行榜组件用于简易排行榜业务场景。

## 基础使用

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

效果

192.168.10.6_8001_components_rank-list.png

基于 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: /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,
    },
  },
  // ...
});

效果

192.168.10.6_8001_components_countdown-button.png

组件打包与发布

打包

打包配置 /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 image.png

发布

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 发布操作。

image.png image.png

组件文档部署

.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 image.png

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

#/docs-dist
/lib
/es

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

配置 Github Pages image.png

访问 hwjfqr.github.io/component-l… ,当页面如下显示时,表示大功告成!image.png

更多

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

本文所演示项目的源码地址为 github.com/hwjfqr/comp… ,有任何疑问欢迎评论或提 issue,如果觉得本文对自己有所帮助,不妨点个赞再走,谢谢! ​

同时最后安利一波本人基于 Ant Design 封装的业务组件库:github.com/hwjfqr/ant-…,欢迎使用。