前言
组件库打包现在有很多方案,如果想着省事可以使用开箱即用的vue-cli以及vite的库模式,当然vue-cli针对性较强,只适用vue项目。vite可以支持vue和react,具体可以根据需求进行配置,使用方式也很简单,管网都有配置不做具体描述。使用这些工具总有一种知其然不知其所以然的感觉,并且在有特殊化的需求时有点束手束脚,对打包过程没有一个深入的了解。基于此我看了一下ant-design的打包流程,决定自己实现一个简易版打包工具,本文以react为例。
本次要实现的目标如下:
- 导出 umd/commonjs/ES module等三种格式文件。
- 导出类型定义。
- 支持按需加载。
- 打包发布npm
初始化项目
项目整体是使用pnpm进行管理, 执行 pnpm init 初始化项目
然后添加 pnpm-workspace.yaml文件 内容如下:
packages:
- 'packages/**'
- 'examples'
然后在根目录创建packages 目录和example目录
packages文件夹目录
此目录为组件开发及打包目录,主要打包配置都在这里。 接下来一点一点完成packages下的配置
packages项目初始化
- 执行同样的命令
pnpm init初始化项目 - 安装webpack 和webpack-cli
- 安装typescript
- 添加打包入口文件index.js
- 添加webpack.config.js配置
安装webpack主要是为了打包输出umd格式的文件
commonjs 和esmodule的文件使用gulp打包生成
打包入口文件
打包入口index.js文件很简单
components文件夹就是所有组件存放的位置
目前只是一段测试代码
webpack配置
webpack.config.js
const path = require('path')
const config = [{
mode: 'development', // 开发模式
devtool: 'source-map',
entry: {
antd: './index.js'
},
output: {
path: path.resolve('dist'),
filename: '[name].js', //打包后的文件
library: 'antd', //打包后名字
libraryTarget: 'umd',
clean: true
},
resolve: {
extensions: ['.ts', '.tsx', '.js', 'jsx', '.json'],
},
}]
module.exports = config
接下来尝试打包是否能成功,
在package.json中添加命令 "build": "webpack"
执行打包命令后
编写组件升级配置
现在尝试使用react写一个简单的button组件,进行打包。
首先安装react 和 react-dom
components下新建button组件
components下的index中统一暴露出组件
export { default as Button } from './button'
export type { ButtonProps } from './button';
...继续放置其他组件
接下来就是尝试打包了,在打包之前我们还得增加个配置,就是babel-loader来处理react编写的组件
webpack.config.js
const path = require('path')
const config = [{
...
module: {
rules: [
{
test: /\.(j|t)sx?$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
]
},
}]
module.exports = config
babel.config.js
module.exports = {
presets: [
[
'@babel/preset-react' // 支持react jsx语法
],
[
'@babel/preset-env', // 根据browsers配置进行语法降级
{
modules: 'commonjs',
targets: {
browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 11']
}
}
]
],
plugins: [
[
'@babel/plugin-transform-typescript',
{
isTSX: true
}
],
['@babel/plugin-transform-runtime', {
useESModules: true,
},] //提取一些编译运行时帮助方法
]
}
需要安装 @babel/preset-react babel-loader @babel/plugin-transform-typescript @babel/core @babel/plugin-transform-runtime @babel/runtime @babel/preset-env
webpack.config.js
const path = require('path')
const config = [{
...
externals: {
react: 'react',
"react-dom": 'react-dom',
},
...
}]
module.exports = config
然后重新打包
在根目录下创建examples目录初始化一个react项目,然后修改package.json 添加一个配置
"dependencies": {
...
"@jasen/antd": "workspace:*", // @jasen/antd 为组件库中package.json 定义的包名
...
注意:依赖需要使用pnpm安装
App.js
import { Button } from '@jasen/antd'
function App() {
return (
<div>
<Button>我是一个按钮</Button>
</div>
);
}
export default App;
效果如下:
按钮成功展示,接下来添加样式
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const config = [{
module: {
rules: [
...
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // 抽离css
{
loader: 'css-loader',
options: {
sourceMap: true
}
},
{
loader: 'postcss-loader', //加厂商前缀
options: {
postcssOptions: {
plugins: ['autoprefixer'],
},
sourceMap: true
}
},
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true
},
sourceMap: true
}
}
]
},
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
})
]
重新打包后在examples项目中入口文件引入css(组件库打包代码会同步到example下的项目中,不用重新安装)
目前为止已经简单实现了umd格式的输出,接下来完成输出es和commonjs
安装并配置gulp
在组件库项目根目录下新建 gulpfile.js 配置如下
const gulp = require('gulp')
const path = require('path')
const rimraf = require('rimraf')
const ts = require('gulp-typescript')
const babel = require('gulp-babel')
const merge2 = require('merge2')
const through2 = require('through2');
const transformLess = require('./transformLess');
const { compilerOptions } = require('./tsconfig.json')
const tsconfig = {
noUnusedParameters: true, // 不能有未使用的变量
noUnusedLocals: true, // 不能有未使用的本地变量
strictNullChecks: true, //严格的null检查
target: 'es6',// 编译目标
moduleResolution: 'node', // 模块的查找规则
declaration: true, //是否生成声明文件
allowSyntheticDefaultImports: true, //允许默认导入
...compilerOptions
}
const babelconfig = require('./babel.config')
const source = [
'components/**/*.tsx',
'components/**/*.ts',
'typings/**/*.d.ts'
];
function getProjectPath(filePath) {
return path.join(process.cwd(), filePath)
}
const base = getProjectPath('components')
const libDir = getProjectPath('lib')
const esDir = getProjectPath('es')
gulp.task('compile-es', (done) => {
compile(false).on('finish', done)
})
gulp.task('compile-lib', (done) => {
compile().on('finish', done)
})
function compile(modules) {
const targetDir = modules === false ? esDir : libDir
rimraf.sync(targetDir)
// =============================== LESS ===============================
let less = gulp
.src(['components/**/*.less'])
.pipe(
through2.obj(function (file, encoding, next) {
const cloneFile = file.clone(); //复制一份打包后可以保留less源文件
const content = file.contents.toString().replace(/^\uFEFF/, '');
cloneFile.contents = Buffer.from(content);
const cloneCssFile = cloneFile.clone();
this.push(cloneFile);
if (file.path.match(/(\/|\\)style(\/|\\)index\.less$/)) {
transformLess(cloneCssFile.contents.toString(), cloneCssFile.path)
.then(css => {
cloneCssFile.contents = Buffer.from(css);
cloneCssFile.path = cloneCssFile.path.replace(/\.less$/, '.css');
this.push(cloneCssFile);
next();
})
.catch(e => {
console.error(e);
});
} else {
next();
}
})
)
.pipe(gulp.dest(modules === false ? esDir : libDir));
const { js, dts } = gulp.src(source, { base }).pipe(ts(tsconfig))
const dtsStream = dts.pipe(gulp.dest(targetDir))
let jsStream = js
if (modules === undefined) {
jsStream = js.pipe(babel(babelconfig))
}
jsStream.pipe(gulp.dest(targetDir))
return merge2([less, jsStream, dtsStream])
}
gulp.task('compile', gulp.parallel('compile-es', 'compile-lib'))
新建 两个 task 'compile-es'和 'compile-lib' 顾名思义,就是最终打包生成es和lib格式文件
两个任务统一调用compile方法,通过参数区分不同的任务
compile方法中首先处理的是less文件,通过through2插件对文件流进行操作。其中,处理less文件的功能在transformLess内。transformLess其实也很简单,就是利用less、postcss 等插件处理less文件输出css文件流
transformLess.js
const less = require('less');
const path = require('path');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
function transformLess(lessContent, lessFilePath, config = {}) {
const { cwd = process.cwd() } = config;
const resolvedLessFile = path.resolve(cwd, lessFilePath);
const lessOpts = {
paths: [path.dirname(resolvedLessFile)],
filename: resolvedLessFile,
javascriptEnabled: true,
};
return less
.render(lessContent, lessOpts)
.then(result => postcss([autoprefixer]).process(result.css, { from: undefined }))
.then(r => r.css);
}
module.exports = transformLess;
以上配置是参考antd-tools进行简化
package.json中添加命令
"compile": "gulp compile"
安装相应插件进行打包
pnpm install gulp rimraf gulp-typescript gulp-babel merge2 through2 postcss autoprefixer -D
打包结果如下:
es输出:
commonjs输出:
接下来修改下package.json
回到examples项目log一下button组件
加载的是lib内的组件
组件库package.json再添加一个配置
再回到examples项目log一下button组件
这次加载的是es内的组件。
验证treeshaking功能
为了能区分button组件是否被打包,给button上打上标记
-
重新打包后查看打包后的源代码
-
把Button组件注释后再次打包
搜索不到button上标记,button组件并未打包进来
接下来换成commonjs模式再走一遍同样的流程 1.把组件库package.json文件修改下
这样就可以加载lib下面的包了
-
同样,把Button组件注释后再次打包
可以看到 页面上没有显示button,但是代码里还是将button组件打包进去了
按需加载
按需加载需要借助babel-plugin-import 插件
现在导出的组件只能支持js文件的按需加载,但是css文件还需要手动导入
- 在项目入口全量导入 dist下的antd.css (缺点:全量导入导致代码体积增加,许多不需要的组件css也一起导入)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@jasen/antd/dist/antd.css'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
- 在使用组件时手动导入(缺点:需要手动导入,相对麻烦)
import { Button } from '@jasen/antd'
import '@jasen/antd/es/button/style/index.css'
function App() {
return (
<div>
<Button type='primary'>我是一个按钮</Button>
</div>
);
}
export default App;
借助babel-plugin-import实现自动引入样式文件
注意这是examples项目内的babel配置
const path = require('path')
module.exports = {
presets: [
["@babel/preset-react", {
"runtime": "automatic" // 添加运行时帮助方法,可以在写组件时不引入react
}],
[
'@babel/preset-env',
{
modules: 'auto',
targets: {
browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 11']
}
}
]
],
plugins: [
[
'@babel/plugin-transform-typescript', // 支持typescript
{
isTSX: true
}
],
[
"import",
{
"libraryName": "@jasen/antd", customStyleName: function (p, f) {
return path.join('@jasen/antd/lib', p, 'style/index.css') //自定义css路径
}
}
]
]
}
看下最终生成的代码
样式文件成功插入到页面中
一个基本组件库打包功能至此已经完成啦,一个完善的组件库构建工具还有许多功能需要考虑:eslint,文档,单元测试,样式主题等一些列功能。
发布npm
发布之前先完善下package.json配置 ,添加files字段配置上传npm的文件白名单目录,以及license开源协议等
{
"name": "@jasen/antd",
"version": "1.0.2",
"description": "",
"scripts": {
"build": "webpack",
"compile": "gulp compile"
},
"keywords": [],
"author": "",
"license": "MIT",
"unpkg": "dist/antd.js",
"main": "lib/index.js",
"module": "es/index.js",
"typings": "lib/index.d.ts",
"files": [
"dist",
"lib",
"es"
],
"devDependencies": {...},
"dependencies": {...}
}
- 注册npm账号
- 终端登录npm执行npm login
- 输入注册的用户名密码登
- 执行npm publish