使用dumi搭建组件库

8,933 阅读7分钟

该教程主要是仿照pro-componentsant-design, 所以感谢dumi, ant-designpro-components,让前端组件更加丰富, 感谢开源社区。

以前本想用storybook搭建一个组件库,后来通过公司的一位前辈了解到,通过dumi搭建更加方便,因此学习一下。

通过本篇文章,可以大概了解到的内容有

  • dumi创建组件库
  • lerna管理多个包
  • prettier和eslint配置代码风格
  • git hooks和commitlin配置提交规范

dumi创建组件库

  1. 创建一个k-component的目录并用vscode打开
mkdir k-component
cd k-component
vscode .
  1. 用dumi脚手架创建组件库项目
npx @umijs/create-dumi-lib --site
  1. 下载依赖并运行
npm install
npm run start

运行的结果如下图所示

企业微信截图_16460981702060.png

为了让界面和pro-components一样好看一点,可以把pro-components的.dumi/theme目录复制到自己的项目中,

企业微信截图_1646098766170.png

然后重新运行,出现以下错误信息

企业微信截图_16460980276060.png

按照提示下载依赖

npm install --save @ant-design/pro-layout @ant-design/pro-skeleton antd moment react-helmet-async react-lazyload

重新运行npm run start, 出现下图所示

企业微信截图_1646098368573.png

用lerna管理组件库

lerna按照官方的解释

Lerna是一个管理工具,用于管理包含多个软件包(package)的JavaScript项目

下面是一些使用步骤

  1. 下载lerna
npm install --global lerna
  1. 在项目根目录中初始化lerna
lerna init

使用lerna初始化之后,会在项目的根目录中多出一个lerna.json文件和packages目录

企业微信截图_16461016641636.png

  1. 在packages目录中创建子包
# 创建一个button组件
lerna create @keith/button packages/button --yes

# 创建一个tag组件
lerna create @keith/tag packages/tag --yes

企业微信截图_16461027489209.png

  1. 创建包之后,需要通过lerna link把当前lerna存储中相互依赖的lerna包符号链接在一起;然后在tsconfig.json文件中新加一个paths, 然后把刚才的组件目录添加进去,否则在组件写demo的时候,不能自动识别到引用的组件
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "react",
    "esModuleInterop": true,
    "types": ["jest"],
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    // 把组件目录添加到该处
    "paths": {
      "@keith/button":["./packages/button/src/index.tsx"],
      "@keith/tag":["./packages/tag/src/index.tsx"]
    }
  }
}

运行完lerna link后如果不能生效,需要重启vscode

  1. 修改组件包中的package.json, 例如button组件下的package.json
{
  "name": "@keith/button",
  "version": "0.0.0",
  "keywords": [],
  "license": "MIT",
  "main": "lib/index.js",
  "module": "es/index.js",
  "types": "lib/index.d.ts",
  "files": [
    "lib",
    "es",
    "dist"
  ],
  "peerDependencies": {
    "antd": "4.x",
    "react": ">=16.9.0",
    "react-dom": ">=16.9.0"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org"
  }
}

  1. .fatherrc.ts文件中,添加buttontag
import { readdirSync } from 'fs';
import { join } from 'path';

// utils must build before core
// runtime must build before renderer-react
// components dependencies order: form -> table -> list

const headPkgs: string[] = ['button', 'tag']; // 添加button和tag
const tailPkgs = readdirSync(join(__dirname, 'packages')).filter(
  (pkg) => pkg.charAt(0) !== '.' && !headPkgs.includes(pkg),
);

const type = process.env.BUILD_TYPE;

let config = {};

if (type === 'lib') {
  config = {
    cjs: { type: 'babel', lazy: true },
    esm: false,
    runtimeHelpers: true,
    pkgs: [...headPkgs, ...tailPkgs],
    extraBabelPlugins: [
      ['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],
    ],
  };
}

if (type === 'es') {
  config = {
    cjs: false,
    esm: {
      type: 'babel',
    },
    runtimeHelpers: true,
    pkgs: [...headPkgs, ...tailPkgs],
    extraBabelPlugins: [
      [require('./scripts/replaceLib')],
      ['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],
    ],
  };
}

export default config;

father是一个构建工具,可以支持cjsesmumd打包,具体看查看father

  1. package.json中添加workspaces
   // ...
   
   // 添加workspaces
  "workspaces": [
    "packages/*"
  ],
  "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",
    "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
    "test": "umi-test",
    "test:coverage": "umi-test --coverage",
    "prepublishOnly": "npm run build"
  },
  // ...
  1. 去除多余的目录,比如组件目录下的lib_test_和根目录下的src

企业微信截图_16462073711234.png

然后重新npm run start, 最后的效果如下图所示

企业微信截图_1646207483783.png

组件库文档中的导航配置,具体可以看基础使用 - dumi (umijs.org)

常用的lerna命令

lerna init // 初始化lerna配置
lerna create // 创建lerna包
lerna link // 链接lerna包
lerna add  // 安装单个依赖
lerna bootstrap // 安装全局依赖并链接到所有子包
lerna clean // 清除node_modules
lerna list // 列出子包

具体可以看lerna

下面记录代码规范和提交规范的内容。。。

prettier和eslint配置代码风格

首先先在vscode中下载prettiereslint,

企业微信截图_16462113298886.png

企业微信截图_16462113645496.png 用dumi脚手架创建的项目中已经有prettier的配置文件了

// .prettierrc.js
module.exports = require('@umijs/fabric').prettier;


// 通过引用路径可以找到@umijs/fabric下的prettier配置,配置如下
"use strict";
/** @format */
module.exports = {
    singleQuote: true, // 单引号
    trailingComma: 'all', // 对象{}的最后一个属性是否加,
    printWidth: 100, // 每行超过100个字符换行
    proseWrap: 'never', // 不强制换行
    endOfLine: 'lf', // 以lf为换行符,常见的还有cr,cflf
    overrides: [
        {
            files: '.prettierrc',
            options: {
                parser: 'json',
            },
        },
        {
            files: 'document.ejs',
            options: {
                parser: 'html',
            },
        },
    ],
};

常见的prettier配置还有

"semi": false, // 是否有分号
"bracketSpacing": true, // 在对象中{a: 1},是否答应空格如果是{ a: 1 }
"arrowParens": "always" // 设置箭头函数中的参数是否包裹()

然后打开vscode中的文件>>首选项>>设置

企业微信截图_1646212631498.png

打开之后,在项目的工作目录中会出现一个.vscode/settings.json的文件,配置如下

{
  "editor.formatOnSave": true, // 自动保存的时候格式化代码
  "search.exclude": { // 排除下面的文件
    "**/node_modules": true,
    "dist": true,
    "yarn.lock": true,
  },
}

ctrl + s的时候就会自动格式化代码

prettier解决了代码的风格统一的问题,对于代码质量的问题需要用到eslint, 在该项目的根目录中新建.eslintrc.js.eslintignore文件,因为该项目中已安装了umi, 可以像配置prettier那样,直接用umi中的eslint配置


// .eslintrc.js
module.exports = {
  extends: [require.resolve('@umijs/fabric/dist/eslint')],
};

// umijs下的eslint
module.exports = {
    extends: ['prettier', 'plugin:react/recommended'],
    parser: '@babel/eslint-parser',
    plugins: ['react', 'jest', 'unicorn', 'react-hooks'],
    env: {
        browser: true,
        node: true,
        es6: true,
        mocha: true,
        jest: true,
        jasmine: true,
    },
    rules: {
        strict: ['error', 'never'],
        'react/display-name': 0,
        'react/jsx-props-no-spreading': 0,
        'react/state-in-constructor': 0,
        'react/static-property-placement': 0,
        // Too restrictive: https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/destructuring-assignment.md
        'react/destructuring-assignment': 'off',
        'react/jsx-filename-extension': 'off',
        'react/no-array-index-key': 'warn',
        'react-hooks/rules-of-hooks': 'error',
        'react-hooks/exhaustive-deps': 'warn',
        'react/require-default-props': 0,
        'react/jsx-fragments': 0,
        'react/jsx-wrap-multilines': 0,
        'react/prop-types': 0,
        'react/forbid-prop-types': 0,
        'react/sort-comp': 0,
        'react/react-in-jsx-scope': 0,
        'react/jsx-one-expression-per-line': 0,
        'generator-star-spacing': 0,
        'function-paren-newline': 0,
        'sort-imports': 0,
        'class-methods-use-this': 0,
        'no-confusing-arrow': 0,
        'linebreak-style': 0,
        // Too restrictive, writing ugly code to defend against a very unlikely scenario: https://eslint.org/docs/rules/no-prototype-builtins
        'no-prototype-builtins': 'off',
        'unicorn/prevent-abbreviations': 'off',
        // Conflict with prettier
        'arrow-body-style': 0,
        'arrow-parens': 0,
        'object-curly-newline': 0,
        'implicit-arrow-linebreak': 0,
        'operator-linebreak': 0,
        'no-param-reassign': 2,
        'space-before-function-paren': 0,
        'react/self-closing-comp': 1,
        'react/jsx-key': 1,
    },
    settings: {
        // support import modules from TypeScript files in JavaScript files
        'import/resolver': {
            node: {
                extensions: isTsProject ? ['.js', '.jsx', '.ts', '.tsx', '.d.ts'] : ['.js', '.jsx'],
            },
        },
        'import/parsers': {
            '@typescript-eslint/parser': ['.ts', '.tsx', '.d.ts'],
        },
        'import/extensions': ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.d.ts'],
        'import/external-module-folders': ['node_modules', 'node_modules/@types'],
        polyfills: ['fetch', 'Promise', 'URL', 'object-assign'],
    },
    overrides: isTsProject
        ? [
            {
                files: ['**/*.{ts,tsx}'],
                parser: '@typescript-eslint/parser',
                rules: tsEslintConfig_1.default,
                extends: ['prettier', 'plugin:@typescript-eslint/recommended'],
            },
        ]
        : [],
    parserOptions: parserOptions,
};

eslint中的配置太多了,具体可以查看的官方文档ESLint 配置好了之后,如果编写的代码不符合eslint中定义的规则,则会出现提示

企业微信截图_16462696199395.png

git hooks和commitlin配置提交规范

git hooks类似于vue中的钩子函数一样,常用的有pre-commitcommit-msg, 这两个都是在git commit之前执行。除了这两个之外,还有其它的git hooks,详情可以看git hooks官网
因为dumi项目中自带yorkie来配置git hooks, 所以就不用husky去配置。

  1. 安装依赖
npm install @umijs/yorkie -D
  1. 在项目的根目录的package.json配置
{
    // ...
  "gitHooks": {
    "pre-commit": "lint-staged",
    "commit-msg": "fabric verify-commit" //这里commitlint配置文件是在 "./node_modules/@umijs/fabric/dist/verifyCommit.js"
  },
  "lint-staged": {
    "packages/**/*.{js,ts,jsx,tsx, md, json}": [
      "eslint --fix",
      "prettier --write",
      "git add ."
    ]
  },
  // ...
}
  1. 代码提交
  • 使用git commit的时候,会触发eslint和prettier, 如果代码中有不符合eslint规则的,则提交不成功

企业微信截图_16463765003986.png

  • 修改eslint中的报错,然后重新提交。如果git commit不符合规范,则提交不成功,例如使用git commit -m "xxx"

企业微信截图_16463625384676.png

yorkie配置的过程中有一个坑,因为查看package.json文件中,发现已有yorkie@umijs/fabric的依赖,但是使用git commit的时候却没有触发git hooks。搜索了一下,最后在github上发现ant-design-pro项目中的package.json中有一个@umi/yorkie的依赖,试着下载一下,发现真的可以了。

本来还想写jest单元测试、gitlab-ci跑lint和test、lerna发包到npmjs.com,下次再写吧。。。

github

参考链接

dumi - 为组件开发场景而生的文档工具
pro-components
lerna
prettier中文文档
esint中文文档