前言
无论团队大小,随着工夫的推动,多多少少都会有一些可提取的业务组件,积淀组件库和对应的文档是一条必经之路。
间接进入正题,从 0 到 1 开始搞一个业务组件库(可从正文中生成)。
最终的 Demo 可看这里,请应用 Mac 或者 Linux 终端来运行,windows 兼容性未做验证。
应用到工具
这三个工具是后续业务组件库搭建应用到的,须要有肯定的理解:
- Lerna,Lerna 是一个 Npm 多包管理工具,具体可查看官网文档。
- Docusaurus,是 Facebook 官网反对的文档工具,能够在极短时间内搭建丑陋的文档网站,具体可查看官网文档。
- Vite,Vite 是一种新型前端构建工具,可能显著晋升前端开发体验,开箱即用,用来代替 rollup 构建代码能够省掉一些繁琐的配置。
初始化我的项目
留神 Node 版本须要在 v16 版本以上,最好应用 v16 版本。
初始化的文件构造如下:
.
├── lerna.json
├── package.json
└── website
假如我的项目 root 文件夹:
-
第一步,初始化 Lerna 我的项目
$ npx lerna@latest init
lerna 会增加
package.json
和lerna.json
。 -
第二步,初始化 Docusaurus 我的项目(typescript 类型的)
$ npx create-docusaurus@latest website classic --typescript
-
第三步,配置
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" } }
-
第四步,配置
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
- 第一步,创立
packages/components
文件夹 -
第二步 ,初始化 Vite 我的项目,选用
react-ts
的模板。$ npm init vite@latest
-
第三步,删除不必要的文件
因为只用 Vite 的打包性能,用不上 Vite 的服务开发性能,所以要做一些清理。
删除
index.html
和 清空src
文件夹。 -
第四步,配置
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`,
},
},
};
});
-
第五步,配置
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" } }
- main,如果没有设置
创立组件
在 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 的代码。
所以在编写文档前还须要做一些筹备,反对 PropsTable
和 CodeShow
的用法,这里实现的细节就不做细说,感兴趣的能够查看 react-doc-starter。
组件文档编写筹备
-
第一步,md 文件反对间接应用 PropsTable 和 CodeShow 组件
新建以下几个文件,同时须要增加相应的依赖包,文件内容能够在这个我的项目 react-doc-starter 中获取。
website/loader/propsTable.js website/loader/codeShow.js website/plugins/mdx.js
-
第二步,反对 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, }, ], }, ], }, ], }, }; }, }; };
-
第三步,反对 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/"] }
-
第四步,配置
website/docusaurus.config.js
应用插件:const config = { ... plugins: [ './plugins/less', './plugins/alias', './plugins/mdx', ], ... }; module.exports = config;
-
第五步,批改默认的文档门路和默认的 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.tsx
和 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 是 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 build
和 lerna 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
命令。
-
第一步,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" } }
-
第二步,增加
package/components/.env
文件,确保构建环境未 productionNODE_ENV=production
-
第三步,创立
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, '/'), ); }
-
第四步,创立
package/components/vite.file.config.ts
文件:其中须要留神的是
rollupOptions.external
字段的配置,除了less
、css
、svg
的后缀名文件外,都不打包进输入文件。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
来解决。
-
第一步,更新 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" } }
-
第二步,更新 vite.config.js
配置
vitest
自身,须要在 Vite 配置中增加test
属性。如果你应用vite
的defineConfig
,还须要将 三斜线指令 写在配置文件的顶部。配置非常简略,只须要敞开 watch 和 增加 dom 执行环境。
/// <reference types="vitest" /> export default defineConfig(() => { return { ... test: { environment: 'happy-dom', watch: false, }, ... }; });
-
第三步,增加
.env.test
,确保环境为 test 环境。NODE_ENV=test
-
第四步,增加测试用例
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
插件。
-
第一步,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}, }, ], }, ], }, }; }, }; };
-
第二步,配置
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;
-
第三步,增加
docs/utils/isNumber.md
文件$utils
别名须要在./plugins/alias
和tsconfig.json
中增加对应的配置。sidebar_position
能够批改侧边栏同级菜单项的程序。--- sidebar_position: 1 --- <TsDoc src="$utils/isNumber" />
公布组件
同 components 分包,在我的项目 root 根目录下运行:
$ npm run build:publish
此名会运行lerna build
和 lerna publish
命令,而后装置提醒进行公布即可,具体的用法可查看 lerna 命令。
部署文档
同上的部署文档。
结语
通过反对 PropsTable
、CodeShow
和 TsDoc
三个便捷的组件,自己搭建的文档工具能够疾速编写并生成文档。
如果对你有所帮忙,能够点个赞,也能够去 Github 我的项目珍藏一下。