2021.07.03-19:49 update:
- Due to docZ problems, some students could not start the project, so we made a migration and used Dumi as a document site tool, which is more friendly to beginners
- Add the process for deploying document sites to Github Pages and configure Github Actions to automatically trigger build deployment
- As it is difficult to maintain multiple platforms, please visit github blog for detailed updates on the above two points. Address: github.com/worldzhao/b…
2020.05.21-10:30 update:
- Docz has a Bug on Windows platform, which has not been fixed yet. It is recommended to run this project using WSL + VSCode.
2020.05.18 – ye that update:
- Part of the code can be expanded – optimize the reading experience;
- Remove some colloquial expressions.
2020.05.16-18:48 update:
- Add headers;
- Added an online preview address.
Since it’s difficult to maintain multiple platforms, check out the Github blog. Address:Github.com/worldzhao/b…
Since it’s difficult to maintain multiple platforms, check out the Github blog. Address:Github.com/worldzhao/b…
Since it’s difficult to maintain multiple platforms, check out the Github blog. Address:Github.com/worldzhao/b…
Some of the problems encountered by students have been solved in the latest article (mainly on docZ and site deployment). I do not guarantee that the gold digging article is up to date, but please send it to my blog Star.
🚀 Online Preview
🚆 Local Preview
git clone [email protected]:worldzhao/react-ui-library-tutorial.git
cd react-ui-library-tutorial
yarn
yarn start
Copy the code
After executing the commands in sequence, you can see the following in localhost:3000:
An overview of
This series of articles covers the following topics:
- Project initialization: Component library development preparation.
eslint
/commit lint
/typescript
And so on; - Development stage: use DumI for development, debugging and documentation;
- Packaging phase: Output ~~
umd
~ ~ /cjs
/esm
Product and support on demand loading; - Component testing: use
@testing-library/react
And its related ecology component testing; - Publish NPM: Write scripts to complete publishing or use NP publishing directly;
- Deploy the document site: Use Github Pages and Github Actions to automatically deploy the document site.
If this article has helped you, please send one to the warehouse ✨✨.
If there is any error, please correct the exchange in the comment area, thank you.
The warehouse address
The preparatory work
Initialize the project
Create a happy-UI folder and initialize it.
mkdir happy-ui
cd happy-ui
npm init --y
mkdir components && cd components && touch index.ts Create a new source folder and import file
Copy the code
Code specification
The @umijs/ Fabric configuration is used directly here.
yarn add @umijs/fabric --dev
yarn add prettier --dev # Because @umijs/ Fabric does not rely on Prettier, we need to install prettier manually
Copy the code
.eslintrc.js
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')]};Copy the code
.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
Students who want to configure their own can refer to the following article:
- Linting Your React+Typescript Project with ESLint and Prettier!
- Use ESLint+Prettier specifications for React+Typescript projects
Commit Lint
Perform pre-commit code specification checks.
yarn add husky lint-staged --dev
Copy the code
package.json
"lint-staged": {
"components/**/*.ts? (x)": [
"prettier --write"."eslint --fix"."git add"]."components/**/*.less": [
"stylelint --syntax less --fix"."git add"]},"husky": {
"hooks": {
"pre-commit": "lint-staged"}}Copy the code
Commit Message detection is performed.
yarn add @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog --dev
Copy the code
Add. Commitlintrc.js and write the following
module.exports = { extends: ['@commitlint/config-conventional']};Copy the code
Package. json writes the following:
// ...
"scripts": {
"commit": "git-cz",}// ...
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"."pre-commit": "lint-staged"}},"config": {
"commitizen": {
"path": "cz-conventional-changelog"}}Copy the code
Next, use YARN Commit instead of Git commit to generate the commit Message of the specification. Of course, you can choose to write it by hand for efficiency, but it should conform to the specification.
TypeScript
yarn add typescript --dev
Copy the code
Create tsconfig.json and write the following
{
"compilerOptions": {
"baseUrl": ". /"."target": "esnext"."module": "commonjs"."jsx": "react"."declaration": true."declarationDir": "lib"."strict": true."moduleResolution": "node"."allowSyntheticDefaultImports": true."esModuleInterop": true."resolveJsonModule": true
},
"include": ["components"."global.d.ts"]."exclude": ["node_modules"]}Copy the code
test
Create an alert folder under components folder with the following directory structure:
Alert ├ ─ ─ alert. The TSX # source file ├ ─ ─ but ts # entry documents ├ ─ ─ interface. The ts # type declaration documents └ ─ ─ style ├ ─ ─ but less # style file └ ─ ─ index. The ts # Why is there an index.ts - Load on Demand style management style dependency in a later sectionCopy the code
Install React dependencies:
yarn add react react-dom @types/react @types/react-dom --dev The host environment must exist when developing dependencies
yarn add prop-types Runtime dependencies, the host environment may not exist when installing this component library together
Copy the code
The prop-types library is still installed here, because there is no guarantee that the host environment will also use typescript for static checking, so we use prop-types to ensure that javascript users get run-time error messages as well.
components/alert/interface.ts
export type Kind = 'info' | 'positive' | 'negative' | 'warning';
export type KindMap = Record<Kind, string>;
export interface AlertProps {
/**
* Set this to change alert kind
* @default info* /kind? :'info' | 'positive' | 'negative' | 'warning';
}
Copy the code
components/alert/alter.tsx
import React from 'react';
import t from 'prop-types';
import { AlertProps, KindMap } from './interface';
const prefixCls = 'happy-alert';
const kinds: KindMap = {
info: '#5352ED'.positive: '#2ED573'.negative: '#FF4757'.warning: '#FFA502'};const Alert: React.FC<AlertProps> = ({ children, kind = 'info'. rest }) = > (
<div
className={prefixCls}
style={{
background: kinds[kind],}} {. rest}
>
{children}
</div>
);
Alert.propTypes = {
kind: t.oneOf(['info'.'positive'.'negative'.'warning']),};export default Alert;
Copy the code
components/alert/index.ts
import Alert from './alert';
export default Alert;
export * from './interface';
Copy the code
components/alert/style/index.less
@popupPrefix: happy-alert;
.@{popupPrefix} {
padding: 20px;
background: white;
border-radius: 3px;
color: white;
}
Copy the code
components/alert/style/index.ts
import './index.less';
Copy the code
components/index.ts
export { default as Alert } from './alert';
Copy the code
The component here references the DocZ project typescript and the less example.
Git a shuttle, you can see that the console has done hook detection.
git add .
yarn commit # git commit -m'feat: chapter 1 '
git push
Copy the code
The preparations are complete. The code can be obtained from the chapter 1 branch of the repository. If there is a discrepancy between the master branch and the article, the master branch and the article will prevail.
Development and debugging
This section solves the preview and debugging problems when developing components, along the way to solve the documentation.
Docz is selected here to assist preview debugging.
Docz is based on MDX (Markdown + JSX). The React component can be introduced in Markdown, making it possible to preview and debug documents at the same time. And thanks to the React ecosystem, we can write documents like apps, not just boring words. Docz also has some built-in components, such as
.
Install docz and custom configuration
yarn add docz --dev
yarn add rimraf --dev Empty a secondary library of the directory
Copy the code
Add NPM scripts to package.json.
"scripts": {
"dev": "docz dev".// Start the local development environment
"start": "npm run dev".// dev Alias of the command
"build:doc": "rimraf doc-site && docz build".// The directory of the packaged file will be named doc-site later, so it will be deleted before each build
"preview:doc": "docz serve" // Preview the document site
},
Copy the code
Note: All operations in this section are for site applications. Packaging refers to documentation site packaging, not component libraries.
Create a new doczrc.js configuration file and write the following:
doczrc.js
export default {
files: './components/**/*.{md,markdown,mdx}'.// Identify the file suffix
dest: 'doc-site'.// The name of the packaged file directory
title: 'happy-ui'.// Site title
typescript: true.// Component source files are developed in typescript and need to be turned on
};
Copy the code
Because you use less as a style preprocessor, you need to install the Less plug-in.
yarn add less gatsby-plugin-less --dev
Copy the code
Create a new gatsby-config.js and write the following:
gatsby-config.js
module.exports = {
plugins: ['gatsby-theme-docz'.'gatsby-plugin-less']};Copy the code
Written document
New components/alert/index. MDX, and write the following content:
- name: Alert Alert route: /AlertMenu: - componentimport { Playground } from 'docz'; import Alert from './alert'; // import component './style'; // Introduce component styles# Alert Indicates a warningWarning indicates the information that needs attention.## Code demo
### Basic usage
<Playground>
<Alert kind="warning">This is a warning</Alert>
</Playground>
## API| | attributes that default value type | | | | -- - | -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- - | | kind type | | warning 'info'/' positive '/' negative '/' warning 'not required | |' info 'Copy the code
Execute the script command:
yarn start # or yarn dev
Copy the code
You can see the following page in localhost:3000:
Now you can have fun documenting and debugging in index.mdx!
Optimize documentation
A large number of demos in the code demo section (basic usage, advanced usage, various usage, and so on) can lead to long and difficult to maintain document source files in complex component situations. Then pull away.
Create a new demo folder under components/alert/ to store the demo we need to reference when writing the document.
components/alert/demo/1-demo-basic.tsx
import React from 'react';
import Alert from '.. /alert';
import '.. /style';
export default() = ><Alert kind="warning"></Alert>;
Copy the code
components/alert/index.mdx
- import Alert from './alert'; // Import components
- import './style'; // Introduce component styles
+ import BasicDemo from './demo/1-demo-basic';. <Playground>-
This is a warning
+ <BasicDemo />
</Playground>
Copy the code
So we’ve separated the demo from the documentation. Preview as follows:
Wait, the code area shows
The
- Allow to override the playground’s editor’s code #906
In fact, the first PR has solved the problem, but was closed, helpless.
Now that the React component can be introduced, it is not difficult to customize the Playground component in MDX environment. It is just a rendering component (MDX comes with it) and display source code.
Optimize code presentation
write<HappyBox />
component
Install dependencies:
yarn add react-use react-tooltip react-feather react-simple-code-editor prismjs react-copy-to-clipboard raw-loader styled-components --dev
Copy the code
- react-use- 2020, of course
hooks
- React-simple -code-editor – Code display area
- Prismjs – Code highlighting
- Raw-loader – Converts source code to a string
- React-copy-to-clipboard – allows dads to copy demo code
- React-tooltip/React-Feather auxiliary components
- Styled – Components facilitate styling for the user in document examples and are also used for styling for document components
These dependencies serve the documentation site application and have nothing to do with the component library itself.
The final effect is as follows:
Create a new doc-comps folder under the root directory to store some of the tool components used in the document, such as
doc-comps
├ ─ ─ happy - box │ ├ ─ ─ style.css. Ts │ └ ─ ─ index. The TSX └ ─ ─ index. The tsCopy the code
components/doc-comps/happy-box/index.tsx
Expand to view code
import React from 'react';
import Editor from 'react-simple-code-editor';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useToggle } from 'react-use';
import ReactTooltip from 'react-tooltip';
import IconCopy from 'react-feather/dist/icons/clipboard';
import IconCode from 'react-feather/dist/icons/code';
import { highlight, languages } from 'prismjs/components/prism-core';
import { StyledContainer, StyledIconWrapper } from './style';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-markup';
require('prismjs/components/prism-jsx');
interface Props {
code: string; title? : React.ReactNode; desc? : React.ReactNode; }export const HappyBox: React.FC<Props> = ({ code, title, desc, children }) = > {
const [isEditVisible, toggleEditVisible] = useToggle(false);
return (
<StyledContainer>
<section className="code-box-demo"> {children}</section>
<section className="code-box-meta">
<div className="text-divider">
<span>{title | | 'sample'}</span>
</div>
<div className="code-box-description">
<p>{desc | | 'no description'}</p>
</div>
<div className="divider" />
<div className="code-box-action">
<CopyToClipboard text={code} onCopy={()= >Alert (' copy succeeded ')}><IconCopy data-place="top" data-tip="Copy code" />
</CopyToClipboard>
<StyledIconWrapper onClick={toggleEditVisible}>
<IconCode data-place="top" data-tip={isEditVisible? 'Fold up your code':'Display code '} />
</StyledIconWrapper>
</div>
</section>
{renderEditor()}
<ReactTooltip />
</StyledContainer>
);
function renderEditor() {
if(! isEditVisible)return null;
return (
<div className="container_editor_area">
<Editor
readOnly
value={code}
onValueChange={()= > {}}
highlight={code => highlight(code, languages.jsx)}
padding={10}
className="container__editor"
style={{
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: 14,
}}
/>
</div>); }};export default HappyBox;
Copy the code
components/doc-comps/happy-box/style.ts
Expand to view code
import styled from 'styled-components';
export const StyledIconWrapper = styled.div` display: flex; align-items: center; margin-left: 10px; `;
export const StyledContainer = styled.div` position: relative; display: inline-block; width: 100%; margin: 0 0 16px; border: 1px solid #ebedf0; border-radius: 2px; The transition: all 0.2 s; .text-divider { display: table; &::before, &::after { content: ''; position: relative; display: table-cell; transform: translateY(50%); content: ''; border-top: 1px solid #e8e8e8; } &::before { top: 50%; width: 5%; } &::after { width: 95%; top: 50%; width: 95%; } & > span { display: inline-block; padding: 0 10px; font-weight: 500; font-size: 16px; white-space: nowrap; text-align: center; font-variant: tabular-nums; The line - height: 1.5; } } .divider { margin: 0; background: none; border: dashed #e8e8e8; border-width: 1px 0 0; display: block; clear: both; width: 100%; min-width: 100%; height: 1px; position: relative; Top: 0.06 em. box-sizing: border-box; padding: 0; font-size: 14px; font-variant: tabular-nums; The line - height: 1.5; list-style: none; font-feature-settings: 'tnum'; }. Code-box-demo {transition: all 0.2s; padding: 42px 24px 50px; } .code-box-meta { font-size: 14px; line-height: 2; } .code-box .ant-divider { margin: 0; } .code-box-description { padding: 18px 24px 12px; } .code-box-action { height: 40px; display: flex; align-items: center; justify-content: center; font-size: 16px; } .code-box-action .anticon { margin: 0 8px; cursor: pointer; } .container_editor_area { border-top: 1px solid rgb(232, 232, 232); padding: 16px; } .container__editor { font-variant-ligatures: common-ligatures; border-radius: 3px; } .container__editor textarea { outline: 0; background-color: none; } .button { display: inline-block; padding: 0 6px; text-decoration: none; background: #000; color: #fff; } .button:hover { background: linear-gradient(45deg, #e42b66, #e2433f); } /* Syntax highlighting */ .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #90a4ae; } .token.punctuation { color: #9e9e9e; }.namespace {opacity: 0.7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #e91e63; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #4caf50; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #795548; } .token.atrule, .token.attr-value, .token.keyword { color: #3f51b5; } .token.function { color: #f44336; } .token.regex, .token.important, .token.variable { color: #ff9800; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } `;
Copy the code
The related configuration
- increase
alias
Alias, sample source code shows the relative path is not friendly, let the user directly copy just enough worry
Create a new gatsby-node.js and say the following to turn on alias:
const path = require('path');
exports.onCreateWebpackConfig = args= > {
args.actions.setWebpackConfig({
resolve: {
modules: [path.resolve(__dirname, '.. /src'), 'node_modules'].alias: {
'happy-ui/lib': path.resolve(__dirname, '.. /components/'),
'happy-ui/esm': path.resolve(__dirname, '.. /components/'),
'happy-ui': path.resolve(__dirname, '.. /components/'),}}}); };Copy the code
Tsconfig. json package should ignore demo to avoid inclusion of types generated by component library package, and add paths attribute to vscode automatically:
tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
+ "paths": {
+ "happy-ui": ["components/index.ts"],
+ "happy-ui/esm/*": ["components/*"],
+ "happy-ui/lib/*": ["components/*"]
+},
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"declaration": true,
"declarationDir": "lib",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["components", "global.d.ts"],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "**/demo/**"]
}
Copy the code
New problem appeared, vscode alias suggests dependence tsconfig. Json, ignore the demo folder after the demo file within the module type can’t find the statement (failure) paths, so can’t will demo in tsconfig. Json removed:
{
- "exclude": ["node_modules", "**/demo/**"]
+ "exclude": ["node_modules"]
}
Copy the code
Create a new tsconfig.build.json file:
tsconfig.build.json
{
"extends": "./tsconfig.json"."exclude": ["**/demo/**"."node_modules"]}Copy the code
Use the TSC generated type declaration file to specify tsconfig.build.json.
Modification related documents
components/alert/demo/1-demo-basic.tsx
- import Alert from '.. /alert';
+ import Alert from 'happy-ui/lib/alert';
- import '.. /style';
+ import 'happy-ui/lib/alert/style';
Copy the code
components/alert/index.mdx
- import { Playground } from 'docz';
+ import { HappyBox } from '.. /.. /doc-comps';
+ import BasicDemoCode from '! raw-loader! ./demo/1-demo-basic.tsx';.- <Playground>
-
- </Playground>
+
+
+ </HappyBox>
Copy the code
If yarn start is stuck, delete the root directory. Docz folder and run the command again.
Now you can have fun developing components. The code can be obtained from the chapter 2 branch of the repository. If there is a discrepancy between the master branch and the article, the master branch and the article will prevail.
Component library packaging
The host environment varies, and the source code needs to be processed and distributed to NPM.
Identify the following goals:
- Export type declaration file;
- export
umd
/Commonjs module
/ES module
Three forms for users to introduce; - Supporting style files
css
Introduce, not just haveless
To reduce the access cost of the business side; - Support loading on demand.
Export the type declaration file
Since this is a component library written in typescript, users should enjoy the benefits of the type system.
We can generate a type declaration file and define entries in package.json as follows:
package.json
{
"typings": "lib/index.d.ts".// Define the type entry file
"scripts": {
"build:types": "tsc -p tsconfig.build.json && cpr lib esm" // Run the TSC command to generate a type declaration file}}Copy the code
Note that a copy of the lib declaration file is made here using CPR (manual installation required) and the folder is renamed ESM to store the ES Module component behind it. The reason for this is to ensure that users can still get automatic prompts when manually importing components on demand.
The initial approach is to store the declaration file in a separate types folder, but then only ‘happy-ui’ is introduced to get the prompt, not ‘happy-ui/esm/ XXX ‘and ‘happy-ui/lib/ XXX’.
tsconfig.build.json
{
"extends": "./tsconfig.json"."compilerOptions": { "emitDeclarationOnly": true }, // Only declaration files are generated
"exclude": ["**/__tests__/**"."**/demo/**"."node_modules"."lib"."esm"] // Exclude samples, tests, and packaged folders
}
Copy the code
The root directory generates the lib folder (declarationDir field defined in tsconfig.json) and esM folder (copied). The directory structure is the same as that of the Components folder, as follows:
lib
├ ─ ─ alert │ ├ ─ ─ alert, which s │ ├ ─ ─ the index, which s │ ├ ─ ─ interface, which s │ └ ─ ─ style │ └ ─ ─ the index, which s └ ─ ─ the index, which sCopy the code
This allows consumers to be automatically prompted when importing an NPM package and to reuse the type definition of the associated component.
Next, files such as TS (x) are processed into JS files.
Note that we need to output both Commonjs Module and ES Module files (not considering UMD). CJS refers to Commonjs Module and ESm refers to ES Module. Exports: import, require, export, module.exports
Export the Commonjs module
It is perfectly possible to compile the code using Babel or TSC command-line tools (in fact, many libraries do), but given the styling and loading on demand, we use gulp to string the process together.
Babel configuration
Start by installing Babel and its associated dependencies
yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-transform-runtime --dev
Copy the code
yarn add @babel/runtime-corejs3
Copy the code
Create a new.babelrc.js file and write the following:
.babelrc.js
module.exports = {
presets: ['@babel/env'.'@babel/typescript'.'@babel/react'].plugins: [
'@babel/proposal-class-properties'['@babel/plugin-transform-runtime',
{
corejs: 3.helpers: true,},],],};Copy the code
About @babel/plugin-transform-runtime and @babel/runtime-corejs3
- if
helpers
Option set totrue
, can be extracted from the code compilation process repeatedly generatedhelper
Function (classCallCheck
.extends
Etc.), reduce the generated code volume; - if
corejs
Set to3
, can be introduced not to pollute the global on demandpolyfill
, commonly used in class library writing (I prefer: not introducedpolyfill
In turn, tell the user what needs to be introducedpolyfill
To avoid repeated introductions or conflicts, more on that later).
See the official documentation -@babel/ plugin-transform-Runtime for more information
Configuring the target Environment
To avoid translating native browser supported syntax, create a new. Browserslistrc file and write the supported browser scope to @babel/preset-env based on adaptation requirements.
.browserslistrc
>0.2%
not dead
not op_mini all
Copy the code
Unfortunately, @babel/ Runtime-corejs3 is not able to reduce the introduction of polyfill again on an on-demand basis depending on target browser support, see @babel/ Runtime for Target Environment.
This means that @babel/ Runtime-corejs3 would inject all possible polyfills even for modern engines: unnecessarily increasing the size of the final bundle.
For component libraries (which can be large in code), I recommend giving the user the option to polyfill in the host environment. If the user is compatible, it’s natural to use @babel/preset-env + core-js +.browserslistrc for global polyfills. This one-two combo introduces all polyfills that the lowest target browser doesn’t support the API.
For example, if @babel/preset-env useBuiltIns is set to usage and node_modules is excluded from babel-loader, Babel does not detect polyfills needed in nodes_modules. useBuiltIns: Usage “for node_modules without transpiling #9419, please set useBuiltIns to Entry before the content mentioned in this issue is not supported, Or do not exclude node_modules from babel-loader.
So the component library is more important than anything else (like Zent and ANTD) by introducing extra polyfills and documenting them well.
Now @babel/runtime-corejs3 is replaced with @babel/runtime, and only the helper functions are removed.
yarn remove @babel/runtime-corejs3
yarn add @babel/runtime
Copy the code
.babelrc.js
module.exports = {
presets: ['@babel/env'.'@babel/typescript'.'@babel/react'].plugins: ['@babel/plugin-transform-runtime'.'@babel/proposal-class-properties']};Copy the code
The helper option for @babel/ transform-Runtime defaults to true.
Gulp configuration
Install gulp-related dependencies
yarn add gulp gulp-babel --dev
Copy the code
Create gulpfile.js and write the following:
gulpfile.js
const gulp = require('gulp');
const babel = require('gulp-babel');
const paths = {
dest: {
lib: 'lib'.// Commonjs file directory name - this block care
esm: 'esm'.// The name of the directory where the ES module file is stored
dist: 'dist'.// The name of the directory where the umd file is stored
},
styles: 'components/**/*.less'.// Style file path - do not care for now
scripts: ['components/**/*.{ts,tsx}'.'! components/**/demo/*.{ts,tsx}'].// Script file path
};
function compileCJS() {
const { dest, scripts } = paths;
return gulp
.src(scripts)
.pipe(babel()) // Use gulp-babel
.pipe(gulp.dest(dest.lib));
}
// Parallel tasks can be processed in parallel after the addition of style processing
const build = gulp.parallel(compileCJS);
exports.build = build;
exports.default = build;
Copy the code
Modify the package. The json
package.json
{
- "main": "index.js",
+ "main": "lib/index.js",
"scripts": {
...
+ "clean": "rimraf lib esm dist",
+ "build": "npm run clean && npm run build:types && gulp",. }},Copy the code
Run yarn build to obtain the following information:
lib
├ ─ ─ alert │ ├ ─ ─ alert. Js │ ├ ─ ─ index. The js │ ├ ─ ─ interface. The js │ └ ─ ─ style │ └ ─ ─ index. The js └ ─ ─ index, jsCopy the code
If you look at the compiled source code, you can see that many helper methods have been removed from @babel/ Runtime, and module import/export form is also the CommonJS specification.
lib/alert/alert.js
Export the ES module
Building ES Modules makes tree shaking better. Based on the Babel configuration from the previous step, update the following:
- configuration
@babel/preset-env
themodules
Options forfalse
, close module conversion; - configuration
@babel/plugin-transform-runtime
theuseESModules
Options fortrue
, the use ofES module
In the form of introductionhelper
Function.
.babelrc.js
module.exports = {
presets: [['@babel/env',
{
modules: false.// Turn off module conversion},].'@babel/typescript'.'@babel/react',].plugins: [
'@babel/proposal-class-properties'['@babel/plugin-transform-runtime',
{
useESModules: true.// 使用esm形式的helper},]],};Copy the code
When the goal is reached, we use environment variables to distinguish ESM and CJS (just set the corresponding environment variables when executing the task), and the final Babel configuration is as follows:
.babelrc.js
module.exports = {
presets: ['@babel/env'.'@babel/typescript'.'@babel/react'].plugins: ['@babel/plugin-transform-runtime'.'@babel/proposal-class-properties'].env: {
esm: {
presets: [['@babel/env',
{
modules: false,}]].plugins: [['@babel/plugin-transform-runtime',
{
useESModules: true,},],],},},};Copy the code
Next, modify gulp configuration to remove compileScripts task and add compileESM task.
gulpfile.js
// ...
/** * Compile the script file *@param {string} BabelEnv Babel environment variable *@param {string} DestDir Target directory */
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
// Set environment variables
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(babel()) // Use gulp-babel
.pipe(gulp.dest(destDir));
}
/** * Compiles CJS */
function compileCJS() {
const { dest } = paths;
return compileScripts('cjs', dest.lib);
}
/** * Compile esM */
function compileESM() {
const { dest } = paths;
return compileScripts('esm', dest.esm);
}
// Execute compile script tasks (CJS, ESM) serially to avoid environment variables
const buildScripts = gulp.series(compileCJS, compileESM);
// Execute tasks in parallel
const build = gulp.parallel(buildScripts);
// ...
Copy the code
After yarn Build is executed, two folders lib and esm are generated. The ESM directories have the same structure as lib. Js files are imported and exported as ES Module modules.
esm/alert/alert.js
Don’t forget to add relevant entries to package.json.
package.json
{
+ "module": "esm/index.js"
}
Copy the code
Working with style files
Copying less files
We will less file contains the NPM package, the user can through the happy – UI/lib/alert/style/index. In the form of js on-demand introduced less file, here is a less directly to copy files to the target folder.
Create a new copyLess task in gulpfile.js.
gulpfile.js
// ...
/** * Copy less file */
function copyLess() {
return gulp
.src(paths.styles)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
const build = gulp.parallel(buildScripts, copyLess);
// ...
Copy the code
If you look at the lib directory, you can see that the less file has been copied to the alert/style directory.
lib
├ ─ ─ alert │ ├ ─ ─ alert. Js │ ├ ─ ─ index. The js │ ├ ─ ─ interface. The js │ └ ─ ─ style │ ├ ─ ─ index. The js │ └ ─ ─ but less # less file └ ─ ─ index.jsCopy the code
Some of you may have noticed the problem: if the user is using sASS or even native CSS without a less preprocessor, the existing solution won’t work. After analysis, there are the following 4 pre-selection schemes:
- Inform the business side of the increase
less-loader
. It will lead to an increase in the use cost of the business side; - Wrap up a whole serving
css
File, proceedFull amountIntroduction. Unable to import on demand; css in js
Plan;- To provide a
style/css.js
File to import componentscss
Style dependent, notless
Dependency, component library layer to smooth out differences.
Focus on plan 3 and Plan 4.
In addition to making style writing more possible, CSS in JS is a great tool for writing third-party component libraries.
If we write a react-use hooks library, no styling, just set sideEffects to false in package.json, when the business side uses WebPack, Only hooks that are used are packaged (ES Module preferred).
Other hooks that are exported from the entry file index.js but not used are shaking from Tree. When I first started using this library I wondered why there was no import on demand way to use it.
Perhaps the commonly used ANTD and Lodash need to be matched, resulting in habitual thinking.
Back to business. If the style is written in javascript, the component library and the tool library are in some dimension the same, with sideEffects, automatically introduced on demand, nice.
And each component is bound to its own style, without the need for the business side or the component developer to maintain style dependencies, which are described later.
Disadvantages:
- Styles cannot be cached separately;
- Styled -components has a large size;
- The override component style requires using the property selector or using
styled-components
Built-in methods.
For the styled components, it is also extremely convenient to do theme customization.
Scheme 4 is the scheme used by ANTD.
In the process of building the component library, a question puzzled me for a long time: why do I need alert/style/index.js to introduce less files or alert/style/css.js to introduce CSS files?
The answer is managing style dependencies.
Because our component does not import style files, the user needs to import them manually.
Assume the following scenario: , depends on
Why can’t components import ‘./index.less’ by themselves?
Yes, but the business side needs to configure less-loader. What, the business side doesn’t want to configure less-loader. Import ‘./index.css’? 🙃
Yes, business is good, component developers are not happy.
So we need to find a solution that makes everyone happy:
- Component developers can happily use the preprocessor;
- There is no additional usage cost for the business side.
The answer is that CSS in JS provides a separate style/css.js file, which introduces component CSS style file dependencies instead of less dependencies, and the bottom layer of the component library wipes out differences.
Father can change index.less to index. CSS when packaging. This is a good way to do it, but some style modules that are introduced repeatedly (e.g. animation styles) will be packaged repeatedly.
Generating CSS Files
Install dependencies.
yarn add gulp-less gulp-autoprefixer gulp-cssnano --dev
Copy the code
Add the less2CSS task to gulpfile.js.
// ...
/** * Generate CSS file */
function less2css() {
return gulp
.src(paths.styles)
.pipe(less()) // Process less files
.pipe(autoprefixer()) // Add the prefix according to browserslistrc
.pipe(cssnano({ zindex: false.reduceIdents: false })) / / compression
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
const build = gulp.parallel(buildScripts, copyLess, less2css);
// ...
Copy the code
Run yarn Build. The CSS file already exists in the component style directory.
Next we need an alert/style/css.js to help the user import the CSS file.
To generate CSS. Js
Here’s how antD-Tools works: In the scripts task, intercept style/index.js, generate style/css.js, and use the re to change the introduced less file suffix to CSS.
Install dependencies.
yarn add through2 --dev
Copy the code
gulpfile.js
// ...
/** * Compile the script file *@param {*} BabelEnv Babel environment variable *@param {*} DestDir Target directory */
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(babel()) // Use gulp-babel
.pipe(
through2.obj(function z(file, encoding, next) {
this.push(file.clone());
// Find the target
if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
const content = file.contents.toString(encoding);
file.contents = Buffer.from(cssInjection(content)); // File content processing
file.path = file.path.replace(/index\.js/.'css.js'); // Rename the file
this.push(file); // Add the file
next();
} else {
next();
}
}),
)
.pipe(gulp.dest(destDir));
}
// ...
Copy the code
CssInjection implementation:
gulpfile.js
/** * Current component styles import './index.less' => import './index.css' * Dependent other component styles import '.. /test-comp/style' => import '.. /test-comp/style/css.js' * dependent other component styles import '.. /test-comp/style/index.js' => import '.. /test-comp/style/css.js' *@param {string} content* /
function cssInjection(content) {
return content
.replace(/\/style\/? '/g."/style/css'")
.replace(/\/style\/?" /g.'/style/css"')
.replace(/\.less/g.'.css');
}
Copy the code
After the package is packaged, you can see that the component style directory is generated into css.js file, which is also the CSS file converted from the previous step less.
lib/alert
├ ─ ─ alert. Js ├ ─ ─ index. The js ├ ─ ─ interface. The js └ ─ ─ style ├ ─ ─ CSS, js # introduced index. The CSS ├ ─ ─ index. The CSS ├ ─ ─ index. The js └ ─ ─ index. The lessCopy the code
According to the need to load
Add sideEffects properties to package.json to work with ES Module for tree shaking effect (mark style dependent files as Side Effects to avoid removing them by mistake).
// ...
"sideEffects": [
"dist/*"."esm/**/style/*"."lib/**/style/*"."*.less"].// ...
Copy the code
It is possible to load the JS part on demand using the following method, but the style needs to be imported manually:
import { Alert } from 'happy-ui';
import 'happy-ui/esm/alert/style';
Copy the code
It can also be introduced in the following ways:
import Alert from 'happy-ui/esm/alert'; // or import Alert from 'happy-ui/lib/alert';
import 'happy-ui/esm/alert/style'; // or import Alert from 'happy-ui/lib/alert';
Copy the code
The above way of introducing style files is not very elegant, and importing full style files directly is a far cry from the intent of loading on demand.
Users can use babel-plugin-import to help reduce the amount of code to be written.
import { Alert } from 'happy-ui';
Copy the code
⬇ ️
import Alert from 'happy-ui/lib/alert';
import 'happy-ui/lib/alert/style';
Copy the code
Generate the umd
I’m not going to use it. Let’s call this one TOdo.
This section of code can be obtained from the chapter 3 branch of the repository. If there is any discrepancy between the master branch and the article, the master branch and the article shall prevail.
Component test
The closer a test is to the software’s operational behavior, the more confidence it will give you.
This section focuses on introducing jest and @testing-library/ React into component libraries, rather than getting into unit testing.
If you are interested in the following questions:
- What- What is a unit test?
- Why- Why write unit tests?
- How- Best practices for writing unit tests?
Check out the following article:
- Test React apps with React Testing Library: Through a
<Counter />
Examples are extended to illustrate the selectionReact Testing Library
Rather thanEnzyme
And carry out some introductory teaching on it; - React Testing Library:
@testing-library/react
The library provides apis that, in part, guide developers to best practices for unit testing; - React Testing Library-examples:
@testing-library/react
Provides tests for a variety of common scenarios; - React unit test strategy and Landing: As the title suggests, it’s worth a look.
The related configuration
Install dependencies:
yarn add jest ts-jest @testing-library/react @testing-library/jest-dom identity-obj-proxy @types/jest @types/testing-library__react --dev
Copy the code
- Jest: JavaScript testing framework, focusing on simplicity;
- ts-jest: in order to
TypeScript
writejest
Test case support; - @testing-library/react: Simple and complete
React DOM
Testing tools that encourage good testing practices; - @testing-library/jest-dom: user-defined
jest
The verifier (matchers
) for testingDOM
The state of (i.ejest
theexcept
Method return value added more focus onDOM
thematchers
); - identity-obj-proxy: a tool library, used here
mock
Style files.
Create jest. Config.js and write the related configuration. For more configuration, please refer to jest official documentation – Configuration.
jest.config.js
module.exports = {
verbose: true.roots: ['<rootDir>/components'].moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy'.'^components$': '<rootDir>/components/index.tsx'.'^components(.*)$': '<rootDir>/components/$1',},testRegex: '(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$'.moduleFileExtensions: ['ts'.'tsx'.'js'.'jsx'].testPathIgnorePatterns: ['/node_modules/'.'/lib/'.'/esm/'.'/dist/'].preset: 'ts-jest'.testEnvironment: 'jsdom'};Copy the code
Modify package.json to add test-related commands and run test cases before code submission as follows:
package.json
"scripts": {
...
+ "test": "jest"
+ "test:watch": "jest --watch", # watch
+ "test:coverage": "jest --coverage", #
+ "test:update": "jest --update
},
...
"lint-staged": {
"components/**/*.ts?(x)": [
"prettier --write",
"eslint --fix",
+ "jest --bail --findRelatedTests",
"git add"
],
...
}
Copy the code
Modify gulpfile.js and tsconfig.json to avoid processing test files at the same time when packaging.
gulpfile.js
const paths = {
...
- scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'],
+ scripts: [
+ 'components/**/*.{ts,tsx}',
+ '! components/**/demo/*.{ts,tsx}',
+ '! components/**/__tests__/*.{ts,tsx}',
+],
};
Copy the code
tsconfig.json
{
- "exclude": ["components/**/demo"]
+ "exclude": ["components/**/demo", "components/**/__tests__"]
}
Copy the code
Write test cases
Create a __tests__ folder under the corresponding component folder to store the test files. Create an index.test. TSX file inside the folder and write the following test cases:
components/alert/tests/index.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import Alert from '.. /alert';
describe('<Alert />'.() = > {
test('should render default'.() = > {
const { container } = render(<Alert>default</Alert>);
expect(container).toMatchSnapshot();
});
test('should render alert with type'.() = > {
const kinds: any[] = ['info'.'warning'.'positive'.'negative'];
const { getByText } = render(
<>
{kinds.map(k => (
<Alert kind={k} key={k}>
{k}
</Alert>
))}
</>,); kinds.forEach(k= > {
expect(getByText(k)).toMatchSnapshot();
});
});
});
Copy the code
Update the snapshot:
yarn test:update
Copy the code
You can see that a new __snapshots__ folder has been added to the same directory to store the snapshot files for the corresponding test cases.
Then execute the test case:
yarn test
Copy the code
You can see that we passed the test case… Well, it certainly passes here, mainly because when we do subsequent iterative refactoring, we will re-execute the test case and compare it with the last snapshot. If it is inconsistent with the snapshot (the structure has changed), the corresponding test case will not pass.
There are mixed reviews for snapshot testing, and this example is so simple that it doesn’t even use the matchers provided by the extended Jest-DOM.
How to write good test cases, I am also a novice, can only say read, write, try, the previous recommended articles are good.
This section of code can be obtained from the chapter 4 branch of the repository. If there is any discrepancy between the master branch and the article, the master branch and the article shall prevail.
Standardized release process
This section explains how to use a single command to complete the following six tasks:
- Version update
- Generate the CHANGELOG
- Push to git repository
- Component library packaging
- Release to NPM
- Tag and push to Git
package.json
"scripts": {
+ "release": "ts-node ./scripts/release.ts"
},
Copy the code
Expand to view code
/* eslint-disable import/no-extraneous-dependencies,@typescript-eslint/camelcase, no-console */
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import child_process from 'child_process';
import util from 'util';
import chalk from 'chalk';
import semverInc from 'semver/functions/inc';
import { ReleaseType } from 'semver';
import pkg from '.. /package.json';
const exec = util.promisify(child_process.exec);
const run = async (command: string) = > {console.log(chalk.green(command));
await exec(command);
};
const currentVersion = pkg.version;
const getNextVersions = (): { [key in ReleaseType]: string | null({} = >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 timeLog = (logInfo: string.type: 'start' | 'end') = > {
let info = ' ';
if (type= = ='start') {
info = '=> Start task:${logInfo}`;
} else {
info = '✨ End task:${logInfo}`;
}
const nowDate = new Date(a);console.log(
` [${nowDate.toLocaleString()}.${nowDate
.getMilliseconds()
.toString()
.padStart(3.'0')}] ${info}
`,); };/** * ask to get the next version number */
async function prompt() :Promise<string> {
const nextVersions = getNextVersions();
const { nextVersion } = await inquirer.prompt([
{
type: 'list'.name: 'nextVersion'.message: Select the version to be released (current version${currentVersion}) `.choices: (Object.keys(nextVersions) as Array<ReleaseType>).map(level= > ({
name: `${level}= >${nextVersions[level]}`.value: nextVersions[level],
})),
},
]);
return nextVersion;
}
/** * Updated version *@param NextVersion Indicates the new version */
async function updateVersion(nextVersion: string) {
pkg.version = nextVersion;
timeLog('Change package.json version number'.'start');
await fs.writeFileSync(path.resolve(__dirname, '. /.. /package.json'), JSON.stringify(pkg));
await run('npx prettier package.json --write');
timeLog('Change package.json version number'.'end');
}
/** * Generates CHANGELOG */
async function generateChangelog() {
timeLog('generation CHANGELOG. Md'.'start');
await run(' npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0');
timeLog('generation CHANGELOG. Md'.'end');
}
/** * commit the code to git */
async function push(nextVersion: string) {
timeLog('Push code to git repository'.'start');
await run('git add package.json CHANGELOG.md');
await run(`git commit -m "v${nextVersion}" -n`);
await run('git push');
timeLog('Push code to git repository'.'end');
}
/** * Component library package */
async function build() {
timeLog(Component library packaging.'start');
await run('npm run build');
timeLog(Component library packaging.'end');
}
/** * publish to NPM */
async function publish() {
timeLog(Publish component Library.'start');
await run('npm publish');
timeLog(Publish component Library.'end');
}
/** * commit to git */
async function tag(nextVersion: string) {
timeLog('Tag and push to git'.'start');
await run(`git tag v${nextVersion}`);
await run(`git push origin tag v${nextVersion}`);
timeLog('Tag and push to git'.'end');
}
async function main() {
try {
const nextVersion = await prompt();
const startTime = Date.now();
// =================== Updated at ===================
await updateVersion(nextVersion);
/ / = = = = = = = = = = = = = = = = = = = updating changelog = = = = = = = = = = = = = = = = = = =
await generateChangelog();
/ / = = = = = = = = = = = = = = = = = = = code push git repository = = = = = = = = = = = = = = = = = = =
await push(nextVersion);
// =================== component library package ===================
await build();
// =================== 发布至npm ===================
await publish();
/ / = = = = = = = = = = = = = = = = = = = play tag and push to git = = = = = = = = = = = = = = = = = = =
await tag(nextVersion);
console.log('✨ Total time it takes to end the release processThe ${((Date.now() - startTime) / 1000).toFixed(3)}s`);
} catch (error) {
console.log('💣 failed to publish, cause: ', error);
}
}
main();
Copy the code
If you’re not interested in this section, you can also publish directly in NP, with some custom hooks configured.
Initialize components
You create many new files (folders) each time you initialize a component. You can also copy and paste, but you can also use a more advanced lazy approach.
Here’s the idea:
- Create component templates and reserve slots for dynamic information (component name, component description, and so on);
- Based on the
inquirer.js
Ask for dynamic information; - Insert the information into the template and render to
components
Under the folder; - Insert the export statement into components/index.ts.
We just need to configure the template and the questions, and leave the questions and rendering to plop.js.
yarn add plop --dev
Copy the code
Added script commands.
package.json
"scripts": {
+ "new": "plop --plopfile ./scripts/plopfile.ts",
},
Copy the code
Added configuration files and component templates. For details, see:
- Configuration file: scripts/plopfile.ts
- Template file: templates/ Component
conclusion
The article is very long and also a summary of my personal learning. If this article helps you, please give the warehouse ✨✨ and this article a thumbs up.
If there is any error, please correct the exchange in the comment area, thank you.
The warehouse address