1. Introduction

2018 is coming to an end. This year has been a busy and busy year. One day in the middle of the year, I suddenly wanted to develop a React component library. I’m glad I’m not a three-minute fan. I spent three months, weekends and weekday breaks, making a CuKE-UI here, and record what I’ve learned

Making | website

2. The modular

In 2011, when I was still in junior high school, the two leaders of Twitter were assigned too many repetitive tasks by their boss. Because they were too lazy, they accidentally developed Bootstrap, which goes without saying, although I don’t like it very much. But it was definitely one of the most popular, one of the first front-end Ui libraries, and it was at that time that I realized the importance of CV programming as little as possible

By now, the three frameworks have dominated the world, components have become an integral part, and various UI libraries have emerged in an endless stream. The most popular was ANTD, so I thought I could copy it and start working

3. Build the project

  • .storebookSome configuration for storeBook
  • componentsReference antD to place all components
  • scriptsPublish, package, and related scripts
  • storiesProject static documents, responsible for demo
  • testsTest something relatedsetup

There is nothing else to say, just some regular files, I have to joke that more and more configuration files are needed to build a project

3.1 StoryBook Website building

A component library must need a demo of the static website, such as ANTD Button comparison, selected a relatively simple storeBook to build the website

import React from "react" import { configure, addDecorator } from '@storybook/react'; import { name, repository } from ".. /package.json" import { withInfo } from '@storybook/addon-info'; import { withNotes } from '@storybook/addon-notes'; import { configureActions } from '@storybook/addon-actions'; import { withOptions } from '@storybook/addon-options'; import { version } from '.. /package.json' import '@storybook/addon-console'; import ".. /components/styles/index.less" import ".. / stories/styles/code. Less "function loadStories () {/ / introduced the require (".. /stories/index'); / / ordinary the require ('.. /stories/general'); // Audio-visual entertainment require('.. /stories/player'); / / navigation the require (".. /stories/navigation') // data entry require('.. /stories/dataEntry'); // require('.. /stories/dataDisplay'); / / layout the require (".. /stories/grid'); // Operation feedback require('.. /stories/feedback'); / / other the require (".. /stories/other'); } configureActions({ depth: 100 }) addDecorator(withInfo({ header: true, maxPropsIntoLine: 100, maxPropObjectKeys: 100, maxPropArrayLength: 100, maxPropStringLength: 100, })) addDecorator(withNotes); addDecorator(withOptions({ name: `${name} v${version}`, url: repository, sidebarAnimations: true, })) addDecorator(story => <div style={{ padding: "0 60px 50px" }}>{story()}</div>) configure(loadStories, module);Copy the code

Write stories

import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Button from '.. /components/button';
import './styles/button.less';

import ".. /components/button/styles.less";
import { SuccessIcon } from '.. /components/icon';

storiesOf('ordinary'.module).add(
  'Button Button', () = > (<div className="button-example">
      <h2>The basic use</h2>

      <Button onClick={action('clicked')} >The default</Button>
     </div>))Copy the code

With webpack.config.js is basically done, the configuration is not pasted, regular operation

Look at the effect

Wow, seems to be such a thing, mei Zi, here although a few words finished, the actual time I masturbated, or encountered a lot of very cumbersome trouble, such as webpack4 [email protected] is not compatible with storybook version ah, a variety of search issue ah, Good thing it worked out

Storybook provides a static publishing plugin, which solves my last problem, publishing to github’s GH-Page and adding two lines of NPM scripts

"scripts": {
    "start": "yarn dev"."clean": "rimraf dist && rimraf lib"."dev": "start-storybook -p 8080 -c .storybook"."build:docs": "build-storybook -c .storybook -o .out"."pub:docs": "yarn build:docs && storybook-to-ghpages --existing-output-dir=.out",}"storybook-deployer": {
    "gitUsername": "cuke-ui"."gitEmail": "[email protected]"."commitMessage": "docs: deploy docs"
},
Copy the code

Then run

yarn pub:docs

Copy the code

The principle is simple: first package the document with WebPack, then git add. Then push when the remote GH-pages branch,

You can see the currently deployed static web site through repo => Setting => Github Pages

3.2 Start writing components

Site set up, equivalent to buy a good kitchen utensils, can start cooking, where is the dish? Ok, and we have to grow our own vegetables. Now let’s start growing Button

cd components && mkdir button
Copy the code

Create a new button directory under the Components directory

  • __tests__/ / test
    • index.test.js
  • index.js// Component entry
  • styles.less// Component styles
// index.js

import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import cls from "classnames";

export default class Button extends PureComponent {

    // The specific code
}
Copy the code

// styles.less

@import ".. /styles/vars.less";
@import ".. /styles/animate.less";
@import ".. /styles/mixins.less"; @prefixCls : cuke-button; .@{prefixCls} {// specific styles}Copy the code
// index.test.js

import React from "react";
import assert from "power-assert";
import { render, shallow } from "enzyme";
import toJson from "enzyme-to-json";
import Button from ".. /index";

describe("<Button/>", () => {
  it("should render a <Button/> components", () => {const wrapper = render(<Button> Hello </Button>) expect(toJson(wrapper)).tomatchsnapshot (); })Copy the code

Now that the component is written, let’s assume that the component library only has a Button component for the time being, and the only thing left is to publish to NPM

This allows users to use it as follows

import { Button } from "cuke-ui"
import "cuke-ui/dist/cuke-ui.min.css"Reactdom.render (<Button> Hello </Button>, document.getelementById ('root'))Copy the code

3.3 Writing a packaging configuration

Typically, component libraries provide two ways to introduce

  1. By Babel
babel components -d lib
Copy the code
  1. Introduced by the script tagUMDGeneric module specification
<link rel="stylesheet" href="https://unpkg.com/cuke-ui@latest/dist/cuke-ui.min.css">
<script type="text/javascript" src="https://unpkg.com/cuke-ui@latest/dist/cuke-ui.min.js"></script>
Copy the code

I’ve only used the first option when writing plugins, and the second option for various open source projects until I learned that umDS can be packaged via WebPack

// scripts/build.umd.js

const config = {
  mode: "production",
  entry: {
    [name]: ["./components/index.js"}, // output: {library: name, libraryTarget:"umd",
    umdNamedDefine: truePath: path.join(process.cwd(),"dist"),
    filename: "[name].min.js"},... } module.exports = configCopy the code

Webpack4 is used here, so specify mode as production and it will automatically optimize for you, focusing on entry and output

Find index.js under Componnets and enter it into the dist directory to generate cuke-ui.min.js.

It turns out we’re missing an entry file

// components/index.js

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

The default module export is given an alias, which has the advantage of unifying the names of components exposed to users

Finally, we added a command to NPM scripts so that we didn’t have to package it manually every time

"clean": "rimraf dist && rimraf lib"."build": "yarn run clean && yarn build:lib && yarn build:umd && yarn build:css"."build:css": "cd scripts && gulp"."build:lib": "babel components -d lib"."build:umd": "webpack --config ./scripts/build.umd.js".Copy the code
  • cleanTo prevent any changes to the dist and lib directories, delete them before each package.
  • build:libBabel package toesModule tolibdirectory
  • build:umdThat was explained just now

Now run

yarn build
Copy the code

Js related parts are ok now and can be used directly

import { Button } from './lib'

Copy the code
<script type="module">
import {Button} from "https://unpkg.com/cuke-ui@latest/dist/cuke-ui.min.js"
</script>
Copy the code

At this point, you will find that there is still a lack of packaging for CSS, and add gulp configuration

This section of configuration copy dragon- UI configuration, slightly changed

const path = require('path');
const gulp = require('gulp');
const concat = require('gulp-concat');
const less = require('gulp-less');
const autoprefixer = require('gulp-autoprefixer');
const cssnano = require('gulp-cssnano');
const size = require('gulp-filesize');
const sourcemaps = require('gulp-sourcemaps');
const rename = require('gulp-rename');
const { name } = require('.. /package.json')
const browserList = [
  "last 2 versions"."Android > = 4.0"."Firefox ESR"."not ie < 9"
]

const DIR = {
  less: path.resolve(__dirname, '.. /components/**/*.less'),
  buildSrc: [
    path.resolve(__dirname, '.. /components/**/styles.less'),
    path.resolve(__dirname, '.. /components/**/index.less'),
  ],
  lib: path.resolve(__dirname, '.. /lib'),
  dist: path.resolve(__dirname, '.. /dist'),}; gulp.task('copyLess', () = > {return gulp.src(DIR.less)
    .pipe(gulp.dest(DIR.lib));
});

gulp.task('dist', () = > {return gulp.src(DIR.buildSrc)
    .pipe(sourcemaps.init())
    .pipe(less({
      outputStyle: 'compressed',
    }))
    .pipe(autoprefixer({ browsers: browserList }))
    .pipe(concat(`${name}.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(rename(`${name}.css.map`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))

    .pipe(cssnano())
    .pipe(concat(`${name}.min.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(rename(`${name}.min.css.map`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist));
});

gulp.task('default'['copyLess'.'dist']);
Copy the code

This code finds all the less files under components, compacting them, packaging them into the dist directory, and generating the cuke-ui.min.css file

4. Publish components

I’m sure you all know how to publish NPM packages so I won’t go into detail here, but what about the code

// package.json
 "name": "cuke-ui"."version": "1.2.1"."main": "lib/index.js"."description": "A React.js UI components for Web"."repository": "https://github.com/cuke-ui/cuke-ui.git"."homepage": "https://cuke-ui.github.io/cuke-ui-landing/"."author": "Jinke.Li <[email protected]>"."license": "MIT"."private": false."files": [
    "lib"."dist"."LICENSE"]."scripts": {
    "prepublish": "yarn build"
    }
Copy the code

Specify that the root directory for the library is lib/index.js

This command is used after user yarn add cuke-ui

import {Button} from 'cuke-ui'
Copy the code

You can think of it as the corresponding

import {Button} from './node_modules/cuke-ui/lib/index.js'
Copy the code

Write the relevant description and publish it

npm publish .
Copy the code

For beta, add a –tag

npm publish . --tag=next
Copy the code

5. Write the rest of the components

Other components, although their logic is different, but the routine is similar, after my efforts and struggle, completed the following components, the following focus to say some points worth saying

  • The Button Button
  • Alert Alert
  • Breadcrumb bread crumbs
  • Grid Layout
  • The Input fields
  • Message Message prompt
  • Modal dialog
  • Pagination pager
  • Tooltip Text prompts
  • TurnTable
  • WordPad Handwriting input board
  • MusicPlayer responsive MusicPlayer
  • Spin in the load
  • BackTop back to the top
  • The Progress bar
  • Tabs TAB
  • The Badge logo for
  • Dropdown Menu
  • The Drawer drawers
  • Radio Radio buttons
  • Container Container
  • Affix solid nail
  • Timeline Timeline
  • The Checkbox Checkbox
  • The Switch Switch
  • The Tag label
  • CityPicker City selection box
  • Collapse panel
  • Select Dropdown selector
  • DatePicker Calendar selection box
  • Notification Indicates the Notification Notification box
  • NumberInput NumberInput box
  • Article Steps Steps
  • The Upload to Upload
  • Calendar Calendar
  • Popover card
  • PopConfirm Indicates the bubble confirmation box
  • Card Card

5.1 Message Prompt Components

message, notification

The ideal state is to call directly from the API

import { message } from 'cuke-ui'
message.success('xxx')
Copy the code

You can easily do this with the class static property

static renderElement = (type, title, duration, onClose, darkTheme) = > {
    const container = document.createElement("div");
    const currentNode = document.body.appendChild(container);
    const _message = ReactDOM.render(
      <Message
        type={type}
        title={title}
        darkTheme={darkTheme}
        duration={duration}
        onClose={onClose}
      />,
      container
    );
    if (_message) {
      _message._containerRef = container;
      _message._currentNodeRef = currentNode;
      return {
        destroy: _message.destroy
      };
    }
    return {
      destroy: () => {}
    };
  };
  static error(title, duration, onClose, darkTheme) {
    return this.renderElement("error", title, duration, onClose, darkTheme);
  }
  static info(title, duration, onClose, darkTheme) {
    return this.renderElement("info", title, duration, onClose, darkTheme);
  }
  static success(title, duration, onClose, darkTheme) {
    return this.renderElement("success", title, duration, onClose, darkTheme);
  }
  static warning(title, duration, onClose, darkTheme) {
    return this.renderElement("warning", title, duration, onClose, darkTheme);
  }
  static loading(title, duration, onClose, darkTheme) {
    return this.renderElement("loading", title, duration, onClose, darkTheme);
  }
Copy the code

Treat each class’s static method as an API, and then, when calling the API, create a ‘div’ in the body and render it through the reactdom.render method

5.2 Pop-up Components

Modal

With the createPortal API in The React-DOM, it became surprisingly easy to write popover class components by mounting the DOM under the body via a so-called portal

    return createPortal(
      <>
        <div class="mask"/>
        <div class="modal"/>
      </>,
      document.body
  ) 
Copy the code

Tooltip

The Tooltip implementation has two options. One is directed absolutely to the parent element, which is less computational code but creates a problem

     <span
      ref={this.triggerWrapper}
      className={cls(`${prefixCls}-trigger-wrapper`)}
    >
      {this.props.children}
    </span>
Copy the code

If the parent element has a property like overflow:hidden, the tooltip might be partially intercepted, so use the second option and mount it on the body

    this.triggerWrapper = React.createRef();
    const {
      width,
      height,
      top,
      left
    } = this.triggerWrapper.current.getBoundingClientRect();
Copy the code

Get the current location information, dynamically assign to the current div, and finally bind a resize event to solve the problem of incorrect position after the window changes

  componentWillUnmount() {
    window.removeEventListener("click", this.onClickOutsideHandler, false);
    window.removeEventListener("resize", this.onResizeHandler);
    this.closeTimer = undefined;
  }
  componentDidMount() {
    window.addEventListener("click", this.onClickOutsideHandler, false);
    window.addEventListener("resize", this.onResizeHandler);
  }
Copy the code

5.3 Initialization animation flicker

When many components need to fade in and out of animation, I will bind two classes, corresponding to the fading in and out animation

 state = {
    visible: false
 }
 <div
    className={cls(`${prefixCls}-content`, {
      [`${prefixCls}-open`]: visible,
      [`${prefixCls}-close`]: ! visible, ["cuke-ui-no-animate"]: visible === null })} ref={this.wrapper} style={{ width, left, top }} > // xx.less &-open { animation: cuke-picker-open @default-transition forwards; } &-close { animation: cuke-picker-close @default-transition forwards; pointer-events: none; } .cuke-ui-no-animate { animation: none ! important; }Copy the code

This will cause a problem. When initialized, visible is false by default, so it will perform a close animation, which will cause blinking, so you just need to initialize with state set to null. Set the CSS to Animation: None when null

5.4 Unified visual style

For future maintenance and peels, you need to maintain a uniform set of variables, with all components referenced uniformly

//vars.less
@primary-color: #31c27c;
@warning-color: #fca130;
@error-color: #f93e3e;
@success-color: #35C613;
@info-color: #61affe;
@bg-color: #FAFAFA;
@border-color: #e8e8e8;
@label-color: # 333;
@default-color: #d9d9d9;
@loading-color: #61affe;
@font-color: rgba(0, 0, 0, .65);
@disabled-color: #f5f5f5;@disabled-font-color: fade(@font-color, 25%); @font-size: 14px; @border-radius: 4px; @default-shadow: 0 4px 22px 0 rgba(15, 35, 95, 0.12); @default-section-shadow: 0 1px 4px 0 rgba(15, 35, 95, 0.12); @default-text-shadow: 0 1px 0 rgba(0, 0, 0, .1); @picker-offset-top: 5px; @mask-bg-color: rgba(0, 0, 0, .5); // Responsebreakpoint @media-screen-xs-max: 576px; @mobile: ~"screen and (max-width: @{media-screen-xs-max})"; // @loading-time: 1.5s; @loading-opacity: .7; @animate-time : .5s; @animate: Cubic - Bezier (0.165, 0.84, 0.44, 1); @animate-type-easy-in-out: cubic-bezier(.9, .25, .08, .83); @default-transition: @animate-time @animate-type;Copy the code

5.5 use opportunely React. CloneElement

When writing components, they are often matched to problems that require matching, such as Collapse

<Collapse rightArrow>
    <Collapse.Item title="1">1</Collapse.Item>
    <Collapse.Item title="2">2</Collapse.Item>
    <Collapse.Item title="3">3</Collapse.Item>
</Collapse>
Copy the code

For example, there is a right tarrow property that tells each of the
arrows to the right. This is where you need to pass values to the child through the cloneElement

// collapse.js

   const items = React.Children.map(children, (element, index) => {
      return React.cloneElement(element, {
        key: index,
        accordion,
        rightArrow,
        activeKey: String(index),
        disabled: element.props.disabled, hideArrow: element.props.hideArrow
      });
    });
Copy the code

Each child component can set the corresponding class after receiving the parent component’s rightArrow property, similar to the implementation of Row, Col, and Timeline

i.

5.6 getDerivedStateFromProps

There are scenarios where state depends on a certain attribute of the props for many components

    <Tabs activeKey="1">
      <Tabs.TabPane tab="Option 1" key="1">
        1
      </Tabs.TabPane>
      <Tabs.TabPane tab="Option 2" key="2">
        2
      </Tabs.TabPane>
      <Tabs.TabPane tab="Option 3" key="3">
        3
      </Tabs.TabPane>
    </Tabs>
Copy the code

For example, the Tabs component above accepts an activeKey to render which option is currently present. The component might look like this

export default class Steps extends PureComponent {
  state = {
    activeKey: ~ ~ (this.props.activeKey || this.props.defaultActiveKey)
  };
   onTabChange = (a)= > {
     this.setState({ activeKey: key })
    }
  };
Copy the code

If the activeKey of props is updated, then state will not be updated. If the activeKey of props is updated, then state will not be updated. Therefore, you need to use the getDerivedStateFromProps life cycle to compare the activeKey of the props and the state after each props change. If the activeKey is different, you need to update it

 static getDerivedStateFromProps({ activeKey }, state) {
    if(activeKey ! == state.activeKey) {return {
        activeKey
      };
    }
    return null;
  }
Copy the code

6. Generate a website home page using ANTD-landing

After continuous efforts, the components were developed almost the same, but a cool home page like Ant. Design/index-CN was still needed. After a search, ANTD-Landing was found to drag and drop and visually build the home page of the website

Finally, all you need to do is hand-write some WebPack configurations, package them and publish them on the Github page

7. Conclusion

Yes, it is another antD-like library, which may not be meaningful. Through this component library, I learned a lot of knowledge points that I can’t get access to in ordinary times, and also realized the hard work of the framework and library author. It is really not easy and thankless, and I also got the star of the big guy, such as right-leaning. My colleagues are also very enthusiastic to help me with some Bug fix PR, anyway, this year’s learning goal has been completed, or happy, next January to start nest and Flutter, come on, pig