preface

You can’t write front-end code without using popular UI component libraries such as ElementUI and Ant Design. But if we only stay at the level of use, it seems superficial. Therefore, I have always wanted to write a UI component library of my own.

In fact, building a component library of your own involves a wide range of knowledge:

  • Language level:TypeScipt 、 React 、 Sass ;
  • Test level:Jest 、React Testing Library  ;
  • Front-end engineering: Component library infrastructure, packaging component libraries, publishing component libraries, publishing component library documentation.

The language level and the test level need to have the basic knowledge because the space is limited in this article, hope you have a certain foundation.

The hard part of building a component library, besides writing generic components, is engineering. So this article will walk you through the steps of building and publishing an enterprise-class component library in the following order.

  1. Building the basic framework of component library;
  2. Write aButtonComponents;
  3. Component library documentation and debugging;
  4. Package and publish component libraries;
  5. Online documents are automatically published.

Component library infrastructure

The directory structure

├ ─ ─ the README, md// Documentation├ ─ ─ node_modules ├ ─ ─ package. Json ├ ─ ─ tsconfig. Json// ts configuration file├ ─ ─. Gitignore └ ─ ─ the SRC ├ ─ ─ the components/ / component library├ ─ ─ styles// Common style library└ ─ ─ index. Js// Component library entry file
Copy the code

If you look at the component library directory structure, you will find that the familiar app. TSX is missing. Indeed, we are a component library, not an application, so we do not need it.

See this summary for the complete code

Style solution

The CSS preprocessing language sass is used. Regarding the use of SASS, you can refer to its official documentation, which will not be explained too much here.

Main directory structure and function analysis:

└ ─ ─ styles ├ ─ ─ _variables. SCSS// Various variables and configurable Settings├ ─ ─ _mixins SCSS/ / global mixins├ ─ ─ _reboot SCSS// Reset the style├ ─ ─ _functions SCSS/ / global functions provides└ ─ ─ index. SCSS// Import all styles from styles└ ─ ─ components └ ─ ─ Button └ ─ ─ style.css. SCSS// Component independent style
Copy the code

Component library style variable classification

The variables in a style library fall into the following categories:

  • Basic color system
  • Writing systems
  • The form
  • button
  • Borders and Shadows
  • .

The advantage of defining variables and constants is that the full component library is uniform, which can be cumbersome to write at first, but once the project reaches a certain volume, you can feel the benefits.

# Basic color system$blue:    #0d6efd !default;
$indigo:  #6610f2 !default; . $apple-system, BlinkMacSystemFont, $apple-system"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji"."Segoe UI Emoji"."Segoe UI Symbol"."Noto Color Emoji" !default;
// Constant width font
$font-family-monospace:       SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"."Courier New", monospace !default;
// Main font
$font-family-base:            $font-family-sans-serif !default;

// Font size
$font-size-base:              1rem !default; // Assumes the browser default, typically `16px`
$font-size-lg:                $font-size-base * 1.25 !default;
$font-size-sm:                $font-size-base * 875. !default;
$font-size-root:              null !default;

/ / word
$font-weight-lighter:         lighter !default;
$font-weight-light:           300 !default; ./ / line height
$line-height-base:            1.5 !default; .// The size of the title
$h1-font-size:                $font-size-base * 2.5 !default; .Copy the code

! What is default?

Sass does! The default logo. Assign a value to a variable only if it is undefined or if its value is null. Otherwise, existing values are used.

Style reset

Use the Normalize.css solution, github github.com/necolas/nor… .

What does it do?

  • With manyCSSReset is different, leaving useful defaults;
  • Standardizing the styles of various elements;
  • Correct errors and common browser inconsistencies;
  • Improve usability with minor modifications;
  • Use detailed comments to explain what the code does.

Style resets: _reboot.scss

body {
  margin: 0; / / 1
  font-family: $font-family-base;
  font-size: $font-size-base;
  font-weight: $font-weight-base;
  line-height: $line-height-base;
  color: $body-color;
  text-align: $body-text-align;
  background-color: $body-bg; / / 2
  -webkit-text-size-adjust: 100%; / / 3
  -webkit-tap-highlight-color: rgba($black, 0); / / 4}... omitCopy the code

The base variables defined in the _variables file have been replaced.

Import the style

index.scss 

// config
@import "variables";

//layout
@import "reboot";
Copy the code

We have defined _variables and _reboot, why does the underline not exist when importing?

If you have an Scss or Sass file that you want to import, but you don’t want it to be compiled as a CSS file, you can avoid compiling it by underlining the file name. This will tell Sass not to compile it into a CSS file. Then, you can import the file as usual, and omit the underscore in front of the file name.

Style files are introduced in SRC /index.tsx

.import './styles/index.scss';
import Button from "./components/Button";

ReactDOM.render(
  <React.StrictMode>
    <Button />
  </React.StrictMode>.document.getElementById('root'));Copy the code

See this summary for the complete code

The Button component

Component Requirement Analysis

Let’s seeAnt Design  中 ButtonWhat does it look like?

Ant Design

<Button type="primary">Primary Button</Button>
<Button type="primary" disabled>Primary(disabled)</Button>.Copy the code

Let’s summarize briefly:

  1. According to thetypeDivided into:Primary 、 Default 、 Danger 、 Link ;
  2. According to thesizeDivided into:Normal 、 Small 、 Large ;
  3. According to thedisabledStatus: Disabled or normal.

With the requirements briefly analyzed here, let’s start coding.

Component code writing

Before writing Button, let’s introduce a useful tool, ClassNames, which is an easy-to-use JavaScript library that concatenates classnames by conditional judgment.

Install classnames

yarn add classnames @types/classnames
Copy the code

Using classnames

classNames('foo'.'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); / / = > '
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true.bar: true }); // => 'foo bar'
 
// lots of arguments of various types
classNames('foo', { bar: true.duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
 
// other falsy values are just ignored
classNames(null.false.'bar'.undefined.0.1, { baz: null }, ' '); // => 'bar 1'
Copy the code

Since we use TypeScript for technical selection, we can define related types based on the requirements above:

// Define the button size type
export type ButtonSize = 'lg' | 'sm';

// Define button type
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'

// Define the underlying input attributes of the Button component
interface BaseButtonProps {
  className: string;
  disabled: boolean;
  size: ButtonSize;
  btnType: ButtonType;
  children: React.ReactNode;
  href: string;
}

// Define the base type of the button to be associated with the native button
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>;

// Define the associative type of the button's base type with the native A tag
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement>;

// Perform Partial, which means that all properties become optional, such as {disabled? :boolean,... }.
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>;
Copy the code

With the type defined, let’s implement the core functionality of the Button component:

src/components/Button/Button.tsx 

const Button: React.FC<ButtonProps> = (props) = >{
  
  // Retrieve all properties using the destruct assignment of the ES6 object, where restProps is all properties except those defined by the display.
  const{ btnType, disabled, size, children, className, href, ... restProps/ / ES6 rest syntax
  } = props;
  
	// Use classNames to determine the corresponding class value of the button.
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType, // Add the 'BTN -${btnType}' class dynamically when the btnType parameter exists
    [`btn-${size}`]: size, // Add 'BTN -${size}' dynamically when size parameter exists
    'disabled': (btnType === 'link') && disabled // Since a link does not have the disabled attribute native, you need to manually add a disabled class to it. The disabled effect is achieved by styling the class
  })
  
	// Output a link if link type, otherwise output button.
  if(btnType === 'link' && href){
    return (
      <a
        {. restProps}
        href={href}
        className={classes}
      >
        {children}
      </a>)}else {
    return (
      <button
        {. restProps}
        className={classes}
        disabled={disabled}
      >
        {children}
      </button>)}}// Attribute default value
Button.defaultProps = {
  disabled: false.btnType: 'default'
}
Copy the code

Button/_styles.scss 

.btn {
  position: relative;
  display: inline-block;
  font-weight: $btn-font-weight;
  line-height: $btn-line-height;
  color: $body-color;
  white-space: nowrap;
  text-align: center;
  vertical-align: middle;
  background-image: none;
  border: $btn-border-width solid transparent;
  @include button-size( $btn-padding-y,  $btn-padding-x,  $btn-font-size,  $border-radius);
  box-shadow: $btn-box-shadow;
  cursor: pointer;
  transition: $btn-transition;
  &.disabled,
  &[disabled] {
    cursor: not-allowed;
    opacity: $btn-disabled-opacity;
    box-shadow: none;
    > * {
      pointer-events: none; // Clear the mouse event} } } .btn-lg { @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); }... omitCopy the code

In addition to the use of sass advanced syntax such as variables, mixins, etc., other styles are the same as normal CSS, the main reason is not to forget the introduction of index. SCSS:

// button
@import ".. /components/Button/styles";
Copy the code

The business code for the Button component is pretty much done here, but there is one very important thing to do, which is unit testing. Since it is a generic component, it is a good way to test whether its function is as expected.

Component test code

The most popular React unit Testing solutions are JEST and React Testing Library. Since unit testing is a very large subject, I won’t go into detail in this article.

JEST

Jest is a JavaScript testing framework designed to ensure that any JavaScript code is correct. It allows you to write tests using an accessible, familiar, and feature-rich API that allows you to get results quickly.

Features:

  • Zero configuration:JestThe goal is at mostJavaScriptOut of the box on the project, no configuration required.
  • Snapshot: Build can easily track largeObjectThe test. Snapshots can be independent of the test code or integrated into the line of code.
  • Isolated: The test program computes in parallel in its own process to maximize performance.
  • goodapiFrom:it 到 expect , JestHaving the whole kit in one place is easy to write, easy to maintain and very convenient.
  • Fast and safe: By making sure your tests have a unique global state,JestTests can be reliably run in parallel. To speed up the testing process,JestPreviously failed tests are run first, and the tests are reorganized based on how long the test files take.
  • Code coverage: by adding--coverageFlag generates a code coverage report without additional Settings.JestCode coverage information can be collected from the entire project, including untested files.

React Testing Library

A very lightweight solution that does not require all the implementation details. It helps us quickly find the nodes in the application.

Button component testing

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps } from './Button'

const defaultProps = {
  onClick: jest.fn()
}

describe('test Button component'.() = > {
  it('should render the correct default button'.() = > {
    // The wrapper gets the React component instance information parsed by the Render method
    const wrapper = render(<Button {. defaultProps} >Nice</Button>)
    // Element is obtained from a text value similar to "DOM"
    const element = wrapper.getByText('Nice') as HTMLButtonElement
  	// You can make a series of assertions using the JEST framework
    expect(element).toBeInTheDocument()
    expect(element.tagName).toEqual('BUTTON')
    expect(element).toHaveClass('btn btn-default')
    expect(element.disabled).toBeFalsy()
  	// Trigger the element click event
    fireEvent.click(element)
  	// Assert that the simulated event was fired
    expect(defaultProps.onClick).toHaveBeenCalled()
  })
})

Copy the code

From this simple example, it can be seen that it is actually simulating the user’s use trajectory, “what do you do to get the result”.

At this point, a Button component with functions, styles, and tests is complete.

See this summary for the complete code

Component library documentation and debugging

Although we only developed a Button component, we had a problem with debugging difficulties and no documentation for the component when it was written. We had to read the source code if we wanted to use the Button component. Obviously, this is very unreasonable, and we need tools to help us implement documentation.

Storybook

Storybook is an open source tool for developing UI components for React, Vue, Angular, and other isolation.

Features:

  • A sandbox is provided for building in isolationUIComponents;
  • Advanced plug-in capabilities are provided to build fasterUI, document component libraries and simplify workflow;
  • StorybookAllowing us to easily incorporate technical documentation into our design system makes developing components much simpler.

Installation:

NPX -p @storybook/cli sb init or yarnglobal @storybook/cli && sb init
Copy the code

Activation:

npm run storybook
Copy the code

Add a file: the Button/Button. Stories. The TSX

import React from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import Button , { ButtonProps } from './Button';
import ".. /.. /styles/index.scss";

export default {
  title: 'Button'.component: Button,
} as Meta;

const Template: Story<ButtonProps> = (args) = > <Button {. args} / >;

export const Primary = Template.bind({});
Primary.args = {
  btnType: 'danger'.children: "Sure"
};
Copy the code

Final document effect:

This is just using storyBook out of the box, it’s very powerful, and interested students can read the official storybook document.

See this summary for the complete code

Component libraries are packaged and released

packaging

In the React App packaging process, the index. TSX file is the entry and the App is the root component. Finally, a JavaScript script is generated to dynamically insert the DOM and styles into the page to form a complete page.

A component library is not an application, so obviously it can’t be packaged this way. Let’s first look at how components in Ant Design are used by applications:

import {Button} from "antd";
Copy the code

This means that the entry file for our component library is the external provider for all components.

Transformation under the index. The TSX

export { default as Button } from "./components/Button";
Copy the code

Of course, here we only write a Button component, if there are more than one component can be exported with the same syntax.

Webpack is known as a module packer, which analyzes the dependencies in a module into a large JavaScript package, whereas our component libraries are available directly to consumers in the form of ESModule modules. So we just need to package TypeScript syntax as normal ES5 syntax. So when packaging, use TS compilation to package.

Create a new tsconfig.build.json file as the configuration for the package:

{
  "compilerOptions": {
    "outDir": "dist".// Pack the output location
    "module": "esnext".// Set the module standard for the generated code. This can be CommonJS, AMD, UMD, etc.
    "target": "es5".// Version of the target language
    "declaration": true.// Generate the declaration file, remember inde.d.ts
    "jsx": "react".// Equivalent React. CreateElement call
    "moduleResolution":"Node".// Module resolution policy. There are two resolution policies: node and classic. Ts uses node resolution by default.
    "allowSyntheticDefaultImports": true.// Allow default imports for modules that do not contain default exports. This option does not affect the generated code, only the type checking.
    "skipLibCheck": true // Skip the library check
  },
  "include": [
    "src" // Compile the folder]."exclude": [ // Exclude compiled files
    "src/**/*.test.tsx"."src/**/*.stories.tsx"."src/setupTests.ts"."stories/**/*.svg"."stories/**/*.mdx"]}Copy the code

Package. Json add scripts

// To clear files, you need to manually install the rimraf package
"clean": "rimraf ./dist".// Compile the TS file with the tsconfig.build.json configuration
"build-ts": "tsc -p tsconfig.build.json".// Compile the sass file
"build-css": "node-sass ./src/styles/index.scss ./dist/index.css".// Compile the total command, and execute the secondary
"build": "npm run clean && npm run build-css && npm run build-ts"
Copy the code

Run the NPM run build command to look at the files produced in the dist directory:

Now that the packing task is complete, check out the complete code for this summary.

release

Once the package is packaged, we need to publish the package to online NPM package management. So we need to log in and register NPM first.

NPM config ls NPM adduserUsername:shiyou
Email([email protected]) # Login to NPM login and fill in the registration informationCopy the code

Package. Json optimization

{
  "name": "lion-design".// The package name must be unique
  "version": "1.0.1"./ / version number
  "author": "Lion"./ / the author
  "private": false./ / not private
  "main": "dist/index.js".// The entry file for the project
  "module": "dist/index.js".// Points to a module with ES2015 module syntax, but only to the syntax functionality supported by the target environment.
  "types": "dist/index.d.ts".// Fields that are valid only in TypeScript, pointing to the declaration file
  "license": "MIT".// Use license
  "homepage": "https://github.com/shiyou00/lion-design".// is the project home or document home of the package.
  "repository": { // Where the code is managed
    "type": "git"."url": "https://github.com/shiyou00/lion-design"
  },
  "files": [ // The files that the project uploads to the NPM server can be individual files, entire folders, or wildcard matching files.
    "dist"]."scripts": {
    "clean": "rimraf ./dist"."build-ts": "tsc -p tsconfig.build.json"."build-css": "node-sass ./src/styles/index.scss ./dist/index.css"."build": "npm run clean && npm run build-css && npm run build-ts"."prepublishOnly": "npm run build" // Execute the default command before NPM Publish
  },
  // The dependencies required by the development and distribution versions
  "dependencies": {
    "@testing-library/jest-dom": "^ 5.11.4." "."@testing-library/react": "^ 11.1.0"."@testing-library/user-event": "^ 12.1.10"."classnames": "^ 2.2.6." ",},// Develop environment dependencies
  "devDependencies": {
    "@storybook/addon-actions": "^ 6.1.10"."@storybook/addon-essentials": "^ 6.1.10"."@storybook/addon-links": "^ 6.1.10"."@storybook/node-logger": "^ 6.1.10"."@storybook/preset-create-react-app": "^ 3.1.5." "."@storybook/react": "^ 6.1.10"."@types/classnames": "^ 2.2.11." "."@types/jest": "^ 26.0.15"."@types/node": "^ 12.0.0"."@types/react": "^ 16.9.53"."@types/react-dom": "^ 16.9.8"."rimraf": "^ 3.0.2." "."node-sass": "^ 4.13.0"."react-scripts": "4.0.1"."react": "^ 17.0.1"."react-dom": "^ 17.0.1"."typescript": "^ 4.0.3"
  },
  // Specify the version that you need to rely on. However, do not forcibly install it to avoid version conflict
  "peerDependencies": {
    "react": "> = 16.8.0"."react-dom": "> = 16.8.0"
  },
  "resolutions": {
    "@storybook/react/babel-loader": "8.1.0" // Allows you to override the version of a particular nested dependency. Resolve Babel dependency conflicts in StoryBook and React-scripts}}Copy the code

Run NPM Publish to send packets. Click to view the lion-design online address after the packets are sent successfully. Once released, you can install your own UI component library in your project, even though it currently only has a Button component.

use

Create a new project create-react-app lion-design-test create a new project create-react-app lion-design-test create a new project create-react-app lion-design-test create a new project create-react-app lion-design-testimport "lion-design/dist/index.css"; # app.jsimport { Button } from "lion-design"
<Button btnType="primary"> confirm < / Button >Copy the code

This should give you a global understanding of how to write and use a component library. As mentioned above, we also need an online document for users to review.

Online documents are automatically published

Those of you familiar with Github should know about Github Pages, which hosts github project Pages.

Now what I want to do is, when I modify the documents in the project, as soon as I perform Git push, the online documents will be automatically updated. How do you do it automatically? From here comes CI/CD, an important concept in software engineering.

CI/CD

  • CIContinuous integration, frequently integrating code into the trunkmainBranching, which is designed to allow products to iterate quickly while maintaining quality.
  • CDContinuous delivery, continuous deployment, the frequent delivery of new versions of software to quality teams or users, and the automatic deployment of code to the build environment after it has been reviewed.

After a brief introduction to the concept, there is an online CI/CD platform, Travis, which helps us with this series of automated tasks.

Travis

1. Login and register with Github authorization.

Add the.travis. Yml file to the project root (please remove the comment code)

language: node_js // Use the node_js language for deployment
node_js:
  - "stable" / / the node. Js version
cache:
  directories:
  - node_modules // node_modules sets the cache
env:
  - CI=true // Set environment variables
script:
  - npm run build-storybook // Execute the script command, in this case, to build the online document
deploy: // Automatically deploy github Pages configuration
  provider: pages
  skip_cleanup: true
  github_token: $github_token
  local_dir: storybook-static // The folder to be deployed
  on:
    branch: main // Deploy based on the main branch
Copy the code

First provide the personal access token on Github and then configure it on the Travis site:

[note]github_tokenistravisThe name of the variable that you set. This must correspond to.

After the setup, git push triggers these automated processes and ultimately updates the component library documentation.

Travis workflow analysis:

  1. To obtaingitThe latest code for the corresponding branch (main).
  2. Using the configurednode.jsperformyarn installCommand (if there is one in the projectyarn.lock) to generatenode_modules
  3. rightnode_modulesCache.
  4. When everything is ready, go aheadnpm run build-storybookCommand to generate component library documentation.
  5. deployIn is the deployment configuration that our project is going to deploy togithub pagesOn, deploy to usetokenToken isgithub_token, the content of the deployment isstorybook-staticFolder.

This is the automated deployment process of Travis. Of course it’s capable of much more than that.

Component library online document address

See this summary for the complete code

summary

This article builds the basic framework of the component library step by step and writes the Button component as a simple skeleton of the component library and publishes the online NPM package and online documentation of the component library.

After learning and expanding on yourself in this article, you should be able to build and publish at least one component library. Therefore, it is certainly not enough to do this only. Due to the limitation of space, the author will write the first and last part. The core of the second part is some thinking and writing of the common component itself.

If you liked this article, please give it a thumbs up!!