4

foreword

Regardless of the size of the team, as time progresses, there will be more or less extractable business components. Precipitating component libraries and corresponding documents is the only way.

Go straight to the topic, starting from 0 to 1 to start a business component library (which can be generated from comments).

The final Demo can be seen here , please use Mac or Linux terminal to run, the windows compatibility has not been verified.

tools used

These three tools are used in the subsequent construction of the business component library, and you need to have a certain understanding:

  • Lerna , Lerna is an Npm multi-package management tool , you can check the official documentation for details.
  • Docusaurus is a documentation tool supported by Facebook's official website. It can build a beautiful documentation website in a very short time. For details, you can check the official website documentation.
  • Vite , Vite is a new front-end construction tool that can significantly improve the front-end development experience. Out of the box, it can be used to replace the rollup build code to save some tedious configuration.

Initialize the project

Note that Node version needs to be above v16 version, it is best to use v16 version.

The initialized file structure is as follows:

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

Assuming the project root folder:

  1. The first step is to initialize the Lerna project

     $ npx lerna@latest init

    lerna will add package.json and lerna.json .

  2. The second step is to initialize the Docusaurus project (typescript type)

     $ npx create-docusaurus@latest website classic --typescript
  3. The third step , configure package.json

    • npm run bootstrap Initial installation of all subpackage dependencies.
    • npm run postinstall is an npm hook command, which will trigger the operation of npm run postinstall after the dependent package is installed.
     {
      "private": true,
      "dependencies": {
        "lerna": "^5.1.4"
      },
      "scripts": {
        "postinstall": "npm run bootstrap",
        "bootstrap": "lerna bootstrap"
      }
    }
  4. The fourth step , configure lerna.json

    • packages Set the location of subcontracting, and configure the lerna document in detail.
    • npmClient can be specified npm client, can be replaced with internal npm client or yarn .
    • hoist After set to true, if the same dependent package of the subpackage is the same, it will be installed in the root directory of the top-level project root/node_modules , if it is not the same, there will be a warning, the same The same version is installed to the topmost root directory, and the different dependent package versions are installed to the current subpackage node_modules currently.
     {
     "packages": ["packages/*", "website"],
     "version": "0.0.0",
     "npmClient": "npm",
     "hoist": true
    }

Use Vite to create component subpackages

The final folder path is as follows:

 .
├── lerna.json
├── package.json
├── packages
│   └── components
│       ├── package.json
│       ├── src
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
└── website
  1. The first step , create packages/components folder
  2. The second step is to initialize the Vite project and select the template of react-ts .

     $ npm init vite@latest
  3. The third step , delete unnecessary files

    Since only the packaging function of Vite is used, the service development function of Vite cannot be used, so some cleaning needs to be done.

    Delete index.html and empty src folder.

  4. The fourth step , configuration packages/components/vite.config.ts

    For the detailed configuration of Vite, you can check the official documents, and you can take a closer look at the library mode of configuring Vite. The packaging of Vite is actually based on rollup. Here are the configurations that need to be paid attention to:

    • rollupOptions.external configuration

      Make sure to externalize dependencies that you don't want to be packaged into the library. Common dependencies such as React don't need to be packaged.

    • rollupOptions.globals configuration

      Provide a global variable for these externalized dependencies in UMD build mode.

      Less configuration

      Vite supports Less by default, and it takes effect after adding less dependency package to package.json. The css module feature is also supported by default.

      Because it is a library type, less needs to configure the class prefix. Here, the component name prefix is obtained from the path of type src/Table.module.less or src/Table/index.module.less .

 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. The fifth step , configure packages/components/package.json

    Note the configuration of three fields:

    • main , if the module field is not set, packaging tools such as Webpack and Vite will use the file set in this field as the entry file of the dependent package.
    • module , the default priority of a general toolkit is higher than main , this field should point to a module based on the ES6 module specification , so that the packaging tool can support the feature of Tree Shaking .
    • files , set the files or folders published on npm, the default package.json does not need to be processed.
     {
      "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"
      }
    }

Create components

Create two files under package/components/src currently:

  • 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;

Write component documentation

Docusaurus is a function that supports mdx, but cannot read comments, and is there any component that can show Demo and Demo code together.

Therefore, some preparations need to be made before writing the document to support the usage of PropsTable and CodeShow . The details of the implementation will not be elaborated here. If you are interested, you can check the react-doc-starter .

Component documentation preparation

  1. The first step , the md file supports the direct use of PropsTable and CodeShow components

    Create the following files and add the corresponding dependency packages. The content of the files can be obtained in this project react-doc-starter .

     website/loader/propsTable.js
    website/loader/codeShow.js
    website/plugins/mdx.js
  2. The second step is to support the Less function

    The functions of Less need to be consistent with the Less configured in Vite packaging.

     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. The third step , support alias

    Need to add website/plugin/alias.js plugin and modification 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. The fourth step , configure website/docusaurus.config.js use the plugin:

     const config = {
      ...
      plugins: [
        './plugins/less',
        './plugins/alias',
        './plugins/mdx',
      ],
      ...
    };
    
    module.exports = config;
  5. The fifth step , modify the default document path and the default sidebar path

    Since we may also have other documents such as Utils documents, we need to configure it outside website/docusaurus.config.js :

    Change the file path to website/docs/components , and change the sidebar path to webiste/componentsSidebars.js , just rename the sidebar file without any processing.

     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;

Formally write component documentation

website/demo文件中Test/Basic.tsx 271dd5cb43a683ecc576270f3efa3afc---和Test/Basic.csswebsite/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 is the usage of Docusaurus, click here for details.

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

docs/components/data-show/Test.md

If you want to customize this Markdown, such as sidebar text, order, etc., you can read the Markdown preface .

The usage of CodeShow and PropsTable components can be found here .

 ## 使用

### 基本使用

<CodeShow fileList={['$demo/ProContent/Basic.tsx', '$demo/ProContent/Basic.css']} />

## Props

<PropsTable src="$components/ProContent" showDescriptionOnSummary />

run document service

Running the document service here, you can view the effect of the Demo, which is equivalent to writing the document and debugging the component at the same time, without the need to develop another service to debug the component.

 $ cd ./website
$ npm start

publish component

Run in the project root root directory:

 $ npm run build:publish

This name will run the lerna build and lerna publish commands, and then install the prompt and publish it. For detailed usage, see the lerna command.

Deployment documentation

See Docusaurus for details.

If the Github project recommends publishing to GitHub Pages, the command is as follows:

 $ cd ./website
$ npm run deploy

expand

Convert all files in src

When packaging, Vite will package all the involved files into one file, and we often convert all JavaScript or Typescript files in the src folder into es5 syntax and provide them directly to development service tools such as Webpack and Vite.

Vite does not support the mode of multiple entry files and multiple output files. I have implemented a vite plugin vite-plugin-build , which supports the modes of single file single output and multiple files multiple output.

The package.json scripts are modified as follows:

 {
  "scripts": {
    "tsc": "tsc",
    "clean": "rimraf lib es dist",
    "build": "npm run clean && npm run tsc && vite build"
  },
  "devDependencies": {
    "vite-plugin-build": "^0.2.2",
  }
}

vite.config.ts is modified as follows:

 import path from 'path';
import { defineConfig } from 'vite';
import { buildPlugin } from 'vite-plugin-build';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react(),
    buildPlugin({
      libBuild: {
        buildOptions: {
          rollupOptions: {
            external: ['react'],
            output: { globals: { dayjs: 'React' } },
          },
          lib: {
            entry: path.resolve(__dirname, 'src/index.ts'),
            name: 'RbacComponents',
            fileName: (format) => `rbac-components.${format}.js`,
          },
        },
      },
    }),
  ],

  css: {
    modules: {
      localsConvention: 'camelCaseOnly',
      generateScopedName: (name: string) => `rbac-${name}`,
    },
  },
});

Add unit tests

Unit testing uses Vitest , Vitest can share the configuration of Vite, the configuration is also very simple, and it is compatible with most of the usage of Jest.

The following steps are handled based on subcontracting packages/components .

  1. The first step , update 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. The second step , update vite.config.js

    The configuration vitest itself, needs to add the test attribute to the Vite configuration. If you use vite of defineConfig , you also need to write the triple slash directive at the top of the configuration file.

    The configuration is very simple, only need to close watch and add dom execution environment.

     /// <reference types="vitest" />
    
    export default defineConfig(() => {
      return {
        ...
        test: {
          environment: 'happy-dom',
          watch: false,
        },
        ...
      };
    });
  3. The third step , add a test case 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();
    });

Use Vite to initialize Utils subpackage

In fact, in addition to business components, we will also have functions of the business Utils tool class, and we will also precipitate tool class libraries and corresponding documents.

Thanks to the multi-package management method, I put the component library and the Utils class library together and create a new utils subpackage in package .

The utils subcontracting is similar to the components subcontracting, and the configuration of vite and package.json will not be discussed in detail. You can refer to the above to use Vite to create component subcontracting .

Create utility function

Create utils/src/isNumber.ts file (example).

 /**
 * @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;
}
Writing utility function documentation

Because the utility function is not suitable for reading comments using PropsTable, and the efficiency of manually writing markdown is low, I implemented a Docusaurus plug-in based on Microsoft tsdoc.

  1. The first step , the md file supports adding website/plugins/tsdoc.js directly using the TsDoc component, and you need to add the corresponding dependency package. The content of the file can be obtained in this project 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. The second step , configure docusaurus.config.js

    Configure the utils document path as docs/utils and the sidebar path as ./utilsSidebars and add the tsdoc plugin.

     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. The third step , add docs/utils/isNumber.md file

    $utils Alias need to add the corresponding configuration in ./plugins/alias and tsconfig.json .

    sidebar_position Can modify the order of menu items at the same level in the sidebar.

     ---
    sidebar_position: 1
    ---
    
    <TsDoc src="$utils/isNumber" />
publish component

Subpackage with components and run in the root directory of the project root:

 $ npm run build:publish

This name will run the lerna build and lerna publish commands, and then install the prompt and publish it. For detailed usage, see the lerna command.

Deployment documentation

Ditto for deployment documentation above.

Epilogue

After supporting PropsTable , CodeShow and TsDoc three convenient components, the documentation tool I built can quickly write and generate documents.

If it is helpful to you, you can give a like or go to the Github project to collect it.


Samon
1.3k 声望92 粉丝