1. Install the component library

Executing installation Commands

$ npx create-react-app echo-rui  --typescript
Copy the code

2. Component library configuration ESLint

  • Configure ESlint new.eslintrc.jsonfile
{
  "extends": "react-app"
}
Copy the code

Create a.vscode/settings.json file

{
  "eslint.validate": [
    "javascript"."javascriptreact",
    { "language": "typescript"."autoFix": true },
    { "language": "typescriptreact"."autoFix": true}}]Copy the code

3. Introduce dependencies

Use className in the component: github.com/jedWatson/c… Executing installation Commands

$ npm install classnames -D
$ npm install @types/classnames -D
$ npm install node-sass -D
Copy the code

[BTN -${btnType}] [BTN -${btnType}] [BTN -${btnType}]

// btn, btn-lg, btn-primary
const classes = classNames('btn', className, {
   [`btn-${btnType}`]: btnType,
   [`btn-${size}`]: size,
   'disabled': (btnType === 'link') && disabled
})
Copy the code

4. Write components

  • newsrc/components/Button/button.tsx
import React,{FC,ButtonHTMLAttributes,AnchorHTMLAttributes} from "react";
import classnames from "classnames"; // Button sizeexport type ButtonSize = "lg" | "sm";

export type ButtonType = "primary" | "default" | "danger" | "link"; Interface BaseButtonProps {/** customizing className */ className? : string; /** Disable Button */ disabled? : boolean; /** Set Button size */ size? : ButtonSize; /** Set Button type */ btnType? : ButtonType; children: React.ReactNode; /** Required when btnType is link */ href? : string; } / / and settype NativeButtonProps = BaseButtonProps &
  ButtonHTMLAttributes<HTMLElement>;
typeAnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>; // Partial: typescript global functions that make all attributes optionalexport typeButtonProps = Partial<NativeButtonProps & AnchorButtonProps>; // React-docgen-typescript loader can only use FC, not react. FCexportconst Button: FC<ButtonProps> = (props) => { const { disabled, size, btnType, children, href, className, ... resetProps } = props; const classes = classnames("echo-btn", className, {
    [`echo-btn-${btnType}`]: btnType,
    [`echo-btn-${size}`]: size,
    "echo-button-disabled": btnType === "link" && disabled,
  });
  if (btnType === "link" && href) {
    return( <a href={href} className={classes} {... resetProps}> {children} </a> ); }else {
    return (
      <button className={classes} disabled={disabled} {...resetProps}>
        {children}
      </button>
    );
  }
};

Button.defaultProps = {
  disabled: false,
  btnType: "default"};export default Button;



Copy the code
  • newsrc/components/Button/index.tsx
import Button from "./button";

export default Button;
Copy the code
  • newsrc/components/Button/_style.scss
@import ".. /.. /styles/variables/button";
@import ".. /.. /styles/mixin/button";
.echo-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,
    $btn-border-radius
  );
  box-shadow: $btn-box-shadow;
  cursor: pointer;
  transition: $btn-transition;
  &.echo-button-disabled
  &[disabled] {
    cursor: not-allowed;
    opacity: $btn-disabled-opacity;
    box-shadow: none;
    > * {
      pointer-events: none;
    }
  }
}

.echo-btn-lg {
  @include button-size(
    $btn-padding-y-lg,
    $btn-padding-x-lg,
    $btn-font-size-lg,
    $btn-border-radius-lg
  );
}

.echo-btn-sm {
  @include button-size(
    $btn-padding-y-sm,
    $btn-padding-x-sm,
    $btn-font-size-sm,
    $btn-border-radius-sm
  );
}

.echo-btn-primary {
  @include button-style($primary.$primary.$white);
}

.echo-btn-danger {
  @include button-style($danger.$danger.$white);
}

.echo-btn-default {
  @include button-style(
    $white.$gray- 400.$body-color,
    $white.$primary.$primary
  );
}

.echo-btn-link {
  font-weight: $font-weight-normal;
  color: $btn-link-color;
  text-decoration: $link-decoration;
  box-shadow: none;
  &:hover {
    color: $btn-link-hover-color;
    text-decoration: $link-hover-decoration;
  }
  &:focus {
    text-decoration: $link-hover-decoration;
    box-shadow: none;
  }
  &:disabled,
  &.echo-button-disabled {
    color: $btn-link-disabled-color; pointer-events: none; }}Copy the code
  • newstyles/variables/button.scssfile
@import "./common"; // Basic button properties$btn-font-weight: 400;
$btn- padding - y: 0.375 rem! default;$btn- padding - x: 0.75 rem! default;$btn-font-family: $font-family-base ! default;$btn-font-size: $font-size-base ! default;$btn-line-height: $line-height-base ! default; . .Copy the code
  • Create a styles/mixin/button.scss ‘file
@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
  padding: $padding-y $padding-x;
  font-size: $font-size;
  border-radius: $border-raduis;
}

@mixin button-style(
  $background.$border.$color// lghTEN,sass built-in function, than$backgroundThe color should be 7.5% lighter$hover-background: lighten($background, 7.5%),
  $hover-border: lighten($border, 10%),
  $hover-color: $color)... .Copy the code
  • insrc/styles/index.scssIntroduce component styles into the file
// The index file mainly introduces styles for all components. // No need to write _, which is a way of writing sass to tell sass that these styles are not packaged into CSS, but can only be imported, which is also a modular // button style @import".. /components/Button/style";

Copy the code
  • newsrc/styles/index.scssfile
// The index file mainly introduces styles for all components. // No need to write _, which is a way of writing sass to tell sass that these styles are not packaged into CSS, but can only be imported, which is also a modular // button style @import".. /components/Button/style"; .Copy the code

5. Delete unnecessary files and reference components

  • deletesrc/App.css + src/logo.svg + src/index.css + src/App.test.js + serviceWorker.tsfile
  • Modify theApp.tsxfile
import React from "react";
import "./styles/index.scss";
import Button, { ButtonType, ButtonSize } from "./components/Button/button";

function App() {
  return (
    <div className="App">
      <Button>hello</Button>
      <Button disabled>hello</Button>
      <Button btnType="primary" size="sm">
        hello
      </Button>
      <Button
        btnType="danger"
        size="lg"
        onClick={() => {
          alert(111);
        }}
      >
        hello
      </Button>
     <Button
        btnType="link"
        href="http://www.baidu.com"
        target="_blank"
      >
        hello
      </Button>
      <Button disabled btnType="link" href="http://www.baidu.com">
        hello
      </Button>
    </div>
  );
}
export default App;
Copy the code
  • Modify thesrc/index.tsxfile
export { default as Button } from "./components/Button";
Copy the code

6. Run the project

Execute the command

$ npm start
Copy the code

Visit the project and see that the Button component is successful!

# # 7. Unit testing new SRC/Button/Button. The test. The TSX composite file

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

describe("Button component", () => {
  it('the default Button', () => {
    const testProps: ButtonProps = { onClick: jest.fn(), } const wrapper = render(<Button {... testProps}>hello</Button>); const element = wrapper.getByText('hello')as HTMLButtonElement; // If the element is rendered in the document expect(element).tobeinthedocument (); // Expect (element.tagname).toequal ("BUTTON"); // Check if there is a class name expect(element).tohaveclass ("echo-btn-default");
     expect(element).not.toHaveClass("echo-disabled"); // Trigger the click event fireEvent.click(element); expect(testProps.onClick).toHaveBeenCalled();
    expect(element.disabled).toBeFalsy();
  })
  it("Test for passing in different attributes", () => {
    const testProps: ButtonProps = {
      btnType: "primary",
      size: "lg",
      className: "test-name"}; const wrapper = render(<Button {... testProps}>hello</Button>); const element = wrapper.getByText("hello") as HTMLButtonElement;
    expect(element).toBeInTheDocument();
    expect(element).toHaveClass("echo-btn-primary");
    expect(element).toHaveClass("echo-btn-lg");
    expect(element).toHaveClass("test-name");
  });

  it("Test the presence of btnType link and href.", () => {
    const testProps: ButtonProps = {
      btnType: "link",
      href: "http://www.baidu.com"}; const wrapper = render(<Button {... testProps}>Link</Button>); const element = wrapper.getByText("Link") as HTMLAnchorElement;
    expect(element).toBeInTheDocument();
    expect(element.tagName).toEqual("A");
    expect(element).toHaveClass("echo-btn-link");
  });

  it("Test disabled condition", () => {
    const testProps: ButtonProps = {
      onClick: jest.fn(),
      disabled: true}; const wrapper = render(<Button {... testProps}>Disabled</Button>); const element = wrapper.getByText("Disabled") as HTMLButtonElement;
    expect(element).toBeInTheDocument();
    expect(element.disabled).toBeTruthy();
    fireEvent.click(element);
    expect(testProps.onClick).not.toHaveBeenCalled();
  });
});
Copy the code

Execute the command

$ npm test
Copy the code

You can see that the unit test passed successfully!

##8. Component libraries are loaded on demand

  • Install dependencies
$npm install node-cmd -D
Copy the code
  • newbuildScss.jsfile
const cmd = require("node-cmd");
const path = require("path");
const fs = require("fs");
const entryDir = path.resolve(__dirname, "./src/components");
const outputDir = path.resolve(__dirname, "./dist/components");
function getScssEntry() {
  let entryMap = {};
  fs.readdirSync(entryDir).forEach(function (pathName) {
    const entryName = path.resolve(entryDir, pathName);
    const outputName = path.resolve(outputDir, pathName);
    let entryFileName = path.resolve(entryName, "_style.scss");
    let outputFileName = path.resolve(outputName, "style/index.css");

    entryMap[pathName] = {};
    entryMap[pathName].entry = entryFileName;
    entryMap[pathName].output = outputFileName;
  });

  return entryMap;
}
const entry = getScssEntry();
let buildArr = [];
for (const key in entry) {
  const promise = new Promise((resolve, reject) => {
    cmd.get(`npx node-sass ${entry[key].entry} ${entry[key].output}`, function (
      err,
      data,
      stderr
    ) {
      if (err) {
        reject(err);
        return;
      }
      console.log("the current working dir is : ", data);
      fs.writeFileSync(
        path.join(__dirname, `./dist/components/${key}/style/css.js`),
        "import './index.css'"
      );
      resolve();
    });
  });
  buildArr.push(promise);
}

Promise.all(buildArr)
  .then(() => {
    console.log("build success");
  })
  .catch((e) => {
    console.log(e);
  });


Copy the code
  • new_babel.config.jsDist /components style: echo-rui (); echo-rui (); echo-rui (); echo-rui (); The CSS type is loaded. The current project only has CSS, not less, so write CSS here. If you need to configure LESS, see comments
module.exports = {
  presets: ["react-app"],
  plugins: [
    [
      "import",
      {
        libraryName: "echo-rui", 
        camel2DashComponentName: falseTurn, whether you need / / hump short-term camel2UnderlineComponentName:false, // Do you want the camel to underline libraryDirectory:"dist/components",
        style: "css"},], // ["import"{/ /"libraryName": "antd", / /"libraryDirectory": "es", / /"style": "css" // `style: true//}]],};Copy the code

⚠️⚠️⚠️ Important Note: During development, there must be a _style. SCSS style file under each component directory, even if it is an empty file. Otherwise, an error will be reported when importing on demand

##9. Storybook document generation

  1. Initialize the storyBook
$ npx -p @storybook/cli sb init
Copy the code
  1. Add dependencies and plug-ins
$ npm install @storybook/addon-info --save-dev
Copy the code
  1. Add the NPM script
 "scripts": {..."storybook": "start-storybook -p 9009 -s public"."build-storybook": "build-storybook -s public"
  },
Copy the code
  1. Configure storybook to support typescript
$ npm install react-docgen-typescript-loader -D
Copy the code
  1. Add the storybook configuration file
  • new.storybook/webpack.config.jsfileshouldExtractLiteralValuesFromEnumThe: storybook automatically expands properties of type type when it crawls component properties.propFilter: Filters out the source of attributes that do not need to be climbed.
module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.tsx? $/, use: [ { loader: require.resolve("babel-loader"),
        options: {
          presets: [require.resolve("babel-preset-react-app")]
        }
      }, 
      {
        loader: require.resolve("react-docgen-typescript-loader"),
        options: {
          shouldExtractLiteralValuesFromEnum: true,
          propFilter: (prop) => {
            if (prop.parent) {
              return! prop.parent.fileName.includes('node_modules')}return true}}}]}); config.resolve.extensions.push(".ts".".tsx");

  return config;
};
Copy the code
  • new.storybook/style.scssfile
// This file is mainly the configuration of the storyBook document styleCopy the code
  • new.storybook/config.tsxFile configuration plug-in and the files that need to be loaded
import { configure,addDecorator,addParameters } from '@storybook/react';
import { withInfo } from '@storybook/addon-info'


import '.. /src/styles/index.scss'
import './style.scss'
import React from 'react'

const wrapperStyle: React.CSSProperties = {
  padding: '20px 40px'} const storyWrapper = (stroyFn: Any) => (<div style={wrapperStyle}> <h3> Component demo </h3> {stroyFn()} </div>) addDecorator(storyWrapper) addDecorator(withInfo) addParameters({info: { inline:true, header: false}})


const loaderFn = () => {
    const allExports = [];
    const req = require.context('.. /src/components'.true, /\.stories\.tsx$/);
    req.keys().forEach(fname => allExports.push(req(fname)));
    return allExports;
  };

configure(loaderFn, module);

Copy the code
  1. Create a new one under each component directory.stories.tsxClosing document
  • New under button componentbutton.stories.tsxfile
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';

import Button from './button'

const defaultButton = () => (
    <div>
        <Button onClick={action('default button')}>default button</Button>
    </div>
)

const buttonWithSize = () => (
    <div>
        <Button size='lg' btnType='primary' onClick={action('lg button')}>lg button</Button>
        <Button className='ml-20' size='sm' btnType='danger' onClick={action('sm button')}>sm button</Button>
    </div>
)


const buttonWithType = () => (
    <div>
        <Button onClick={action('danger button')} btnType='danger'>danger button</Button>
        <Button onClick={action('primary button')} className='ml-20' btnType='primary'>primary button</Button>
        <Button onClick={action('link')} className='ml-20' btnType='link' href='https://www.baidu.com/'>link</Button>
    </div>
)

const buttonWithDisabled = () => (
    <div>
        <Button onClick={action('disabled button')} btnType='danger' disabled={true}>disabled button</Button>
        <Button onClick={action('unDisabled button')} className='ml-20' btnType='primary'>unDisabled button</Button>
    </div>
)

// storiesOf('Button component'. The module) / /. AddDecorator (withInfo) / /. AddParameters ({/ / info: {/ / text: ` / / / / ~ ~ ~ this is the default component js / / const a = 12 / / ~ ~ ~ / / `, // inline:true
//     }
// })
//   .add('the default Button', defaultButton)
//   .add('Different size buttons', buttonWithSize,{info:{inline:false}})
//   .add('Different types of buttons', buttonWithType)

storiesOf('Button Button', module)
    .addParameters({
        info: {
            text: `
        ## Reference method
        ~~~js
        import {Button} from ecoh-rui
        ~~~
        `
        }
    })
    .add('the default Button', defaultButton)
    .add('Different size buttons', buttonWithSize)
    .add('Different types of buttons', buttonWithType)
    .add('Disabled Button',buttonWithDisabled)

Copy the code

7. Run the command

$ npm run storybook
Copy the code

You can see it on the terminal

http://localhost:9009/

10. Typescript compilation configuration

Create a new tsconfig.build.json file


{
  "compilerOptions": {// Output path"outDir": "dist"// Package module specifications"module": "esnext"// Build the target"target": "es5"// Generate the definition file d.ts"declaration": true."jsx": "react"// The module introduces the policy"moduleResolution": "Node"// allow import React from'react'This guide package"allowSyntheticDefaultImports": true}, // The directory to compile"include": ["src"], // No need to compile"exclude": ["src/**/*.test.tsx"."src/**/*.stories.tsx"."src/setupTests.ts"]}Copy the code

11. Package. json Configuration

  • Move the dependency package from Dependencies to devDependencie

Why would you do that? React and react-dom are also used during development, so we cannot delete the react and react-dom. Other dependencies that are not related to the published component library, such as Storybook, need to be added to devDependencies

// Run NPM start and NPM runtestAnd NPM Run Storybook find everything ok, have fun!!"dependencies": {},
Copy the code
  • Added configuration related to NPM publishing
"description": "react components library"."author": "echo"."private": false// Main entrance"main": "dist/index.js"// Module entry"module": "dist/index.js"// Type file declaration"types": "dist/index.d.ts"."license": "MIT", // Keywords"keywords": [
    "React"."UI"."Component"."typescript"], // Home your github address"homepage": "https://github.com/... /echo-rui"// Warehouse address"repository": {
    "type": "git"."url": "https://github.com/... /echo-ruii"}, // If the file needs to be uploaded, the default is. Gitignore"files": [
    "dist"].Copy the code
  • Check your code before submitting it to Git. Here we use the husky tool
"husky": {
    "hooks"Before submitting code with git"pre-commit": "npm run test:nowatch && npm run lint"}},"scripts": {..."lint": "eslint --ext js,ts,tsx src --fix --max-warnings 5"."test:nowatch": "cross-env CI=true react-scripts test".Copy the code
  • Add packaged publishing configuration
"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-ts && npm run build-css && node ./buildScss.js"."prepublishOnly": "npm run lint && npm run build".Copy the code

After you run the package command, the package is successfully packaged according to the configuration.

12. Publish to NPM

All we need is a launch.

  • Complete the readme.md document. Just write two sentences about it
  • Create a new.npmignore file in the root directory with the same content as.gitignore:
Copy the code

Finally, execute NPM login to login to NPM account, and then execute NPM publish publish. Of course, the premise is that you have a NPM account, no words to register a bar, very easy, and then search your NPM package name whether someone uses, if there is a change.

Conclusion: Perfect!