preface
Component library, a standardized set of components, is an indispensable tool for front-end engineers to develop and improve performance.
Excellent component libraries such as Antd Design and Element UI save us a lot of development time. So, is it easy to make a component library?
The answer is no, because when you do it, it’s a system. From development, compilation, testing, and finally release, each process requires a great deal of knowledge. But once you’ve actually built a component library, you’ll probably get more than you bargained for.
I hope this article can help you sort out a set of component library knowledge system, gather together to form a surface, if it can help you, please send a Star.
Sample component library online site: Frog-UI
Warehouse Address:Frog-Kits
An overview of
This paper mainly includes the following contents:
- Environment building:
Typescript
+ ESLint
+ StyleLint
+ Prettier
+ Husky
- Component development: Standardized component development catalogs and code structures
- Documentation site: Docz-based documentation presentation site
- Compile package: output matches
umd
/ esm
/ cjs
Three standard packaging products - Unit testing: Based on
jest
的 React
Component test solution and complete report - One-click publishing: integrate multiple commands, pipeline control NPM publish process
- Online deployment: Rapid deployment online documentation site based on now
If you have any mistakes, please feel free to communicate in the comments section
Initialize the
The whole directory
├── Heavy Metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // Constant. Js │ ├ ─ ─ the js │ └ ─ ─ a rollup. Config. Dist., js ├ ─ ─ the components / / component source │ ├ ─ ─ Alert │ ├ ─ ─ Button │ ├ ─ ─ index. The TSX │ └ ─ ─ style ├ ─ ─ coverage / / test report │ ├ ─ ─ clover. The XML │ ├ ─ ─ coverage - final. Json │ ├ ─ ─ the lcov - report │ └ ─ ─ the lcov. Info ├ ─ ─ dist // Component library packaging product: UMD │ ├ ─ ─ frog. CSS │ ├ ─ ─ frog. Js │ ├ ─ ─ frog. Js. Map │ ├ ─ ─ frog. Min. CSS │ ├ ─ ─ frog. Min. Js │ └ ─ ─ frog. Min. Js. Map ├ ─ ─ doc / / ├─ ├─ activities.txt │ ├─ activities.txt │ ├─ activities.txt │ ├─ activities.txt │ ├─ activities.txt │ ├─ activities.txt │ ├─ activities.txt │ ├─ activities.txt ESM │ ├ ─ ─ Alert │ ├ ─ ─ Button │ ├ ─ ─ index. The js │ └ ─ ─ style ├ ─ ─ gatsby - config. Js / / docz theme configuration ├ ─ ─ gulpfile. Js/configuration/gulp ├ ─ ─ Lib // Component library packaging product: ├─ ├─ ├─ ├─ ├─ ├─ download.txt // Download.txt/download.txt/download.txt Tsconfig. json // typescript configurationCopy the code
ESLint + StyleLint + Prettier is configured
Each Lint could write a separate article, but configuration is not our focus, so using @umijs/ Fabric, a collection of configuration files containing ESLint + StyleLint + Prettier, saves us a lot of time.
Interested students can go to see its source code, in the case of time to allow their own configuration from zero as learning is also good.
The installation
yarn add @umijs/fabric prettier @typescript-eslint/eslint-plugin -D
Copy the code
.eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
require.resolve('@umijs/fabric/dist/eslint'),
'prettier/@typescript-eslint',
'plugin:react/recommended'
],
rules: {
'react/prop-types': 'off',
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": ["error", { "allowShortCircuit": true }]
},
ignorePatterns: ['.eslintrc.js'],
settings: {
react: {
version: "detect"
}
}
}
Copy the code
Since the @umijs/ Fabric directory path for isTsProject is SRC based and cannot be modified, the component source is in the Components path, so add the relevant typescript configuration manually.
.prettierrc.js
const fabric = require('@umijs/fabric'); module.exports = { ... fabric.prettier, };Copy the code
.stylelintrc.js
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/stylelint')],
};
Copy the code
Configure Husky + Lint-staged
Husky provides a variety of hooks to intercept Git operations, such as git commit or Git push. However, we usually take on existing projects and it would be too expensive to fix if we did Lint checks for all code, so we want to be able to check only our own submitted code so that we can restrict everyone’s development specifications from now on and check existing code as it changes.
This introduces lint-staged code, which can be checked only against the current COMMIT and regular match files can be written.
The installation
yarn add husky lint-staged -D
Copy the code
package.json
"lint-staged": { "components/**/*.ts? (x)": [ "prettier --write", "eslint --fix" ], "components/**/**/*.less": [ "stylelint --syntax less --fix" ] }, "husky": { "hooks": { "pre-commit": "lint-staged" } }Copy the code
Configuration Typescript
typescript.json
{
"compilerOptions": {
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"paths": {
"components/*": ["src/components/*"]
}
},
"include": [
"components"
],
"exclude": [
"node_modules",
"build",
"dist",
"lib",
"es"
]
}
Copy the code
Component development
We are familiar with normal writing components, here we mainly look at the directory structure and part of the code:
├ ─ ─ Alert │ ├ ─ ─ __tests__ │ ├ ─ ─ index. The TSX │ └ ─ ─ style ├ ─ ─ Button │ ├ ─ ─ __tests__ │ ├ ─ ─ index. The TSX │ └ ─ ─ style ├ ─ ─ ├── ├─ less.├ ─ less.├ ─ less.├ ─ less.├ ─ less.├ ─Copy the code
Components /index.ts is the entry to the entire component library, responsible for collecting and exporting all components:
export { default as Button } from './Button';
export { default as Alert } from './Alert';
Copy the code
Components /style contains the base less file of the component library, including common styles such as core and color and variable Settings.
Each style directory contains at least two files: index. TSX and index.less:
style/index.tsx
import './index.less';
Copy the code
style/index.less
@import './core/index';
@import './color/default';
Copy the code
As you can see, style/index.tsx exists as a unique entry point for each component style reference.
__tests__ is the unit test directory for the component, which will be covered separately later. The specific Alert and Button components of the code are very simple, here is not redundant, you can go to the source code to find.
Component test
There has been a lot of discussion in the community about why tests should be written and whether they are necessary. You can make a decision based on your actual business scenario. My personal opinion is:
- Basic tools, be sure to do unit testing, for example
utils
,hooks
,components
- Business code, because the update iteration is fast, may not have time to write a single test, according to the pace of their own decisions
But the implications of single testing are definitely positive:
The more your tests resemble the way your software is used, the more confidence they can give you. – Kent C. Dodds
The installation
yarn add jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer @testing-library/react -D
yarn add @types/jest @types/react-test-renderer -D
Copy the code
package.json
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
}
Copy the code
Add __tests__/index.test. TSX as a single test entry file under each component.
import React from 'react'; import renderer from 'react-test-renderer'; import Alert from '.. /index'; describe('Component <Alert /> Test', () => { test('should render default', () => { const component = renderer.create(<Alert message="default" />); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); test('should render specific type', () => { const types: any[] = ['success', 'info', 'warning', 'error']; const component = renderer.create( <> {types.map((type) => ( <Alert key={type} type={type} message={type} /> ))} </>, ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); });Copy the code
A snapshot is a snapshot of the test result generated during the test case execution and stored in the __snapshots__/index.test.tsx.snap file. The next time we execute the test case, if we modify the source code of the component, the result snapshot will be compared with the last snapshot. If the result snapshot does not match, the test fails and we need to modify the test case to update the snapshot. This ensures that each modification of the source code must be compared with the results of the last test snapshot, in order to determine whether to pass, eliminating the need to write complex logic test code, is a simplified test means.
There is also a DOM based test based on @testing-library/react:
import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import renderer from 'react-test-renderer'; import Button from '.. /index'; describe('Component <Button /> Test', () => { let testButtonClicked = false; const onClick = () => { testButtonClicked = true; }; test('should render default', () => { // snapshot test const component = renderer.create(<Button onClick={onClick}>default</Button>); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); // dom test render(<Button onClick={onClick}>default</Button>); const btn = screen.getByText('default'); fireEvent.click(btn); expect(testButtonClicked).toEqual(true); }); });Copy the code
As you can see, @testing-library/ React provides methods, Render renders components into the DOM, Screen provides methods to retrieve DOM elements from the page, and fireEvent is responsible for triggering events that bind DOM elements.
The following article is recommended for more details on component testing:
- The Complete Beginner’s Guide to Testing React Apps: By simple
<Counter />
Test aboutToDoApp
The complete test and comparisonEnzyme
和@testing-library/react
The difference is a good introduction to the article - React Unit Testing strategy and Implementation: This section explains the significance of unit testing and implementation solutions
Component library packaging
Component library packaging is our main focus, we mainly achieve the following goals:
- Export umD/CJS/ESM specification files
- Export the component library CSS style file
- Support loading on demand
Here we expand around three fields in package.json: main, Module, and unpkg.
{
"main": "lib/index.js",
"module": "es/index.js",
"unpkg": "dist/frog.min.js"
}
Copy the code
When we look at the source code of each component library in the industry, we can always see these three fields, so what are their functions?
-
main
Is the package entry file that we passrequire
orimport
loadingnpm
When it comes to bags, it comes frommain
Field gets the file to load -
module
, is a field proposed by the packaging tool, not yet availablePackage. json official specificationIs responsible for specifying entry files that conform to the ESM specification. whenwebpack
orrollup
In the loadnpm
Pack when you see onemodule
Field will be loaded firstesm
Import files because it can be done bettertree-shaking
, reduce the code size. -
unpkg
, is also an unofficial field responsible for lettingnpm
Open the file in the packageCDN
Service, which means we can get throughunpkg.com/Directly obtain the file content. Here, for example, we can get throughunpkg.com/[email protected]…Direct accessumd
Version of the library file.
We used gulp to concatenate the workflow and export files in three formats using three commands:
"scripts": {
"build": "yarn build:dist && yarn build:lib && yarn build:es",
"build:dist": "rm -rf dist && gulp compileDistTask",
"build:lib": "rm -rf lib && gulp",
"build:es": "rm -rf es && cross-env ENV_ES=true gulp"
}
Copy the code
-
build
, aggregate command -
build:es
And the outputesm
Specification, the directory ises
build:lib
And the outputcjs
Specification, the directory islib
-
build:dist
And the outputumd
Specification, the directory isdist
Export the umd
To export umD files, run gulp compileDistTask. Gulpfile
gulpfile
function _transformLess(lessFile, config = {}) { const { cwd = process.cwd() } = config; const resolvedLessFile = path.resolve(cwd, lessFile); let data = readFileSync(resolvedLessFile, 'utf-8'); data = data.replace(/^\uFEFF/, ''); const lessOption = { paths: [path.dirname(resolvedLessFile)], filename: resolvedLessFile, plugins: [new NpmImportPlugin({ prefix: '~' })], javascriptEnabled: true, }; return less .render(data, lessOption) .then(result => postcss([autoprefixer]).process(result.css, { from: undefined })) .then(r => r.css); } async function _compileDistJS() { const inputOptions = rollupConfig; const outputOptions = rollupConfig.output; // frog.js const bundle = await rollup. Rollup (inputOptions); await bundle.generate(outputOptions); await bundle.write(outputOptions); / / packaging frog. Min. Js inputOptions. Plugins. Push (terser ()); outputOptions.file = `${DIST_DIR}/${DIST_NAME}.min.js`; const bundleUglify = await rollup.rollup(inputOptions); await bundleUglify.generate(outputOptions); await bundleUglify.write(outputOptions); } function _compileDistCSS() { return src('components/**/*.less') .pipe( through2.obj(function (file, encoding, Next) {if (/ compile/style/index. Less for. CSS file. The path. The match (/ / (\ | \ \) style (\ | \ \) index \. Less $/)) { _transformLess(file.path) .then(css => { file.contents = Buffer.from(css); file.path = file.path.replace(/\.less$/, '.css'); this.push(file); next(); }) .catch(e => { console.error(e); }); } else { next(); }}), ) .pipe(concat(`./${DIST_NAME}.css`)) .pipe(dest(DIST_DIR)) .pipe(uglifycss()) .pipe(rename(`./${DIST_NAME}.min.css`)) .pipe(dest(DIST_DIR)); } exports.compileDistTask = series(_compileDistJS, _compileDistCSS);Copy the code
rollup.config.dist.js
const resolve = require('@rollup/plugin-node-resolve');
const { babel } = require('@rollup/plugin-babel');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const commonjs = require('@rollup/plugin-commonjs');
const { terser } = require('rollup-plugin-terser');
const image = require('@rollup/plugin-image');
const { DIST_DIR, DIST_NAME } = require('./constant');
module.exports = {
input: 'components/index.tsx',
output: {
name: 'Frog',
file: `${DIST_DIR}/${DIST_NAME}.js`,
format: 'umd',
sourcemap: true,
globals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
},
plugins: [
peerDepsExternal(),
commonjs({
include: ['node_modules/**', '../../node_modules/**'],
namedExports: {
'react-is': ['isForwardRef', 'isValidElementType'],
}
}),
resolve({
extensions: ['.tsx', '.ts', '.js'],
jsnext: true,
main: true,
browser: true
}),
babel({
exclude: 'node_modules/**',
babelHelpers: 'bundled',
extensions: ['.js', '.jsx', 'ts', 'tsx']
}),
image()
]
}
Copy the code
Packaging tools like Rollup or WebPack are good at packing one or more chunks from one or more entry files, looking for dependencies in turn, while UMD is designed to output as a JS file.
So rollup is used to package umD files. The entry is Component /index.tsx, and the output format is UMD.
To package frog.js and frog.min.js at the same time, the teser plug-in is introduced in _compileDistJS and rollup packaging is performed twice.
A component library with only JS files is not enough. It also needs style files, such as when using Antd:
import { DatePicker } from 'antd';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
ReactDOM.render(<DatePicker />, mountNode);
Copy the code
Therefore, we also need to package up a component library CSS file.
_compileDistCSS traverses all less files in components, matches all index.less entry style files, compiles to CSS files with less, and aggregates them. The final output is frog.css and frog.min.css.
The final dist directory structure is as follows:
├ ─ ─ frog. CSS ├ ─ ─ frog. Js ├ ─ ─ frog. Js. Map ├ ─ ─ frog. Min. CSS ├ ─ ─ frog. Min. Js └ ─ ─ frog. Min. Js. The mapCopy the code
Export CJS and ESM
Exporting CJS or ESM, which means modularizing exports, is not an aggregate JS file, but each component is a Module, but CJS code is Commonjs standard, ESM code is ES Module standard.
So, we naturally thought of Babel, which compiles high-level code into various formats.
gulpfile
function _compileJS() {
return src(['components/**/*.{tsx, ts, js}', '!components/**/__tests__/*.{tsx, ts, js}'])
.pipe(
babel({
presets: [
[
'@babel/preset-env',
{
modules: ENV_ES === 'true' ? false : 'commonjs',
},
],
],
}),
)
.pipe(dest(ENV_ES === 'true' ? ES_DIR : LIB_DIR));
}
function _copyLess() {
return src('components/**/*.less').pipe(dest(ENV_ES === 'true' ? ES_DIR : LIB_DIR));
}
function _copyImage() {
return src('components/**/*.@(jpg|jpeg|png|svg)').pipe(
dest(ENV_ES === 'true' ? ES_DIR : LIB_DIR),
);
}
exports.default = series(_compileJS, _copyLess, _copyImage);
Copy the code
babel.config.js
module.exports = {
presets: [
"@babel/preset-react",
"@babel/preset-typescript",
"@babel/preset-env"
],
plugins: [
"@babel/plugin-proposal-class-properties"
]
};
Copy the code
Here the code is relatively simple, scan the TSX file in the Components directory, compile it using Babel and copy it to the es or lib directory. Less files are copied directly. _copyImage is used to prevent images from being copied directly, but it is not recommended to use images in the component library. Font ICONS can be used instead.
Component document
Docz is used here to build a document site. For more specific methods of use, you can read the official website document, which is not repeated here.
doc/Alert.mdx
-- name: Alert Alert route: / Alert menu: feedback -- import {Playground, Props} from 'docz' import {Alert} from '.. /components/'; import '.. /components/Alert/style'; # Alert Displays information that needs attention. <Alert message="Success Text" type=" Success "/> <Alert message="Info Text" type="info" /> <Alert message="Warning Text" type="warning" /> <Alert message="Error Text" type="error" /> </Playground>Copy the code
package.json
"scripts": {
"docz:dev": "docz dev",
"docz:build": "docz build",
"docz:serve": "docz build && docz serve"
}
Copy the code
Online documentation site deployment
Use now. Sh to deploy the online site, register, install command line, login successful.
yarn docz:build
cd .docz/dist
now deploy
vercel --production
Copy the code
A key release
We have many steps to go through when we release a new VERSION of the NPM package, and here is a set of scripts for one-click release.
The installation
yarn add conventional-changelog-cli -D
Copy the code
release.js
const child_process = require('child_process'); const fs = require('fs'); const path = require('path'); const inquirer = require('inquirer'); const chalk = require('chalk'); const util = require('util'); const semver = require('semver'); const exec = util.promisify(child_process.exec); const semverInc = semver.inc; const pkg = require('.. /package.json'); const currentVersion = pkg.version; const run = async command => { console.log(chalk.green(command)); await exec(command); }; Const logTime = (logInfo, type) => {const info = '=> ${type} : ${logInfo}'; console.log((chalk.blue(`[${new Date().toLocaleString()}] ${info}`))); }; const getNextVersions = () => ({ major: semverInc(currentVersion, 'major'), minor: semverInc(currentVersion, 'minor'), patch: semverInc(currentVersion, 'patch'), premajor: semverInc(currentVersion, 'premajor'), preminor: semverInc(currentVersion, 'preminor'), prepatch: semverInc(currentVersion, 'prepatch'), prerelease: semverInc(currentVersion, 'prerelease'), }); const promptNextVersion = async () => { const nextVersions = getNextVersions(); const { nextVersion } = await inquirer.prompt([ { type: 'list', name: 'nextVersion', message: `Please select the next version (current version is ${currentVersion})`, choices: Object.keys(nextVersions).map(name => ({ name: `${name} => ${nextVersions[name]}`, value: nextVersions[name] })) } ]); return nextVersion; }; const updatePkgVersion = async nextVersion => { pkg.version = nextVersion; logTime('Update package.json version', 'start'); await fs.writeFileSync(path.resolve(__dirname, '.. /package.json'), JSON.stringify(pkg)); await run('npx prettier package.json --write'); logTime('Update package.json version', 'end'); }; const test = async () => { logTime('Test', 'start'); await run(`yarn test:coverage`); logTime('Test', 'end'); }; const genChangelog = async () => { logTime('Generate CHANGELOG.md', 'start'); await run(' npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0'); logTime('Generate CHANGELOG.md', 'end'); }; const push = async nextVersion => { logTime('Push Git', 'start'); await run('git add .'); await run(`git commit -m "publish frog-ui@${nextVersion}" -n`); await run('git push'); logTime('Push Git', 'end'); }; const tag = async nextVersion => { logTime('Push Git', 'start'); await run(`git tag v${nextVersion}`); await run(`git push origin tag frog-ui@${nextVersion}`); logTime('Push Git Tag', 'end'); }; const build = async () => { logTime('Components Build', 'start'); await run(`yarn build`); logTime('Components Build', 'end'); }; const publish = async () => { logTime('Publish Npm', 'start'); await run('npm publish'); logTime('Publish Npm', 'end'); }; const main = async () => { try { const nextVersion = await promptNextVersion(); const startTime = Date.now(); await test(); await updatePkgVersion(nextVersion); await genChangelog(); await push(nextVersion); await build(); await publish(); await tag(nextVersion); console.log(chalk.green(`Publish Success, Cost ${((Date.now() - startTime) / 1000).toFixed(3)}s`)); } catch (err) { console.log(chalk.red(`Publish Fail: ${err}`)); } } main();Copy the code
package.json
"scripts": {
"publish": "node build/release.js"
}
Copy the code
The code is also relatively simple, which is the basic use of some tools. By executing YARN publish, you can publish version in one click.
At the end
This article is my learning summary in the process of building component library, in the process of learning a lot of knowledge, and set up a clear knowledge system, I hope to be helpful to you, welcome to exchange in the comments section ~
Reference documentation
Tree-Shaking Performance Optimization Practices – Principles
Know ESLint and Prettier inside out
Integration configuration @umijs/ Fabric
TypeScript and React: Components
TypeScript ESLint
Caused by allowSyntheticDefaultImports thinking
Tsconfig. json Guide to getting started
React Unit test policy and implementation
The Complete Beginner’s Guide to Testing React Apps