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:
The first step is to initialize the Lerna project
$ npx lerna@latest init
lerna will add
package.json
andlerna.json
.The second step is to initialize the Docusaurus project (typescript type)
$ npx create-docusaurus@latest website classic --typescript
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 ofnpm run postinstall
after the dependent package is installed.
{ "private": true, "dependencies": { "lerna": "^5.1.4" }, "scripts": { "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap" } }
-
The fourth step , configure
lerna.json
-
packages
Set the location of subcontracting, and configure the lerna document in detail. -
npmClient
can be specifiednpm
client, can be replaced with internal npm client oryarn
. -
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 projectroot/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 subpackagenode_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
- The first step , create
packages/components
folder The second step is to initialize the Vite project and select the template of
react-ts
.$ npm init vite@latest
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 emptysrc
folder.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
orsrc/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`,
},
},
};
});
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" } }
- main , if the
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
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
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, }, ], }, ], }, ], }, }; }, }; };
The third step , support alias
Need to add
website/plugin/alias.js
plugin and modificationtsconfig.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/"] }
The fourth step , configure
website/docusaurus.config.js
use the plugin:const config = { ... plugins: [ './plugins/less', './plugins/alias', './plugins/mdx', ], ... }; module.exports = config;
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 towebiste/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.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 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
.
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" } }
The second step , update vite.config.js
The configuration
vitest
itself, needs to add thetest
attribute to the Vite configuration. If you usevite
ofdefineConfig
, 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, }, ... }; });
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.
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 }, }, ], }, ], }, }; }, }; };
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;
The third step , add
docs/utils/isNumber.md
file$utils
Alias need to add the corresponding configuration in./plugins/alias
andtsconfig.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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。