前言

无论团队大小,随着工夫的推动,多多少少都会有一些可提取的业务组件,积淀组件库和对应的文档是一条必经之路。

间接进入正题,从 0 到 1 开始搞一个业务组件库(可从正文中生成)。

最终的 Demo 可看这里,请应用 Mac 或者 Linux 终端来运行,windows 兼容性未做验证。

应用到工具

这三个工具是后续业务组件库搭建应用到的,须要有肯定的理解:

  • Lerna ,Lerna是一个 Npm 多包管理工具,具体可查看官网文档。
  • Docusaurus,是 Facebook 官网反对的文档工具,能够在极短时间内搭建丑陋的文档网站,具体可查看官网文档。
  • Vite,Vite 是一种新型前端构建工具,可能显著晋升前端开发体验,开箱即用,用来代替 rollup 构建代码能够省掉一些繁琐的配置。

初始化我的项目

留神 Node 版本须要在 v16 版本以上,最好应用 v16 版本。

初始化的文件构造如下:

.├── lerna.json├── package.json└── website

假如我的项目 root 文件夹:

  1. 第一步,初始化 Lerna 我的项目

    $ npx lerna@latest init

    lerna 会增加 package.jsonlerna.json

  2. 第二步,初始化 Docusaurus 我的项目(typescript 类型的)

    $ npx create-docusaurus@latest website classic --typescript
  3. 第三步,配置 package.json

    • npm run bootstrap 可初始化装置所有分包的依赖包。
    • npm run postinstall 是 npm 钩子命令,在依赖包实现装置后会触发 npm run postinstall 的运行。
    {  "private": true,  "dependencies": {    "lerna": "^5.1.4"  },  "scripts": {    "postinstall": "npm run bootstrap",    "bootstrap": "lerna bootstrap"  }}
  4. 第四步,配置 lerna.json

    • packages 设置分包的地位,具体配置可长 lerna 的文档。
    • npmClient 可指定应用的 npm 客户端,能够替换为外部的 npm 客户端或者 yarn
    • hoist 设置为true后,分包的同一个依赖包如果雷同,会对立装置到最上层我的项目的根目录 root/node_modules 中,如果不雷同会有正告,同一个雷同的版本装置到最上层根目录,不雷同的依赖包版本装置到以后分包的 node_modules 目前下。
    { "packages": ["packages/*", "website"], "version": "0.0.0", "npmClient": "npm", "hoist": true}

应用 Vite 创立组件分包

最终文件夹门路如下:

.├── lerna.json├── package.json├── packages│   └── components│       ├── package.json│       ├── src│       ├── tsconfig.json│       ├── tsconfig.node.json│       └── vite.config.ts└── website
  1. 第一步,创立 packages/components 文件夹
  2. 第二步,初始化 Vite 我的项目,选用 react-ts的模板。

    $ npm init vite@latest
  3. 第三步,删除不必要的文件

    因为只用 Vite 的打包性能,用不上 Vite 的服务开发性能,所以要做一些清理。

    删除 index.html 和 清空 src 文件夹。

  4. 第四步,配置 packages/components/vite.config.ts

    Vite 的具体配置能够查看官网文档,能够细看配置 Vite 的库模式,Vite 的打包其实是基于 rollup,这里阐明一下须要留神的配置:

    • rollupOptions.external 配置

      确保内部化解决那些你不想打包进库的依赖,如 React 这些公共的依赖包就不须要打包进来。

    • rollupOptions.globals 配置

      在 UMD 构建模式下为这些内部化的依赖提供一个全局变量。

      Less 配置

      Vite 默认是反对 Less 的,须要再 package.json 增加 less 依赖包后才失效。也默认反对 css module 性能。

      因为是库类型,所以 less 须要配置 classs 前缀,这里还依据 src/Table.module.less 或者 src/Table/index.module.less 类型的门路获取 Table 为组件名前缀。

import { defineConfig } from 'vite';import path from 'path';// 在 UMD 构建模式下为内部依赖提供一个全局变量export const GLOBALS = {  react: 'React',  'react-dom': 'ReactDOM',};// 解决类库应用到的内部依赖// 确保内部化解决那些你不想打包进库的依赖export const EXTERNAL = [  'react',  'react-dom',];// https://vitejs.dev/config/export default defineConfig(() => {  return {    plugins: [react()],    css: {      modules: {        localsConvention: 'camelCaseOnly',        generateScopedName: (name: string, filename: string) => {          const match = filename.replace(/\\/, '/').match(/.*\/src\/(.*)\/.*\.module\..*/);          if (match) {            return `rabc-${decamelize(match[1], '-')}__${name}`;          }          return `rabc-${name}`;        },      },      preprocessorOptions: {        less: {          javascriptEnabled: true,        },      },    },    build: {      rollupOptions: {        external: EXTERNAL,        output: { globals: GLOBALS },      },      lib: {        entry: path.resolve(__dirname, 'src/index.ts'),        name: 'RbacComponents',        fileName: (format) => `rbac-components.${format}.js`,      },    },  };});
  1. 第五步,配置 packages/components/package.json

    须要留神三个字段的配置:

    • main,如果没有设置 module 字段,Webpack、Vite 等打包工具会以此字段设置的文件为依赖包的入口文件。
    • module,个别的工具包默认优先级高于 main,此字段指向的应该是一个基于 ES6 模块标准的模块,这样打包工具能力反对 Tree Shaking 的个性。
    • files,设置公布到 npm 上的文件或者文件夹,默认 package.json 是不必做解决的。
    {  "version": "0.0.0",  "name": "react-antd-business-components",  "main": "dist/rbac-components.umd.js",  "module": "dist/rbac-components.es.js",  "files": [    "dist"  ],  "scripts": {    "build": "vite build"  },  "dependencies": {},  "devDependencies": {    "@types/react": "^18.0.14",    "@types/react-dom": "^18.0.5",    "@vitejs/plugin-react": "^1.3.2",    "classnames": "2.3.1",    "cross-spawn": "7.0.3",    "decamelize": "4.0.0",    "eslint": "8.18.0",    "less": "^4.1.3",    "prop-types": "^15.7.2",    "react": "^17.0.2",    "react-dom": "^17.0.2",    "rimraf": "^3.0.2",    "typescript": "^4.6.4",    "vite": "^2.9.12"  }}

创立组件

package/components/src 目前下创立两个文件:

  • index.ts

    export { default as Test } from './Test';
  • Test.tsx

    import React from 'react';export interface ProContentProps {  /**   * 题目   */  title?: React.ReactNode;  /**   * 内容   */  content: React.ReactNode;}/** * 展现题目和内容 */const Test: {  (props: ProContentProps): JSX.Element | null;  displayName: string;  defaultProps?: Record<string, any>;} = (props: ProContentProps) => {  const { title, content } = props;  return (    <div>      <div>{title}</div>      <div>{content}</div>    </div>;  );};Test.displayName = 'Card';Test.defaultProps = {  title: '题目',  content: "内容",};export default Test;

编写组件文档

Docusaurus 是反对 mdx 的性能,然而并不能读取正文,也有没组件能够一起展现 Demo 和 Demo 的代码。

所以在编写文档前还须要做一些筹备,反对 PropsTableCodeShow 的用法,这里实现的细节就不做细说,感兴趣的能够查看 react-doc-starter。

组件文档编写筹备

  1. 第一步,md 文件反对间接应用 PropsTable 和 CodeShow 组件

    新建以下几个文件,同时须要增加相应的依赖包,文件内容能够在这个我的项目 react-doc-starter 中获取。

    website/loader/propsTable.jswebsite/loader/codeShow.jswebsite/plugins/mdx.js
  2. 第二步,反对 Less 性能

    Less 的性能须要和 Vite 打包是配置的 Less 统一。

    const decamelize = require('decamelize');module.exports = function (_, opt = {}) {  delete opt.id;  const options = {    ...opt,    lessOptions: {      javascriptEnabled: true,      ...opt.lessOptions,    },  };  return {    name: 'docusaurus-plugin-less',    configureWebpack(_, isServer, utils) {      const { getStyleLoaders } = utils;      const isProd = process.env.NODE_ENV === 'production';      return {        module: {          rules: [            {              test: /\.less$/,              oneOf: [                {                  test: /\.module\.less$/,                  use: [                    ...getStyleLoaders(isServer, {                      modules: {                        mode: 'local',                        getLocalIdent: (context, _, localName) => {                          const match = context.resourcePath.replace(/\\/, '/').match(/.*\/src\/(.*)\/.*\.module\..*/);                          if (match) {                            return `rabc-${decamelize(match[1], '-')}__${localName}`;                          }                          return `rabc-${localName}`;                        },                        exportLocalsConvention: 'camelCase',                      },                      importLoaders: 1,                      sourceMap: !isProd,                    }),                    {                      loader: 'less-loader',                      options,                    },                  ],                },                {                  use: [                    ...getStyleLoaders(isServer),                    {                      loader: 'less-loader',                      options,                    },                  ],                },              ],            },          ],        },      };    },  };};
  3. 第三步,反对 alias

    须要增加 website/plugin/alias.js 插件和批改 tsconfig.json

    alias.js

    const path = require('path');module.exports = function () {  return {    name: 'alias-docusaurus-plugin',    configureWebpack() {      return {        resolve: {          alias: {            // 反对以后正在开发组件依赖包(这样依赖包就无需构建,可间接在文档中应用)            'react-antd-business-components': path.resolve(__dirname, '../../packages/components/src'),            $components: path.resolve(__dirname, '../../packages/components/src'), // 用于缩短文档门路            $demo: path.resolve(__dirname, '../demo'), // 用于缩短文档门路          },        },      };    },  };};

    tsconfig.json

    {  // This file is not used in compilation. It is here just for a nice editor experience.  "extends": "@tsconfig/docusaurus/tsconfig.json",  "compilerOptions": {    "baseUrl": ".",    "paths": {      "react-antd-business-components": ["../packages/components/src"]    }  },  "include": ["src/", "demo/"]}
  4. 第四步,配置 website/docusaurus.config.js 应用插件:

    const config = {  ...  plugins: [    './plugins/less',    './plugins/alias',    './plugins/mdx',  ],  ...};module.exports = config;
  5. 第五步,批改默认的文档门路和默认的 sidebar 门路

    因为咱们还可能有其余文档如 Utils 文档,咱们须要而外配置一下 website/docusaurus.config.js:

    把文档门路批改为 website/docs/components,sidebar 门路改为 webiste/componentsSidebars.js,sidebar 文件间接改名即可,无需做任何解决。

    const config = {  ...  presets: [    [      'classic',      /** @type {import('@docusaurus/preset-classic').Options} */      ({        docs: {          path: 'docs/components',          routeBasePath: 'components',          sidebarPath: require.resolve('./componentsSidebars.js'),          // Please change this to your repo.          // Remove this to remove the "edit this page" links.          editUrl: 'https://github.com/samonxian/react-doc-starter/tree/master',        },      }),    ],  ],  ...};module.exports = config;

正式编写组件文档

website/demo 文件中创立 Test/Basic.tsxTest/Basic.css ,在 website/docs/components/data-show 中创立 Test.md_category_.json 文件:

demo/Test/Basic.tsx

import React from 'react';import { Test } from 'react-antd-business-components';import './Basic.css';const Default = function () {  return (    <div className="pro-content-demo-container">      <Test title="题目" content="内容"/>    </div>  );};export default Default;

demo/Test/Basic.css

.test-demo-container {  background-color: #eee;  padding: 16px;}

docs/components/data-show/\_category\_.json 是 Docusaurus 的用法,具体点击这里。

{  "position": 2,  "label": "数据展现",  "collapsible": true,  "collapsed": false,  "link": {    "type": "generated-index"  }}

docs/components/data-show/Test.md

如果要定制化配置此 Markdown,如侧边栏文本、程序等等,可细看 Markdown 前言。

CodeShow 和 PropsTable 组件用法可看这里。

## 应用### 根本应用<CodeShow fileList={['$demo/ProContent/Basic.tsx', '$demo/ProContent/Basic.css']} />## Props<PropsTable src="$components/ProContent" showDescriptionOnSummary />

运行文档服务

这里运行文档服务,能够查看 Demo 的成果,相当于一遍写文档一边调试组件,无需另起开发服务调试组件。

$ cd ./website$ npm start

公布组件

在我的项目 root 根目录下运行:

$ npm run build:publish

此名会运行lerna buildlerna publish 命令,而后装置提醒进行公布即可,具体的用法可查看 lerna 命令。

部署文档

具体可看 Docusaurus。

如果是 Github 我的项目倡议公布到 GitHub Pages,命令如下:

$ cd ./website$ npm run deploy

拓展

应用 Vite 转换 src 中的文件

打包时候,Vite 会把所有波及到的文件打包为一个文件,而咱们经常把 src 文件夹的所有 JavaScript 或者 Typescript 文件转换为 es5 语法,间接提供为 Webpack、Vite 这些开发服务工具应用。

Vite 不反对多入口文件和多输入文件的模式,须要本人实现一套 npm run build:file 命令。

  1. 第一步,package.json 增加如下 scripts 命令

    {  "scripts": {    "tsc:es": "tsc --declarationDir es",    "tsc:lib": "tsc --declarationDir lib",    "tsc": "npm run tsc:lib && npm run tsc:es",    "clean": "rimraf lib es dist",    "vite": "vite",    "build:lib": "vite build",    "build:file": "node ./scripts/buildFiles.js",    "build": "npm run clean && npm run tsc && npm run build:file && npm run build:lib"  }}
  2. 第二步,增加 package/components/.env 文件,确保构建环境未 production

    NODE_ENV=production
  3. 第三步,创立 package/components/scripts/buildFile.js 文件:

    • 通过读取 src 文件夹中(蕴含子辈)的所有 js 或 ts 类型的文件获取所有待转换的文件门路
    • 而后遍历文件门路,再通过 vite 的 mode 选项传递入口文件到 vite.file.config.ts 配置中
    const fs = require('fs');const path = require('path');const spawn = require('cross-spawn');const srcDir = path.resolve(__dirname, '../src');// 所有 src 文件夹包含子文件夹的 js、ts、jsx、tsx 文件门路数组const srcFilePaths = getTargetDirFilePaths(srcDir);srcFilePaths.forEach((file) => { const fileRelativePath = path.relative(srcDir, file); spawn( 'npm', ['run', 'vite', '--', 'build', '--mode', fileRelativePath, '--outDir', 'es', '--config', './vite.file.config.ts'], {   stdio: 'inherit', }, );});/*** 获取 src 文件夹下的所有文件* @param {String} [targetDirPath] 指标文件夹门路* @return {Array} 文件列表数组*/function getTargetDirFilePaths(targetDirPath = path.resolve(__dirname, '../src')) { let fileList = []; fs.readdirSync(targetDirPath).forEach((file) => { const filePath = path.resolve(targetDirPath, file); const isDirectory = fs.statSync(filePath).isDirectory(); if (isDirectory) {   fileList = fileList.concat(getTargetDirFilePaths(filePath)); } else {   fileList.push(filePath); } }); return fileList .filter((f) => {   if (/__tests__/.test(f)) {     return false;   }   if (/\.d\.ts/.test(f)) {     return false;   }   if (/\.[jt]?sx?$/.test(f)) {     return true;   }   return false; }) .map((f) =>   // 兼容 windows 门路   f.replace(/\\/g, '/'), );}
  4. 第四步,创立 package/components/vite.file.config.ts 文件:

    其中须要留神的是 rollupOptions.external 字段的配置,除了 lesscsssvg 的后缀名文件外,都不打包进输入文件。

    import { defineConfig } from 'vite';import react from '@vitejs/plugin-react';import path from 'path';import decamelize from 'decamelize';export const commonConfig = defineConfig({  plugins: [react()],  css: {    modules: {      localsConvention: 'camelCaseOnly',      generateScopedName: (name: string, filename: string) => {        const match = filename.replace(/\\/, '/').match(/.*\/src\/(.*)\/.*\.module\..*/);        if (match) {          return `rabc-${decamelize(match[1], '-')}__${name}`;        }        return `rabc-${name}`;      },    },    preprocessorOptions: {      less: {        javascriptEnabled: true,      },    },  },  resolve: {    alias: [      // fix less import by: @import ~      // less import no support webpack alias '~' · Issue #2185 · vitejs/vite      { find: /^~/, replacement: '' },    ],  },});// https://vitejs.dev/config/export default defineConfig(({ mode }) => {  return {    ...commonConfig,    build: {      emptyOutDir: false,      rollupOptions: {        external: (id) => {          if (id.includes('.less') || id.includes('.css') || id.includes('.svg')) {            return false;          }          return true;        },        output: [          {            file: `es/${mode.replace(/\.[jt]?sx?$/, '.js')}`,            indent: false,            exports: 'named',            format: 'es',            dir: undefined,          },          {            file: `lib/${mode.replace(/\.[jt]?sx?$/, '.js')}`,            indent: false,            exports: 'named',            format: 'cjs',            dir: undefined,          },        ],      },      lib: {        // mode 非凡解决为文件名        entry: path.resolve(__dirname, 'src', mode),        name: 'noop', // 这里设置只有在 UMD 格局才无效,防止验证报错才设置的,在这里没用      },      minify: false,    },  };});

增加单元测试

单元测试应用 Vitest,Vitest 可共用 Vite 的配置,配置也很简略,同时兼容 Jest 的绝大部分用法。

下方的步骤基于分包 packages/components 来解决。

  1. 第一步,更新 package.json

    {  "scripts": {    "test": "vitest",    "coverage": "vitest run --coverage"  },  "devDependencies": {    "happy-dom": "^6.0.2",    "react-test-renderer": "^17.0.2",    "vitest": "^0.18.0"  }}
  2. 第二步,更新 vite.config.js

    配置 vitest 自身,须要在 Vite 配置中增加 test 属性。如果你应用 vitedefineConfig ,还须要将 三斜线指令 写在配置文件的顶部。

    配置非常简略,只须要敞开 watch 和 增加 dom 执行环境。

    /// <reference types="vitest" />export default defineConfig(() => {  return {    ...    test: {      environment: 'happy-dom',      watch: false,    },    ...  };});
  3. 第三步,增加 .env.test,确保环境为 test 环境。

    NODE_ENV=test
  4. 第四步,增加测试用例 packages/components/src/__tests__/Test.spec.tsx

    import { expect, it } from 'vitest';import React from 'react';import renderer from 'react-test-renderer';import Test from '../index';function toJson(component: renderer.ReactTestRenderer) {  const result = component.toJSON();  expect(result).toBeDefined();  return result as renderer.ReactTestRendererJSON;}it('ProContent rendered', () => {  const component = renderer.create(    <Test />,  );  const tree = toJson(component);  expect(tree).toMatchSnapshot();});

应用 Vite 初始化 Utils 分包

其实除了业务组件,咱们还会有业务 Utils 工具类的函数,咱们也会积淀工具类库和相应的文档。

得益于多包治理的形式,自己把组件库和 Utils 类库放在一起解决,在 package 中新建 utils 分包。

utils 分包和 components 分包大同小异,vite 和 package.json 配置就不细说了,可参考上方应用 Vite 创立组件分包。

创立工具函数

创立 utils/src/isNumber.ts 文件(范例)。

/** * @param value? 检测的指标 * @param useIsFinite 是否应用 isFinite,设置为 true 时,NaN,Infinity,-Infinity 都不算 number * @default true * @returns true or false * @example * ```ts * isNumber(3) // true * isNumber(Number.MIN_VALUE) // true * isNumber(Infinity) // false * isNumber(Infinity,false) // true * isNumber(NaN) // false * isNumber(NaN,false) // true * isNumber('3') // false * ``` */export default function isNumber(value?: any, useIsFinite = true) {  if (typeof value !== 'number' || (useIsFinite && !isFinite(value))) {    return false;  }  return true;}
编写工具函数文档

因为工具类函数不适宜应用 PropsTable 读取正文,手动编写 markdown 效率又低,自己基于微软 tsdoc 实现了一个 Docusaurus 插件。

  1. 第一步,md 文件反对间接应用 TsDoc 组件
    增加 website/plugins/tsdoc.js,同时须要增加相应的依赖包,文件内容能够在这个我的项目 react-doc-starter 中获取。

    module.exports = function (context, opt = {}) {  return {    name: 'docusaurus-plugin-tsdoc',    configureWebpack(config) {      const { siteDir } = context;      return {        module: {          rules: [            {              test: /(\.mdx?)$/,              include: [siteDir],              use: [                {                  loader: require.resolve('ts-doc-webpack-loader'),                  options: { alias: config.resolve.alias, ...opt },                },              ],            },          ],        },      };    },  };};
  2. 第二步,配置 docusaurus.config.js

    配置 utils 文档门路为 docs/utils,sidebar 门路为 ./utilsSidebars,还要增加 tsdoc 插件。

    const config = {  ...  plugins: [    [      'content-docs',      /** @type {import('@docusaurus/plugin-content-docs').Options} */      ({        id: 'utils',        path: 'docs/utils',        routeBasePath: 'utils',        editUrl: 'https://github.com/samonxian/react-doc-starter/tree/master',        sidebarPath: require.resolve('./utilsSidebars.js'),      }),    ],    './plugins/tsdoc',  ],  ...};module.exports = config;
  3. 第三步,增加 docs/utils/isNumber.md 文件

    $utils 别名须要在 ./plugins/aliastsconfig.json 中增加对应的配置。

    sidebar_position 能够批改侧边栏同级菜单项的程序。

    ---sidebar_position: 1---<TsDoc src="$utils/isNumber" />
公布组件

同 components 分包,在我的项目 root 根目录下运行:

$ npm run build:publish

此名会运行lerna buildlerna publish 命令,而后装置提醒进行公布即可,具体的用法可查看 lerna 命令。

部署文档

同上的部署文档。

结语

通过反对 PropsTableCodeShowTsDoc 三个便捷的组件,自己搭建的文档工具能够疾速编写并生成文档。

如果对你有所帮忙,能够点个赞,也能够去 Github 我的项目珍藏一下。