preface

The thing is that when we work together with other front-end students, we often use the same component written by one of our friends. If it is in the same front-end package, it is needless to say. It is written under the Components folder and then we use it together.

But now if there are multiple front-end packages, we have to copy this component and its dependencies into another front-end package, and then use it. If this component is updated, we need to change the same code in several packages, which is very uncomfortable. Is there any solution?

Fortunately, we live in the big front end era of 2021, and the big guys have already solved this problem for us. NPM helps us unify the management of various components, plug-ins, libraries, etc…

Because I am a fan of React Hooks, I would like to create my own component library using React Hooks, which other people can use from NPM, and one person or a basic component team will maintain this component library.

Ant Design, a well-known component library, uses this strategy, so follow me to see how to implement your own component library based on it.

With what technology?

Let’s start by analyzing the techniques we should be using.

  1. We need to use React Hooks to develop components, so we need to ensure that our React version is inV16.8.0The React version has already been updatedV17.0.2, we prefer the latest version;
  2. As an engineering project, of course, to use the packaging tool, here is the most popularwebpack;
  3. It’s 2021. Any strict project should use itTypeScriptDevelopment, I said nothing wrong with it, hee hee ~
  4. (Optional) In terms of code format and style, we also have to make some restrictions, otherwise the development of the mess will be broken, of course, we also choose the most populareslint + prettierrc + stylelintrcOf course, you can use other tools to constrain your code style, or if you don’t want it, you can remove it completely.
  5. (Optional) Also very important, is the code submission, after writing the code, we need to commit to Git, if you do not have a constraint, you may have some very ugly code to commit to Git, so we need to choosehuskyImplement git commit constraints so that ugly code cannot be committed to Git, as explained below.

After the analysis is complete, let’s start our journey to build the wheel

Initialize the project

First of all, we will prepare an empty folder named cos-Design. Of course, you don’t have to call it cos-design. This name is already occupied by me in NPM, so you need to change it, otherwise you will be reminded that you already have an NPM package with this name when you publish it.

Create package. Json

The first step is to open the terminal in the cos-design folder and do YARN init (make sure you have yarn locally, of course you can choose CNPM or NPM). Following the prompts, we get a package.json file with basic configuration.

$ yarn init
Copy the code
{" name ":" cos - design ", "version" : "1.0.0", "description" : "cos - design", "main" : "index. Js", "scripts" : {" test ": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/jiaxiantao/cos-design.git" }, "keywords": [ "cos", "cos-design" ], "author": "jiaxiantao", "license": "MIT", "bugs": { "url": "https://github.com/jiaxiantao/cos-design/issues" }, "homepage": "https://github.com/jiaxiantao/cos-design#readme" }Copy the code

Document Explanation:

  • Package name is the name of the NPM package you created, which will be used when installed after release. NPM specifies that the package name should start with a lowercase letter. Import App from ‘your-module’;

  • Version refers to the package version. The package version must be updated before each release. Otherwise, the package version must comply with the semantic specification. The semantic version number is divided into three digits: 0.0.0. Major version number: Should be upgraded when major changes are made or when there are many incompatible changes to the API. Minor version: Upgrade this version when some features are added or optimized. Revision number: Upgrade this version when a bug or minor change is corrected;

  • The main entry path, which is indexed by the main path in package.json when the user uses the package.

  • License Open source license agreement, other people violate this agreement for legal purposes;

  • Repository Associated git repository;

  • Keywords show your project keywords in NPM;

Add the LICENSE file and readme. md file

  1. LICENSE
MIT LICENSE

Copyright (c) 2021-present jiaxiantao

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copy the code
  1. README.md

Install base dependencies

  1. Install the React framework base dependencies
$ yarn add react react-dom -D
Copy the code
  1. Because you’re using typescript, install typescript dependencies
$ yarn add typescript @types/react @types/react-dom  -D
Copy the code
  1. Install the WebPack base dependency
$ yarn add webpack webpack-cli webpack-dev-server -D
Copy the code
  1. Install the loader required for webPack packaging
$ yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react css-loader less less-loader style-loader ts-loader -D
Copy the code
  1. Install the plugin required for WebPack packaging
$ yarn add clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin -D
Copy the code

At this point, our dependencies should be installed, but of course we can put these instructions together for convenience

$ yarn add react react-dom typescript @types/react @types/react-dom webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-react css-loader less less-loader style-loader ts-loader clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin -D
Copy the code

After executing the above command, our package.json will have more of these dependencies

// package.json { // ... "DevDependencies" : {" @ Babel/core ":" ^ 7.15.0 ", "@ Babel/preset - env" : "^ 7.15.0", "@ Babel/preset - react" : "^ 7.14.5", "@ types/react" : "^ 17.0.19", "@ types/react - dom" : "^ 17.0.9", "Babel - loader" : "^ 8.2.2", "the clean - webpack - plugin" : "^ 4.0.0 - alpha. 0" and "CSS - loader" : "^ 6.2.0", "HTML - webpack - plugin" : "^ 5.3.2", "less" : "^ 4.4.1", "less - loader" : "^ 10.0.1", "mini - CSS - extract - the plugin" : "^ 2.2.2", "react" : "^ 17.0.2", "the react - dom" : "^ 17.0.2", "style - loader" : "^ 3.2.1," "ts - loader" : "^ 9.2.5", "typescript" : "^ 4.3.5", "webpack" : "^ 5.51.1", "webpack - cli" : "^ 4.8.0 webpack - dev -", "server" : "^ 4.0.0"} / /... }Copy the code

Create a webpack. Config. Js

To use webpack we first create a webpack.config.js file, but we have some small requirements for Webpack;

  1. Support development environment and production environment to distinguish different configuration items;
  2. When packaged, it should be able to become a library, supporting the import {… } from ‘cos-design’ to introduce components;
  3. Support code separation, prevent packaging after only one file, resulting in an unusually large file;
  4. Other basic loader and plugin configurations;

Analysis and solution requirements:

  1. To distinguish the development environment from the production environment, we need to addcross-envThe dependence of
$ yarn add cross-env -D 
Copy the code

Next, configure package.json and add instructions for packaging and compiling

{/ /... "scripts": { "start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js", "build": "cross-env NODE_ENV=production webpack --config webpack.config.js", "build:module": "cross-env NODE_ENV=production BUILD_MODE=module webpack --config webpack.config.js" }, // ... }Copy the code

Note here that I added the BUILD_MODE=module environment variable in the Build: Module directive to tell the current environment when webpack is packaged that I want to package as an NPM module.

  1. In order toPack into a library, I went to check the official webpack documentation, it is very clear, the main core is to configure the webpack export, and toumdOf courseWay to exportMany of them are described in detail on webpack’s official website. Based on my requirements and considerations, I finally choose to export them in the form of UMD, with the following codes:
module.exports = { // ... output:{ // ... + library: {+ name: 'cosDesign', + type: 'umd'}}Copy the code
  1. To achieve code separation, the Webpack website is also very detailed

SplitChunksPlugin is recommended for code separation. Although SplitChunksPlugin is a plug-in, it does not need to be introduced separately, because it is a built-in plug-in of Webpack. There are many configuration items for this plug-in, which can be configured according to specific needs. Only the simplest configuration is shown here

module.exports = { // ... + optimization: {+ splitChunks: {+ chunks: 'all', + name: 'chunk' Setting it to false will keep the chunk's name the same, so it won't change the name unnecessarily. This is the recommended value for a build in production. +}, +},};Copy the code

4. The configuration of other loaders and plug-ins can be added according to their own needs. I need to support typescript, so I need ts-Loader; If less is supported, less-loader is required. Babel-loader for browser old syntax compatibility; The plugin also uses the simple three necessary plug-ins htmlWebpackPlugin, CleanWebpackPlugin, MiniCssExtractPlugin;

After the analysis is complete, the following webpack.config.js configuration items are obtained

/* * @descripttion: webpack configuration item * @version: 1.0.0 * @author: jiaxiantao * @date: 2021-08-24 17:47:29 * @lasteditors: jiaxiantao * @LastEditTime: 2021-09-07 23:35:45 */ const path = require("path"); const htmlWebpackPlugin = require("html-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const NODE_ENV = process.env.NODE_ENV || false; const BUILD_MODE = process.env.BUILD_MODE || false; const isProduction = NODE_ENV === "production" || false; const isModuleBuild = BUILD_MODE === "module" || false; module.exports = { mode: isProduction ? "production" : "development", entry: { index: isModuleBuild ? ". / SRC/components/index. The TSX ":". / SRC/index. Benchmark ",}, / / if you set the entry to an array, then only the last of the array will be exposed as library output: {filename: "[name].js", path: path.resolve(__dirname, isModuleBuild ? "lib" : "dist"), library: { name: "cosDesign", type: },}, // implementation of code separation optimization: {splitChunks: {chunks: "all", name: "chunk", // The name of the chunk to be split. Setting it to false will keep the chunk's name the same, so it won't change the name unnecessarily. This is the recommended value for a build in production. }, }, devtool: "inline-source-map", module: { rules: [ { test: /\.js$/, loader: "babel-loader", exclude: /node_modules/, }, { test: /\.(tsx|ts)?$/, loader: "ts-loader", exclude: /node_modules/, }, { test: /\.css$/, exclude: /node_modules/, use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { modules: { mode: "local", localIdentName: "Cos - [path] [name] __ [local] - [5] hash: base64:",,}},},]}, / / solution using CSS modules antd style effect not {test: /\.css$/, // Exclude: [/ SRC /], use: [MiniCssExtractPlugin.loader, "css-loader"], }, { test: /\.less$/, exclude: /node_modules/, use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { modules: { mode: "local", localIdentName: "cos-[path][name]__[local]--[hash:base64:5]", }, }, }, "less-loader", ], }, ], }, resolve: { extensions: [".tsx", ".ts", ".js", ".json"], alias: { "@": path.resolve(__dirname, "src"), }, }, devServer: { static: { directory: path.join(__dirname, "dist"), }, compress: true, port: 4000, open: true, }, externals: isModuleBuild ? { react: "react", "react-dom": "react-dom", } : {}, plugins: [new MiniCssExtractPlugin({// similar to the options in webpackoptions. output // All options are optional filename: "css/[name].css", }), !isModuleBuild && new htmlWebpackPlugin({ template: "public/index.html", }), isProduction && new CleanWebpackPlugin(), ].filter(Boolean), };Copy the code

Note:

The important thing to note here is to configure externals. Make sure react and react-dom are set as external dependencies, otherwise you will get the following error

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons...

It will prompt you to encounter an hooks error, and then there are three possibilities:

  1. Your React and React DOM versions may not match.
  2. You may have broken the rules of Hook.
  3. You may have multiple copies of React in the same application.

Comparing webpack and React data, the third mistake is most easily ignored and made by us. An application has multiple copies. Here, we configure externals during packaging to tell the packaged file no longer has related dependencies. You just need to fetch dependencies from external packages that use it, so you don’t have multiple dependencies;

{peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies: peerDepndencies The following code

{... "PeerDependencies" : {" react ":" > = 16.12.0 ", "the react - dom" : "> = 16.12.0"}}Copy the code

If you look at the characteristics of peerDependencies, why do you set this up

  • If the user explicitly relies on the core library, the plug-in’speerDependencyThe statement;
  • If the user does not explicitly rely on the core library, follow the plug-inpeerDependenciesThe declared version installs the library into the project root directory.
  • When the version that the user depends on and the version that each plug-in depends on are not compatible with each other, an error will be reported for the user to repair by himself.

Create tsconfig. Json

To use typescript, tsconfig.json must be configured to support typescript. We installed the dependencies in the previous step, so we can generate tsconfig.json directly by executing the following command

$ tsc --init
Copy the code

This will add a tsconfig.json file to our root directory

Next we will modify tsconfig.json to look like this

{"compilerOptions": {"outDir": "./lib/", // output folder "sourceMap": true, "noImplicitAny": "Declaration ": true, // Generates'.d. s' files "module":" ES6 ", "target": "ES5 "," JSX ": "react", "allowJs": true, "moduleResolution": "Node ", // used to select the module resolution policy, Have a "node" and "classic" two types "allowSyntheticDefaultImports" : true, / / used to specify allow no default default import export modules "esModuleInterop" : }, "include": ["./ SRC /*", "./index.d.ts" // configured.d.ts files], "exclude": ["node_modules", "lib", "es"] }Copy the code

Here we find that we also need an index.d.ts file that writes typescript’s global type support. I’ve configured a few media files and modularity support for less and sass.

declare module '*.less';
declare module '*.png';
declare module '*.jpg';
declare module '*.sass';

declare module '*.svg' {
    export const ReactComponent: React.FunctionComponent<
        React.SVGProps<SVGSVGElement>
    >;
    const src: string;
    export default src;
}
Copy the code

See this article for a detailed introduction to tsconfig.json

After this configuration, we run the Webpack package at the same time as typescript compilation, writing typescript content to lib files.

Creating the React component

First we create a SRC directory and write index.tsx as the entry file. Then we create a components folder under SRC and put some custom components in it, just like the following directory.

Here I will use the clock component created using canvas as the reference component I shared last time, and then introduce this component in index.tsx to test whether it works

Run yarn start on the console. The effect is as follows

If you find the component working, you can publish it first

Publish the NPM package and use it

Release NPM package

To publish an NPM package, you need to register an account on the NPM website. Second, run NPM Publish on the application console we just set up

$ npm publish
Copy the code

You will be asked to fill in the version to be released

We just need to superimpose a small version on the current version, then enter your own NPM account password to publish a NPM package, if the NPM has the name of the package you created, the release will fail, need to create a new package name;

After the successful launch, we will be able to see our newly released package on the NPM website.

Use NPM package

Now that we have published the package we just created to NPM, the next step must be to use it. It’s as simple as adding dependencies under a new project as ant-Design does

$ yarn add cos-design
Copy the code

We then introduce the relevant CSS files and corresponding components as we would normally do with Ant-Design

import * as React from "react"; import * as ReactDOM from "react-dom"; import "cos-design/lib/css/index.css"; import { CanvasClock } from "cos-design"; ReactDOM. Render (< div > < div > hello, cos - deisgn - use! </div> <CanvasClock /> </div>, document.getElementById("root") );Copy the code

Let’s see how it worksIt was nice to successfully introduce the clock component from the NPM package into our project.

Engineering optimization

Excellent students would not want our project to be so simple, of course we need more support to make our NPM project better development, such as Lint checking, code submission, and more rich webPack capabilities. But space is limited, so I’ll leave optimizing webPack to the next installment, which starts with lint checking and code submission restrictions.

Example Configure ESLint + Prettier

First install dependencies

$ yarn add eslint prettier -D
Copy the code

Configuration. The prettierrc

{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 120,
  "singleQuote": true,
  "trailingComma": "none",
  "bracketSpacing": true,
  "semi": true,
  "htmlWhitespaceSensitivity": "ignore"
}
Copy the code

Create an ESLint configuration file:

$ npx eslint --init
Copy the code

Run the preceding command and perform operations as prompted to create the configuration file.eslintrc.js.

module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 12,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
};
Copy the code

Also, remember to install Prettier in VSCode with the ESLint plugin, and you can format files automatically when you save them.

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
Copy the code

Resolve the conflict between ESLint and Prettier

When you add extra configuration rules for ESLint and Prettier to your project, there are often rules that conflict.

For example, ESLint configuring validation to use some preconfigured style would conflict with the Prettier configuration, causing ESLint to detect formatting problems and throw an error.

The conflict can be resolved by introducing eslint-plugin-prettier and eslint-config-prettier.

  • Eslint-plugin-prettier: To set the rule for prettier into an ESLint rule
  • Eslint-config-prettier: Disables a rule in ESLint that conflicts with Prettier

Priority can be formed by: Prettier Configuration rule > ESLint Configuration rule

Using a prettier

$ yarn add eslint-plugin-prettier eslint-config-prettier -D
Copy the code

Add the Prettier plug-in to.eslintrc.js

module.exports = { // ... Extends: [' plugin: prettier/it / /... / / add a prettier plug-in], / /... }Copy the code

At this point, you can quickly format your code with the eslint –fix command.

Configuration stylelint

The stylelint check is added to check for style irregularities in our project. Again, we need to install dependencies first

$ yarn add stylelint -D
Copy the code

Add stylelint. Json

{
  "extends": ["stylelint-config-standard", "stylelint-config-prettier"],
  "rules": {
    "declaration-empty-line-before": null,
    "no-descending-specificity": null,
    "selector-pseudo-class-no-unknown": null,
    "selector-pseudo-element-colon-notation": null
  }
}
Copy the code

See stlelint official introduction for more configuration items

Adopt husKY and Lint-staged specifications

Even if ESLint and Prettier were integrated into a project to help validate code, some members of the team might find the restrictions too cumbersome and write code in their own style, or disable them altogether and send the code to the repository as soon as it’s finished.

In order to normalize the project code, some restrictions should be made to prevent code that has not been detected and fixed by ESLint from being submitted to the online repository to keep the repository code normative.

To do this, you need to use a Git Hook, which performs ESLint –fix (ESLint –fix) on your code when you locally execute Git commit instructions, which is achieved via Husky + Lint-staged.

  • Husky: Git Hook is a tool that can be set to trigger Git commands at various stages (pre-commit, commit-msg, pre-push, etc.)
  • Lint-staged: Run linters on files temporarily stored in Git
  1. Install dependencies
$ yarn add husky lint-staged -D
Copy the code
  1. Add husky and Lint-staged configuration items to package.json
{/ /... "husky": { "hooks": { "pre-commit": "npm run lint-staged" } }, "lint-staged": { "**/*.less": "stylelint --syntax less", "**/*.{js,jsx,tsx,ts,less,md,json}": [ "prettier --write", "git add" ], "**/*.{js,jsx}": "npm run lint-staged:js", "**/*.{js,ts,tsx}": "npm run lint-staged:js" }, // ... }Copy the code

The following steps are performed after the configuration is complete

  1. huskyWill be called before you submitpre-commitHook, executelint-stagedIf the code does not conformprettierThe configured rules will be formatted.
  2. And then useeslintIf there is a rule that does not meet the rules and cannot be fixed automatically, the submission will be stopped.
  3. If they pass, they add the code to the stage, and thencommit.

At the end

At this point, we will talk about a simple and relatively standard project set up, of course, a good project scalability is far more than this, for example, the optimization of Webpack has a lot of noteworthy places, as well as for lint checking, tsconfig.json configuration items are very diverse, These things require little by little accumulation and attention.

This article will focus on creating hooks components, publishing them to NPM and referencing them, but it will be very light on engineering.

In the future, I have a good study of these engineering content and then separate out a good talk, thank you for reading, let us work together, good learning front end ~

The project address

  • Making the address
  • NPM package address

Refer to the article

  • Webpack creates a library

  • Learn about peerDependencies

  • Why are Hooks for third-party components wrong?