前言
事情是这样的,在工作的时候,我们和别的前端同学一起协作的时候,很多时候要用到某个小伙伴写的同一个组件,如果在同一个前端包里,那没得说,都是写到 components 文件夹下面,然后大家一起用;
但是现在如果有多个前端包,我们就不得不需要重新把这个组件以及其依赖复制到另一个前端包里面,然后用起来,如果这个组件被更新了,我们就需要改好几个包里面的同一段代码,非常难受,那有没有什么办法呢?
非常幸运的是,我们生活在2021大前端时代,大佬们已经帮我们解决了这个问题,那就是 npm,它帮我们统一管理了各种组件,插件,库等等。。。
由于个人崇尚React Hooks,所以希望使用 React Hooks 创建一套属于自己的组件库,然后其他小伙伴可以从 npm 上拉下来用,最终由一个人或者一个基础组件团队来维护这一套组件,也就是大家常说的造轮子,想想就很酷,有没有。
知名组件库 Ant Design 便是采用了这样的策略,那么跟随我看看怎么参考它实现自己的组件库的吧!
用什么技术?
首先分析一下我们应该要用到的技术。
- 我们要用 React Hooks 开发组件,那么必须保证自己的 React 版本在
V16.8.0以上,对 React 版本比较了解的同学应该都知道,当然,现在 React 版本已经更新到V17.0.2了,我们优先选择最新的版本; - 作为一个工程化项目,当然要使用打包工具啦,这里选用最热门的
webpack; - 都 2021 了,任何一个严格的项目都应该用
TypeScript开发,我说的没毛病吧,嘻嘻~ - (可选)关于代码格式和风格,我们也要做点限制吧,不然到时候开发乱起八糟就玩断了,当然我们也是选用最热门的
eslint + prettierrc + stylelintrc来约束自己的代码风格,当然,你也可以使用其他工具约束,或者,你不想要它,也可以完全去除,大不了代码写的丑了一点,哈哈哈; - (可选)还有一点也是非常重要的,就是代码提交,我们在写完代码之后,要提交到 git 上,如果不做约束的话,就可能会把一些很‘丑’的代码提交到 git 上,因此我们需要选用
husky进行 git 提交约束,这样那些‘丑’的代码就没办法提交到 git 上面了,后面会详解怎么配置。
在分析完成之后,开始我们的造轮子之旅吧~
初始化项目
首先我们准备一个空的文件夹,例如名字就叫做 cos-design,当然你可以不叫这个名字,这个名字已经被我在npm 里占用了,所以你得换一个名字,不然到时候发布的时候会提示你已经有这个名字的 npm 包了。
创建 package.json
第一步我们要以这个 cos-design 这个文件夹打开终端,然后执行 yarn init(首先保证自己的本地拥有 yarn,当然你可以选择 cnpm 或者 npm),根据提示信息我们就得到了一个具有基本配置的 package.json 文件。
$ yarn init
{
"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"
}
文件解释:
-
package name 为你创建的npm包的名称,在发布后被安装使用即该名字,npm规定包名首字母需要为小写。如
import App from 'your-module';; -
version 即为包版本,每次发布前都需要更新包版本,否则会失败,包版本应该遵守语义化规范。语义化版本号分为三位
0.0.0。主版本号:当进行了大都改动或者对api有很多不兼容修改时应该进行版本号升级。次版本号:增加了部分特性或者优化时升级该版本。修订号:当修改了项目bug或者小的改动时升级该版本; -
main 入口路径,当用户使用包的时候,会根据该入口也就是
package.json的main中的路径来进行索引; -
license 开源许可协议,别的人违反了这个协议用来打官司用的;
-
repository 关联的git仓库;
-
keywords 会在npm中展示你的项目关键字;
加入 LICENSE 和 README.md 文件
- 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.
- README.md
安装基本依赖
- 安装 react 框架基础依赖
$ yarn add react react-dom -D
- 由于要使用 typescript,所以要安装一下 typescript 相关
$ yarn add typescript @types/react @types/react-dom -D
- 安装 webpack 基本依赖
$ yarn add webpack webpack-cli webpack-dev-server -D
- 安装 webpack 打包所需的 loader
$ yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react css-loader less less-loader style-loader ts-loader -D
- 安装 webpack 打包所需的 plugin
$ yarn add clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin -D
至此,我们的依赖应该安装完成了,当然为了方便快捷我们可以把这些指令放到一起执行
$ 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
执行完成上述命令之后,我们的 package.json 就会多上这些依赖
// 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",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^10.0.1",
"mini-css-extract-plugin": "^2.2.2",
"react": "^17.0.2",
"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"
}
// ...
}
创建 webpack.config.js
要使用 webpack 我们首先要创建一个 webpack.config.js 文件,但是我们对 webpack 有一些小小的要求;
- 支持开发环境与生产环境区分不同的配置项;
- 打包之后要能够成为库,支持以 import { ... } from 'cos-design' 的形式引入组件;
- 支持代码分离,防止打包之后只有一个文件,导致文件异常庞大;
- 其他一些基本的 loader 和 plugin 的配置;
分析解决要求:
- 为了区分开发环境与生产环境,我们需要添加
cross-env的依赖
$ yarn add cross-env -D
接下来再配置 package.json,添加打包编译的指令
{
// ...
"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"
},
// ...
}
这里要注意的是,我在 build:module 的指令加入了 BUILD_MODE=module 的环境变量,以便在webpack 打包的时候告诉当前环境我要打包成一个 npm 模块。
- 为了打包成一个库,我去查阅了 webpack 的官方文档,上面讲的很清楚,主要的核心是配置 webpack 的打包出口,并以
umd的形式导出,当然导出的方式有很多中,在 webpack 的官网中讲的非常细致,经过我的需求和考量,最终选择了使用 umd 的形式导出,代码如下:
module.exports = {
// ...
output:{
// ...
+ library: {
+ name: 'cosDesign',
+ type: 'umd' // 以库的形式导出入口文件时,输出的类型,这里是通过umd的方式来暴露library,适用于使用方import的方式导入npm包
}
}
}
- 要实现代码分离,webpack 的官网上同样讲的很细致
看完之后,官方很明显是推荐让我们使用 SplitChunksPlugin 来实现代码分离,它虽然作为插件,但是并不需要单独引入,因为它属于 webpack 的内置插件,关于这个插件的配置项也很多,可以根据具体需要去配置,这里只演示最简单的配置方式
module.exports = {
// ...
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ name: 'chunk' // 拆分 chunk 的名称。设为 false 将保持 chunk 的相同名称,因此不会不必要地更改名称。这是生产环境下构建的建议值。
+ },
+ },
};
4.关于其他的一些 loader 和插件的配置可以根据自己的需要添加,我这里需要支持 typescript,所以需要有 ts-loader;支持 less,则需要使用 less-loader;babel-loader 用于浏览器旧语法兼容;而插件也就使用简单的三个必要插件 htmlWebpackPlugin,CleanWebpackPlugin,MiniCssExtractPlugin;
在分析完成之后也就得到了如下的 webpack.config.js 的配置项
/*
* @Descripttion: webpack 配置项目
* @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.tsx" : "./src/index.tsx",
}, //如果你将 entry 设置为一个 array,那么只有数组中的最后一个会被暴露成库
output: {
filename: "[name].js",
path: path.resolve(__dirname, isModuleBuild ? "lib" : "dist"),
library: {
name: "cosDesign",
type: "umd", // 以库的形式导出入口文件时,输出的类型,这里是通过umd的方式来暴露library,适用于使用方import的方式导入npm包
},
},
// 实现代码分离
optimization: {
splitChunks: {
chunks: "all",
name: "chunk", // 拆分 chunk 的名称。设为 false 将保持 chunk 的相同名称,因此不会不必要地更改名称。这是生产环境下构建的建议值。
},
},
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]--[hash:base64:5]",
},
},
},
],
},
// 解决使用css modules时antd样式不生效
{
test: /\.css$/,
// 排除业务模块,其他模块都不采用css modules方式解析
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({
// 类似于 webpackOptions.output 中的选项
// 所有选项都是可选的
filename: "css/[name].css",
}),
!isModuleBuild &&
new htmlWebpackPlugin({
template: "public/index.html",
}),
isProduction && new CleanWebpackPlugin(),
].filter(Boolean),
};
注意:
这里有值得注意的一点是配置 externals,一定要把 react和react-dom 设置成外部依赖,否则你将会遇到以下错误
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...
会提示你遇到 hooks 的错误,然后有三种可能性,也就是react官方解释上的这三种:
- 你的 React 和 React DOM 可能版本不匹配。
- 你可能打破了 Hook 的规则。
- 你可能在同一个应用中拥有多个 React 副本。
对比了 webpack 和 react 的相关资料,最容易被我们所忽略和犯的错误也就是第三个了,一个应用中拥有多个副本,这里我们打包的时候配置 externals 就是告诉打包之后的文件不再自带相关的依赖,只需要从外部使用它的包中去取依赖即可,这样就不会导致多重依赖的问题出现了;
当然,在实践之后发现,尽管配置了这个 externals ,依然会报上述 hooks 的错误,在挣扎了许久之后,另外发现还需要在 package.json 中把 react 和 react-dom 设置成 peerDepndencies ,代码如下
{
...
"peerDependencies": {
"react": ">=16.12.0",
"react-dom": ">=16.12.0"
}
}
我们可以看看 peerDependencies 的特点,也就明白为什么要这么设置了
- 如果用户显式依赖了核心库,则可以忽略各插件的
peerDependency声明; - 如果用户没有显式依赖核心库,则按照插件
peerDependencies中声明的版本将库安装到项目根目录中; - 当用户依赖的版本、各插件依赖的版本之间不相互兼容,会报错让用户自行修复;
创建 tsconfig.json
由于要使用 typescript,那么必须要配置 tsconfig.json 来实现对 typescript 的支持,刚刚上一步我们已经安装了相关依赖,因此直接执行以下命令就能生成 tsconfig.json
$ tsc --init
这样子我们的根目录就多了一个 tsconfig.json 文件
接下来我们要把 tsconfig.json 修改成下面这样
{
"compilerOptions": {
"outDir": "./lib/", // 输出文件夹
"sourceMap": true,
"noImplicitAny": true, // 如果没有设置明确的类型会报错,默认值为false
"declaration": true, // 生成 `.d.ts` 文件
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node", // 用于选择模块解析策略,有"node"和"classic"两种类型
"allowSyntheticDefaultImports":true, // 用来指定允许从没有默认导出的模块中默认导入
"esModuleInterop" : true // 通过导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性
},
"include": [
"./src/*",
"./index.d.ts" //配置的.d.ts文件
],
"exclude": ["node_modules", "lib", "es"]
}
这里我们发现还需要一个index.d.ts 文件,这个文件是写入 typescript 的全局类型支持,我这里简单配置了一些媒体以及 less,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;
}
具体的对于 tsconfig.json 的介绍,可以参考这篇文章
这样配置之后,我们在运行 webpack 打包的时候就会同时执行 typescript 编译,将 typescript 的内容写入 lib 文件中。
创建 react 组件
首先我们创建一个 src 目录,然后在里面写 index.tsx 作为入口文件,然后 src 目录下创建 components 文件夹,里面放入自定义的一些组件,就像下面的目录一样。
在这里我就用上一次分享的使用 canvas 创建的时钟组件作为参考组件,然后在 index.tsx 中引入这个组件,测试是否正常
在控制台运行 yarn start 效果如下
发现组件正常,那么接下来就可以先发布一下看看啦~
发布 npm 包并使用
发布 npm 包
要想发布一个 npm 包,首先我们需要在 npm 的官网 注册一个自己的账号;
第二步,在我们刚刚建立的应用控制台运行 npm publish
$ npm publish
这时候会让你填写本次要发布的版本
我们只需要在当前的版本上叠加一个小版本即可,接下来输入自己的 npm 的账号密码即可发布一个 npm 包了,如果 npm 上已经有你创建的包名的话,会发布失败,需要创建一个全新的包名;
发布成功后,在 npm 官网上就可以看到我们刚刚发布的包了。
使用 npm 包
既然我们已经把刚刚创建的包发布到 npm 上了,那么下面必然是要用它,很简单,我们只需要像 ant-design 一样,先在一个新的工程下面添加依赖
$ yarn add cos-design
然后就像我们平时使用 ant-design 一样引入相关的 css 文件和对应的组件
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")
);
那让我们看看效果吧~
非常nice,就这样成功的把 npm 包中的时钟组件引入到我们的项目里面了。
工程优化
优秀的同学肯定不希望我们的工程如此简陋,当然需要更多的支持让我们的 npm 工程得到更好的发展,例如 lint 检查,代码提交,已经更多 webpack 的丰富能力。但是篇幅有限,关于 webpack 的优化我将会放到下一期再详述,这一期先加入 lint 检查和代码提交限制。
配置 ESLint + Prettier
首先安装依赖
$ yarn add eslint prettier -D
配置 .prettierrc
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": true,
"htmlWhitespaceSensitivity": "ignore"
}
创建 eslint 配置文件:
$ npx eslint --init
执行以上命令,并根据终端提示操作即可创建配置文件 .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": {
}
};
另外,记得在 VSCode 中也安装 Prettier 和 ESLint 插件搭配使用,并且可以设置保存文件时自动格式化。
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
解决 ESLint 和 Prettier 的冲突
通常大家会在项目中根据实际情况添加一些额外的 ESLint 和 Prettier 配置规则,难免会存在规则冲突情况。
比如 ESLint 配置采用某些预置风格的校验,而导致与 Prettier 配置发生冲突,导致 ESLint 检测出格式问题,从而抛出错误提示。
这时候可以通过引入 eslint-plugin-prettier 和 eslint-config-prettier 解决冲突。
- eslint-plugin-prettier:将 Prettier 的规则设置到 ESLint 的规则中
- eslint-config-prettier:关闭 ESLint 中与 Prettier 中会发生冲突的规则
通过以上插件可形成优先级:Prettier 配置规则 > ESLint 配置规则。
使用 prettier
$ yarn add eslint-plugin-prettier eslint-config-prettier -D
在 .eslintrc.js 中添加 Prettier 插件
module.exports = {
// ...
extends: [
// ...
'plugin:prettier/recommended' // 添加 prettier 插件
],
// ...
}
至此,即可通过 eslint --fix 命令快速格式化代码了。
配置 stylelint
加入 stylelint 这个检查是为了检查我们项目中样式不规范的问题,同样,我们需要先安装依赖
$ yarn add stylelint -D
添加 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
}
}
更多的配置项请看 stlelint 官方介绍
采用 husky 和 lint-staged 规范代码
就算在项目中集成了 ESLint 和 Prettier 帮助我们进行代码校验,但团队可能会有些人觉得这些条条框框的限制很麻烦,依旧按自己的一套风格来写代码,或者干脆禁用掉这些工具,开发完成就直接把代码提交到了仓库。
为了项目代码的规范化,需要做一些限制,防止未通过 ESLint 检测和修复的代码提交到线上仓库,从而保证仓库代码的规范性。
为此,需要用到 Git Hook,在本地执行 git commit 指令时,就对代码进行 ESLint 的检测和修复(eslint --fix),这里通过 husky + lint-staged 实现。
- husky:Git Hook 工具,可以设置在 git 各个阶段(pre-commit、commit-msg、pre-push 等)触发我们的命令
- lint-staged:在 git 暂存的文件上运行 linters
- 安装依赖
$ yarn add husky lint-staged -D
- 在 package.json 中添加 husky 和 lint-staged 配置项
{
// ...
"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"
},
// ...
}
在配置完成之后会按照下面步骤执行
husky会在你提交前,调用pre-commit钩子,执行lint-staged,如果代码不符合prettier配置的规则,会进行格式化;- 然后再用
eslint的规则进行检查,如果有不符合规则且无法自动修复的,就会停止此次提交。 - 如果都通过了就会把代码添加到stage,然后
commit。
结尾
至此,我们就讲一个简单的较为规范的工程建立完毕啦,当然,一个好的工程可扩展性远不止于此,比如对于 webpack 的优化就有很多值得注意的地方,还有对于 lint 检查,tsconfig.json 的配置项就非常丰富多样,这些东西都需要一点一点积累和注意。
本文主要还是讲怎么创建 hooks 组件并发布到 npm 中并引用,对于工程化的东西讲的还是很粗浅。
未来,在我好好学习了这些工程化的内容之后再单独细分出来好好讲讲,感谢您的阅览,让我们一起共勉,好好学习前端~
项目地址
参考文章