[Huawei Developers] Martin Bieber:
Recently, we have been trying to figure out how to build React component libraries. The reason for this idea is that component libraries are very important to the front-end ecosystem. Every Internet company that focuses on long-term development and development efficiency basically customizes their own component libraries. Its benefits need not be said much. For front-end engineers, understanding and mastering it will enable us to develop a special skill in future jobs and in the recruitment process, and it will be beneficial for their vertical development. Here's how I document my process of building the component library.

Initializing a Project

I'm not going to use the create-react-app scaffolding to build the project because the scaffolding encapsulates a lot of things, and some things don't work for the component library. It's too bloated to build the component library, so I'm not going to use any scaffolding to build the project.

First of all,create a project folder pony-react-ui and run the following commands in the folder:

npm init // Generating package.json

tsc --init // Generating tsconfig.json

Then, initialize the project according to the following directory structure:

pony-react-ui

├── src

├── assets

├── components

├── Button

├── Button.tsx

└── index.ts

└── Dialog

├── Dialog.tsx

└── index.ts

├── styles

├── _button.scss

├── _dialog.scss

├── _mixins.scss

├── _variables.scss

└── pony.scss

└── index.ts // Packaged entry file, import pony.scss, and throw each component

├── index.js // Main file entry, which is specified by the main field in package.json

├── package.json

├── tsconfig.json // Specifies the root file and compilation options for compiling this project

├── webpack.config.js

└── README.md

Compile a Button Component

The Button component must meet the following requirements:

· Different sizes

· Different types

· Different colors

· Disabled status

· Click events

Button.tsx

import React from 'react';

import classNames from 'classnames';

export interface IButtonProps {

onClick?: React.MouseEventHandler;

// types

primary?: boolean;

secondary?: boolean;

outline?: boolean;

dashed?: boolean;

link?: boolean;

text?: boolean;

// sizes

xLarge?: boolean;

large?: boolean;

small?: boolean;

xSmall?: boolean;

xxSmall?: boolean;

// colors

success?: boolean;

warn?: boolean;

danger?: boolean;

// Disable Status

disabled?: boolean;

className?: string;

style?: React.CSSProperties;

children?: React.ReactNode;

}

export const Button = (props: IButtonProps) => {

const {

className: tempClassName,

style,

onClick,

children,

primary,

secondary,

outline,

dashed,

link,

text,

xLarge,

large,

small,

xSmall,

xxSmall,

success,

danger,

warn,

disabled,

} = props;

const className = classNames(

{

'pony-button': true,

'pony-button-primary': primary,

'pony-button-secondary': secondary && !text,

'pony-button-outline': outline,

'pony-button-dashed': dashed,

'pony-button-link': link,

'pony-button-text': text && !secondary,

'pony-button-text-secondary': secondary && text,

'pony-button-round': round,

'pony-button-rectangle': noRadius,

'pony-button-fat': fat,

'pony-button-xl': xLarge,

'pony-button-lg': large,

'pony-button-sm': small,

'pony-button-xs': xSmall,

'pony-button-xxs': xxSmall,

'pony-button-long': long,

'pony-button-short': short,

'pony-button-success': success,

'pony-button-warn': warn,

'pony-button-danger': danger,

'pony-button-disabled': disabled,

},

tempClassName

);

return (

<button

type="button"

className={className}

style={style}

onClick={onClick}

disabled={disabled}>

<span className="pony-button__content">{children}</span>

</button>

);

}

Throws the Button component and the defined type in the Button/index.ts file.

export * from './Button';

In this way, a sample component is basically complete, and some students will have this question, why not introduce its style file _button.scss in Button.tsx**, but introduce global styles or** _button.scss alone when using it?

// Introduce Component Styles Separately

import { Button } from 'pony-react-ui';

import 'pony-react-ui/lib/styles/button.scss';

// Globally introduced component styles, which are extracted during packaging

import 'pony-react-ui/lib/styles/index.scss';

This is related to the weight of the style. The weight of the style imported through import is lower than that defined by className in JSX. Therefore, the internal style can be modified outside the component.

For example:

import { Button } from 'pony-react-ui';

import 'pony-react-ui/lib/styles/button.scss';

import styles from './index.module.scss';

const Demo = () => (

<div className={styles.btnBox}>

<Button onClick={submit}>submit</Button>

</div>

)

Button.scss and local index.module.scss from the imported component library are injected into the page with the <style></style> tag after packaging. The sequence is as follows:

<style type="text/css">

// Button.scss style

</style>

<style type="text/css">

// index.module.scss style

</style>

Therefore, the style weight in index.module.scss is higher than that in Button.scss. You can modify the style of Button.scss in index.module.scss.

Writing Styles

├── styles

├── _button.scss

├── _dialog.scss

├── _mixins.scss

├── _variables.scss

└── pony.scss

All style files are stored in the style file. Style files of the _button.scss and _dialog.scss types are component style files. _mixins.scss is used to store mixin instructions to improve style logic reuse.

// _mixins.scss

u/mixin colors($text, $border, $background) {

color: $text;

background-color: $background;

border-color: $border;

}

// Set Button Size

u/mixin button-size($padding-x, $height, $font-size) {

height: $height;

padding: 0 $padding-x;

font-size: $font-size;

line-height: ($height - 2);

}

For example, in _button.scss

$values: #ff0000, #00ff00, #0000ff;

.primary {

u/include colors($values...);

}

node-sass compiles it into

.primary {

color: #ff0000;

background-color: #00ff00;

border-color: #0000ff;

}

_variables.scss is used to store some style constants, such as defining the font size of buttons of different sizes:

$button-font-size: 14px !default;

$button-xl-font-size: 16px !default;

$button-lg-font-size: 16px !default;

$button-sm-font-size: 12px !default;

pony.scss introduces all style files. Tool style files such as _mixins.scss and _variables.scss need to be imported before the pony.scss file because the subsequent component style files may depend on them.

u/import 'variables';

u/import 'mixins';

u/import 'button';

u/import 'dialog';

...

Instead of using css modules to avoid duplicate style names, I use the BEM specification to write styles to avoid this problem. Why would I do that?

rules: [

{

test: /\.(sa|sc|c)ss$/,

use: [

loader: 'css-loader',

options: {

modules: false // Disabling CSS Modules

}

]

}

]

The internal style of a component cannot be modified from outside the component because css modules are used. Typically, modifying a component style from the outside would say this:

<Button className="btn"> Button </Button>

// Modify the internal style of the Button. Assume that the internal style of the component has a style class named pony-button-promary

.btn {

:global {

.pony-button-promary {

color: #da2227;

}

}

}

However, after css modules is used, a string of hash values is added after the pony-button-promary class name. In addition, the generated hash values are different each time the Button component is modified. As a result, the class name cannot be found during the deep traversal lookup.

.btn {

:global {

// After the Button component is modified next time, the generated hash may not be sadf6756

.pony-button-promary-sadf6756 {

color: #da2227;

}

}

}

Construct

Package the entry file.

src/index.ts Build the entry file for the webpack.

import './styles/pony.scss';

export * from './components/Button';

export * from './components/Dialog';

The global style file is introduced. During construction, MiniCssExtractPlugin extracts and compresses the style, and then separates the JS script and CSS script.

Package and output the UMD specifications.

Before building, we must identify the usage scenarios of the component library. Currently, the es module and CommonJS are commonly used. In some scenarios, <script> is directly used to import the HTML file. In some rare scenarios, AMD (require.js) and CMD (sea.js) are used to import the file. As a component library, it should be compatible with these usage scenarios. Component libraries should be neutral and should not be limited to one use.

To support multiple usage scenarios, we need to select a proper packaging format. Webpack provides multiple packaging and output modes, as follows:MyLibrary is the variable name defined by output.library.

· libraryTarget: 'var': When the library is loaded, the return value of the entry start point is assigned to a variable.

var MyLibrary = _entry_return_;

// In a separate script...

MyLibrary.doSomething();

· libraryTarget: 'this': The return value from the entry start point will be assigned to an attribute of this, and the meaning of this depends on you.

this['MyLibrary'] = _entry_return_;

// In a separate script...

this.MyLibrary.doSomething();

MyLibrary.doSomething(); // If this is a window

· libraryTarget: 'window': The return value of the entry start point is assigned to this property of the window object.

window['MyLibrary'] = _entry_return_;

window.MyLibrary.doSomething();

· libraryTarget: 'global': The return value of the entry start point is assigned to this attribute of the global object.

global['MyLibrary'] = _entry_return_;

global.MyLibrary.doSomething();

· libraryTarget: 'commonjs': The return value of the entry start point is assigned to the exports object. This name also means that the module is used in the CommonJS environment.

exports['MyLibrary'] = _entry_return_;

require('MyLibrary').doSomething();

· libraryTarget: 'module': The ES module is output. Note that this function is not fully supported.

· libraryTarget: 'commonjs2': The return value from the entry start point is assigned to the module.exports object. This name also means that the module is used in the CommonJS environment.

module.exports = _entry_return_;

require('MyLibrary').doSomething();

· libraryTarget: 'amd': AMD modules require entry chunks (e.g., the first script loaded with tags) to be defined by specific attributes, such as define and require, which are typically provided by RequireJS or any compatible module loader (e.g., almond). Otherwise, loading the generated AMD bundle will result in an error message, such as define is not defined.

module.exports = {

//...

output: {

library: 'MyLibrary',

libraryTarget: 'amd',

},

};

The generated output name will be defined as "MyLibrary":

define('MyLibrary', [], function () {

return _entry_return_;

});

You can introduce the bundle as a module in the script tag, and you can call the bundle like this:

require(['MyLibrary'], function (MyLibrary) {

// Do something with the library...

});

If output.library is not defined, the following is generated:

define([], function () {

return _entry_return_;

});

· libraryTarget: 'umd': exposes your library as a way to run under all module definitions. It will run in a CommonJS, AMD environment, or export the module to a variable under global.

module.exports = {

//...

output: {

library: 'MyLibrary',

libraryTarget: 'umd',

},

};

The final output is as follows:

(function webpackUniversalModuleDefinition(root, factory) {

if (typeof exports === 'object' && typeof module === 'object')

module.exports = factory();

else if (typeof define === 'function' && define.amd) define([], factory);

else if (typeof exports === 'object') exports['MyLibrary'] = factory();

else root['MyLibrary'] = factory();

})(typeof self !== 'undefined' ? self : this, function () {

return _entry_return_;

});

Set libraryTarget="umd" to the umd packaging format according to the preceding description. The configuration of the webpack processing script, style, and font file is as follows:

const path = require('path');

const webpack = require('webpack');

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const TerserJSPlugin = require('terser-webpack-plugin');

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

// const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')

// const { CleanWebpackPlugin } = require('clean-webpack-plugin');

// const LoadablePlugin = require('@loadable/webpack-plugin')

// const smp = new SpeedMeasurePlugin() // Measure build speed

const devMode = process.env.NODE_ENV !== 'production';

const pkg = require('./package.json');

module.exports = ({

mode: devMode ? 'development' : 'production',

devtool: devMode ? 'inline-source-map' : 'hidden-source-map',

entry: path.resolve(__dirname, './src/index.ts'),

output: {

path: path.resolve(__dirname, './dist'),

filename: devMode ? 'pony.js' : 'pony.min.js',

library: 'pony',

libraryTarget: 'umd'

},

resolve: {

// Add `.ts` and `.tsx` as a resolvable extension.

extensions: ['.ts', '.tsx', '.js'],

alias: {

}

},

module: {

rules: [

// all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`

{

test: /\.tsx?$/,

use: [

'babel-loader?cacheDirectory',

{

loader: 'ts-loader',

options: {

configFile: 'tsconfig.json'

}

}

]

},

{

test: /\.(sa|sc|c)ss$/,

use: [

{

loader: MiniCssExtractPlugin.loader // Extract the style file and import the CSS style file using the link tag. If you use this loader, you do not need to use the style-loader

},

{

loader: 'css-loader',

options: {

modules: {

auto: true,

localIdentName: '[path][name]__[local]'

},

importLoaders: 2, // If another CSS is introduced to a CSS, the first two loaders, postcss-loader and sass-loader, are also executed

}

},

{

// Use postcss to add a browser prefix to the CSS

loader: 'postcss-loader',

options: {

// options has an unknown property 'plugins';

postcssOptions: {

// PostCSS plugin autoprefixer requires PostCSS 8. The autoprefixer version is reduced to 8.0.0

plugins: [require('autoprefixer')]

}

}

},

{

loader: 'sass-loader' // Run the sass-loader command to convert scss to css

}

]

},

{

test: /(\.(eot|ttf|woff|woff2)|font)$/,

loader: 'file-loader',

options: { outputPath: 'fonts/' }

},

{

test: /\.(png|jpg|gif|svg|jpeg)$/,

loader: 'file-loader',

options: { outputPath: 'images/' }

}

]

},

plugins: [

// new CleanWebpackPlugin(),

// new LoadablePlugin(),

// This plug-in enables the specified directory to be ignored, which makes packing faster and files smaller. The file directory containing the ./locale/ field is omitted. However, the Chinese language cannot be displayed. Therefore, you can manually import the directory in the Chinese language

new webpack.IgnorePlugin(/\.\/locale/, /moment/),

// This command is used to add a copyright notice to the beginning of a packaged JS file

new webpack.BannerPlugin(`pony ${pkg.version}`),

// Extract CSS into a separate file

new MiniCssExtractPlugin({

// Options similar to the same options in webpackOptions.output

// both options are optional

filename: devMode ? 'pony.css' : 'pony.min.css',

chunkFilename: '[id].css'

})

// devMode ? new webpack.HotModuleReplacementPlugin() : null

],

optimization: {

minimizer: devMode

? []

: [

// Compressing JS Code

// new UglifyJsPlugin({

// cache: true, // Enable file caching and set the path to the cache directory

// parallel: true, // Running with Multiple Processes in Parallel

// sourceMap: true // set to true if you want JS source maps

// }),

// webpack v5 uses the built-in TerserJSPlugin to replace UglifyJsPlugin because UglifyJsPlugin does not support ES6

new TerserJSPlugin({

// cache: true, // Enable file caching and set the path to the cache directory

parallel: true, // Running with Multiple Processes in Parallel

// sourceMap: true // set to true if you want JS source maps

}),

// Used to optimize or compress CSS resources.

new OptimizeCSSAssetsPlugin({

assetNameRegExp: /\.css$/g,

cssProcessor: require('cssnano'), // CSS processor used to optimize/minimize the CSS. The default value is cssnano

cssProcessorOptions: { safe: true, discardComments: { removeAll: true } }, // 传递给 cssProcesso

canPrint: true // Boolean value indicating whether the plug-in can print messages to the console, defaults to true

})

],

sideEffects: false

}

});

Continue...