1
本文介绍一个基本的组件库需要满足哪些要求,重点介绍了基于umi father构建react组件库的关键点,并提供了示例工程。

WHAT

一个UI组件库的基本要求

支持多种格式

支持umd cjs esm ,当然就现在前端开发而言, umd 的支持与否并不是那么重要。

TypeScript

完整的类型定义,支持静态检查。都2020年了,不支持typescript是不是有点说不过去了。

支持全量引入

import { ComponentA } from 'package';
import 'package/dist/index.min.css;

支持按需引入

组件库能够默认支持基于 ESM 的 tree shaking,也能够通过babel-plugin-import实现按需加载。

基于 ESM 的 tree shaking 并不会实现 css 的按需加载,仍然需要手动引入css文件。当然,这里存在一些例外情况,比如,你采用 css in js 的方案。
// babel plugin config
{
    plugins: [
      ...otherPlugins,
      [import, {
        libraryName: 'package',
        style: true,
           libraryDirectory: 'lib',
      }, 'package'],
    ]
  }

支持主题定制

主题定制与组件库的css方案相关,一般来说,可以通过以下方案实现主题定制:

  • sass/less 变量 - antd采用的就是这种方式,但不建议采用这种方式,原因在于应用部署后,这种方式在线切换主题时需要在线解析sass/less,开销大,体验较大
  • 采用 css in js 的方案,主题定制即是天然而成的
  • css变量 - 如果不考虑IE的话,这是相当推荐的方案
document.documentElement.style.setProperty('--primary-color', '#0170fe');

单元测试

对于组件库而言,单元测试是保证质量的一个重要环节。

文档

一个清晰明了带示例的文档,对组件库而言是必备的。

css解决方案

详细待述

HOW

开发React组件库有很多方式,推荐使用 umi father, 集组件打包与文档于一体,对于开发组件库十分方便。然后,尽管umi father开发组件库十分便利,但是,要实现上述功能,仍然需要一些额外工作,本节的重点即是这些额外工作。

核心点

babel-plugin-import做了什么

import { PackageA } from 'package'; 
// 通过babel-plugin-import配置(style配置为true, libraryDirectory配置为lib)后等同于
import PackageA from 'package/lib/package-a'; 
import PackageA from 'package/lib/package-a/style'; 

基本原则

  • 每个组件不引入本组件的样式文件
  • 在全局样式文件内引入各个组件的样式文件
  • 使用babel来打包为cjs/esm格式,样式文件不处理,原样输出到目录,此时生成包也不会包含ts类型文件
  • 打包为umd格式时,配置两个入口文件(样式入口文件、组件入口文件),最终输出提取为两个文件(组件文件js和样式文件css)以及类型定义文件 - umd的生成文件除了可以以 script 的方式引入外,还有两个作用:1)提供类型定义文件 2)采用npm方式进行工程开发时,采用全量引入的方式时,需要使用umd提取的css文件作为样式文件引入(因为每个组件本身是没有引入任何样式文件的)

package.json

{
  "types": "dist/index.d.ts", // 指定ts类型文件入口
  "main": "lib/index", // 指定cjs方式入口
  "module": "es/index" // 指定esm方式入口
}

因此,对应的生成文件分别对应 dist  lib  es  三个目录

组件关键目录

基于基本原则,设计此目录结构
.
├── package-a // 组件 PackageA 目录
│   ├── __test__ // 单元测试文件目录
│   │   ├── index.spec.tsx // 以 .spec|test.tsx 结尾
│   ├── style // 样式文件目录
│   │   ├── index.less
│   │   ├── index.ts
│   ├── index.tsx
├── package-b // 组件 PackageB 目录
├── style // 样式文件
│   ├── core // 核心文件主要包括mixin/function等
│   │   ├── mixin.less
│   ├── component.less // 所有组件的样式文件入口
│   ├── entry.less // 样式打包入口less
│   ├── entry.ts // 样式打包入口
│   ├── index.less
│   ├── index.ts // 用于单个组件样式引用
│   ├── theme.less // 默认主题
└── 

打包配置

建议 esm cjs umd 分开打包,也就最终执行三次 father build 

esm打包配置

只编译ts(x)/js(x),不处理less样式文件,样式文件保持原样到输出目录。
// .fatherrc.ts
export default {
  runtimeHelpers: true,
  entry: 'src/index.ts',
  esm: {
    type: 'babel',
  },
  lessInBabelMode: false,
}

cjs打包配置

基本与esm相同
// .fatherrc.ts
export default {
  runtimeHelpers: true,
  entry: 'src/index.ts',
  esm: {
    type: 'cjs',
  },
  lessInBabelMode: false,
}

umd打包配置

umd打包配置存在较大不同,需要配置多入口,除了 src/index.ts 入口外,还需以 src/style/entry.ts (样式文件)为入口,umi father将跟进 entry 把项目依赖打包在一起输出一个文件。
// .fatherrc.ts
export default {
  entry: ['src/index.ts', 'src/style/entry.ts'],
  autoprefixer: {
    flexbox: 'no-2009',
  },
  extractCSS: true,
  runtimeHelpers: true,
  umd: {
    globals: {
      react: 'React',
    },
    minFile: true,
  },
  overridesByEntry: {
    'src/index.ts': {
      umd: { name: 'ufs', file: 'index' },
    },
    'src/style/entry.ts': {
      umd: { file: 'entry' },
    },
  },
}

在打包完成后,我们还需要一些重命名、删除多余文件等操作

// script/build-umd.js
const fs = require('fs-extra');
const util = require('util');
const { exec } = require('child_process');
const path = require('path');

const execSync = util.promisify(exec);
const getRelativePath = pathStr => path.join(__dirname, pathStr);

const build = async () => {
  console.info('Build umd');
  await execSync('father build');
  await fs.removeSync(getRelativePath('../dist/entry.js'));
  await fs.removeSync(getRelativePath('../dist/entry.min.js'));
  await fs.moveSync(getRelativePath('../dist/entry.css'), getRelativePath('../dist/index.css'));
  await fs.moveSync(getRelativePath('../dist/entry.min.css'), getRelativePath('../dist/index.min.css'));
};

build();

单元测试

umi father 集成了 jest 用于单元测试,但针对react组件,还需要一些额外配置。
// jest.config.js
module.exports = {
  setupFiles: ['<rootDir>/setupTests.ts'],
  // testEnvironment: 'node', // 测试环境可选值为 jsdom(default)/node
};
// setupTests.ts
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// import 'jsdom-global/register'; // 如果测试环境为node,还需要引入jsdom

configure({ adapter: new Adapter() });
// 一个简单的测试示例
// 测试文件应以 .test|spec.ts(x)|js(x) 结尾
import React from 'react';
import { render } from 'enzyme';
import toJSON from 'enzyme-to-json';
import Hello from '../index';

describe('Hello Component', () => {
  it('test render', () => {
    const wrapper = render(<Hello />);
    expect(toJSON(wrapper)).toMatchSnapshot();
  });
});

文档

umi father 包括两块,文档(cli命令为father doc,等同于dumi)和组件打包(cli命令为father build,等同于father-build)。

这一块的内容不详述,参考 dumi

示例工程

umi-father-seed

Q&A

待述

vdfor
100 声望9 粉丝

大前端