本文由 简悦 SimpRead 转码, 原文地址 juejin.cn
前言
最近两个月来博主在做一件事情,彻底重构了之前搭建的组件库,目前已经初具雏形,前往,在这个过程中,遇到了很多问题,这里分享下过程和解决方式,同时也会将我至今尚未想通的问题提出来,还望指点一二。
项目准备
之前的组件库是源于公司的需求,基于 tuya expo 的主题和所需要的组件进行定制开发的组件库,需求完成之后让我萌生了写个组件库的想法。
首先,我想写的组件库是一个实用性较大的组件库不是某个项目定制的,因此需要将项目的一些变量暴露出来,便于修改。其次需要支持深色模式以及紧凑模式。可以在项目运行时使用 js 修改主题色。后续将在主题站点推出主题选项
方案选择
- 首先想要在运行时修改主题色,能做到这一点的方案最合适的就是采用
css variables或者是css in js,css in js本质上就是通过 js 写法写的 css,存在于 js 的代码中,优势在于我们可以代码的任何时候修改其中的变量,并且不会影响其他组件,但是过多的 js 会增加运行时的消耗,而css variables则是需要通过 js 操作 dom 修改css variables挂载节点的 css 变量,当然对象能影响可以忽略不计,而且仅仅只在切换的时候执行不影响项目正常代码的性能。 由于项目的主题没有支持某一个组件单独进行主题修改(antd 5.x 支持),因此这里采用css variables + less的方式,更小的使用及更小的性能消耗。 - 色彩适配,例如一个按钮颜色可能是蓝色,hover 之后颜色浅一点的蓝色,这个时候用户将
primary-color修改为橙色,那么按钮 hover 的时候也需要是浅一点的橙色,这个时候我们不可能把所有情况的变量都暴露出来供用户消费,因此需要根据主色计算其余颜色。这里采用了@ant-design/colors的方式,根据颜色计算出对应渐变的是个色阶。 - 深色模式,切换为深色模式之后为了更加的舒适视觉效果,组件的颜色需要调整。这里同样是
@ant-design/colors,配置深色的背景颜色,根据背景色和原有颜色计算出深色模式下的颜色。
组件打包
令我头大的地方来了,首先是工具选择,显然 rollup 支持 esm cjs umd 等产物的构建,更适合库的打包,并且配置简单。但是了解到 webpack5.x 支持了构建 esm 的能力,出于对老朋友的偏爱最终还是选择了 webpack。
webpack 配置
首先常见的 less css 的配置就不多赘述了,可以参考 这一篇
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const getStyleLoaders = (preProcessor) => {
return [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
preProcessor === 'less-loader'
? {
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
},
},
}
: preProcessor,
].filter(Boolean);
};
const baseConfig = {
entry: './src/index.ts',
module: {
rules: [
{
oneOf: [
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders('less-loader'),
},
{
test: /\.(js|jsx)$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [['@babel/plugin-transform-runtime']],
presets: [['@babel/preset-env']],
ignore: ['node_modules/**'],
},
},
],
},
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
},
],
},
],
},
plugins: [
// 同事生成gzip的产物,在大部分情况下提供更小的体积
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
}),
],
optimization: {
minimize: true,
// 压缩的操作
minimizer: [
// 压缩css
new MiniCssExtractPlugin({
// 这里的目录在dist/[mode] 所以用 ../ 提出来
filename: '../style/rain-ui.min.css',
}),
// 压缩js
new TerserWebpackPlugin(),
],
},
resolve: {
extensions: ['.tsx', '.ts', '.json', '.js', '.jsx'],
},
mode: 'production',
devtool: 'source-map',
};umd
首先是打包生成 umd 的产物,umd的产物支持多种模块以及script 标签引入,体积稍大,通常作为备选的方案。在以上的 baseConfig 中添加 output,设置libraryTarget:umd
module.exports ={
output: {
path: path.resolve(__dirname, './dist/umd'),
filename: 'index.js',
libraryTarget: 'umd',
},
...baseConfig
}cjs
然后是 commonjs ,相比之下拥有更小的体积,在 webpack 不支持 ES Module 的日子里常常作为首选。
module.exports ={
output: {
path: path.resolve(__dirname, './dist/cjs'),
filename: 'index.js',
libraryTarget: 'commonjs',
},
...baseConfig
}esm
最后是新宠ES Module, 天然支持依赖分离,支持Tree-shaking, 基于 ESM 的能力,模块化交给浏览器端,不存在资源重复加载问题,这也是为什么基于 es 的构建工具会比 webpack 快得多。 webpack5 构建 esm 产物的时候需要设置experiments.outputModule=true 。
module.exports ={
output: {
path: path.resolve(__dirname, './dist/esm'),
filename: 'index.js',
libraryTarget: 'module',
chunkFormat: 'module',
},
experiments: {
outputModule: true,
},
}配置合并
webpack5 支持 导出一个数组,然后给数组每一项添加 name 最后采用 webpack --config-name esm 的方式执行。修改代码如下
module.exports = [
{
name: 'esm',
output: {
path: path.resolve(__dirname, './dist/esm'),
filename: 'index.js',
libraryTarget: 'module',
chunkFormat: 'module',
},
experiments: {
outputModule: true,
},
...baseConfig,
},
{
name: 'cjs',
output: {
path: path.resolve(__dirname, './dist/cjs'),
filename: 'index.js',
libraryTarget: 'commonjs',
},
},
{
name: 'umd',
output: {
path: path.resolve(__dirname, './dist/umd'),
filename: 'index.js',
libraryTarget: 'umd',
},
...baseConfig,
},
];package.json 添加命令
{
"build": "rm -rf ./dist && npm run build:esm & npm run build:umd & npm run build:cjs",
"build:esm": "webpack --config-name esm",
"build:cjs": "webpack --config-name cjs",
"build:umd": "webpack --config-name umd",
}问题
在开发一切顺利的时候将包打包好发布到 npm 之后,在项目中引用报错 Minified React error #321;
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: 1. You might have mismatching versions of React(反应) and the renderer (such(这样) as React(反应) DOM) 2. You might be breaking the Rules of Hooks(钩) 3. You might have more than one copy of React(反应) in the same app See reactjs.org/link(链接)/invali… for tips about how to debug and fix(修复) this problem.
根据官网的提示 :
- 不匹配的 react(反应) 和 react(反应)-dom 版本
- 违反了 hooks 规则
- 应用程序中拥有多个 react 副本
根据提示就想到需要使用 externals 将 react 和 react-dom 抽离出来,添加如下代码:
{
"externals": {
"react": {
commonjs: 'react',
commonjs2: 'react',
module: 'react',
amd: 'react',
root: 'React',
},
'react-dom': {
commonjs: 'react-dom',
commonjs2: 'react-dom',
module: 'react-dom',
amd: 'react-dom',
root: 'ReactDOM',
},
},
}问题到这里就解决了。
聊一聊组件库中 package.json 需要的修改
main: 引用文件的入口,通常是作为备选,一般设置为 umd 的构建产物。types/typing: 类型文件,在 ts 中使用库时会在types/typing指定的目录找类型文件。module: 在 ES Module 引用项目的时候指定的路径,优先级高于 main 设置为库构建的 esm 产物的目录。unpkg: CDN 方式下,引入当前 npm 包的链接。
另外作为一个 react 的组件库,其宿主环境肯定是有react 和 react-dom的,因此我们的组件库不需要安装这两个依赖, package.json 设置
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},总结
最后最后,欢迎小伙伴加入👏👏👏