Webpack 5.x 相比于 Webpack 4.x 有了很多重大改进,有些改进对于我们使用它开发组件库有了更好的支持。
实现目标
eslint-loader
改用 eslint-webpack-plugin
Webpack 5.x 的
externals
配置有所增强,同时 Api 有所改动
针对 npm node_modules 中的公共依赖,通过传入正则排除以第三方package 开头的package:
module.exports = {
externals: [
/^react\/.+$/,
/^react-dom\/.+$/,
/^lodash\/.+$/,
/^@babel\/runtime\/.+$/
]
}
与 Rollup 一样通过 babel 来处理
webpack 可以直接通过 import 在 js 中使用图片资源,不需要引入插件
无论 js 中引入的图片还是 css 中引入的图片,都是通过以下方式最终打包进代码中的(inline 即不输出单独的图片 asset 而是集成到了 js 或者 css 中)
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
type: 'asset/inline', // 使用webpack 5 内置 loader
}
]
}
}
打包时,将输出的分割包按照组件维度分割,同样是使用 “多入口文件的方式来实现”,这样打包后文件会按照入口来分割,实现组件维度的拆分
module.exports = {
entry: {
index: { import: 'src/index.ts', filename: 'index.js },
Button: 'src/components/Button/index.tsx',
Alert: 'src/components/Alert/index.tsx',
},
output: {
filename: 'components/[name]/index.js',
library: {
type: 'commonjs2', // commonjs 是 只有 export=moduleName; commonjs2 还支持 module.exports = moduleName
}
}
}
Webpack 5. 对 entry 有了增强,每个入口点可以配置成一个对象,对象含有如下属性
module.exports = {
entry: {
index: {
import: 'src/index.ts', // 入口文件地址
filename: 'index.js, // 打包后输出文件地址,会覆盖 output.fileName 的配置
dependOn: ['Button', 'Alert'], // 将其他 entry 块设置为 dependOn 后 当前文件如果引用了这几个块,则不会将其打包进来,可以一定程度的实现代码复用
},
Button: 'src/components/Button/index.tsx',
Alert: 'src/components/Alert/index.tsx',
},
}
理论上我们可以通过设置 entry 入口文件的 dependOn 来解决输出模块间因相互引用导致的代码重复问题,但是,dependOn 必须在编译时明确的清除,当前入口点的依赖情况,不能做到一定程度的自动化,因此要解决输出代码重复的问题还是通过 webpack 的
externals
来实现: 我们可以把对内部组件间的引用都理解为不需要打包的外部模块,将其排除打包
module.exports = {
externals: [
/^react\/.+$/,
/^react-dom\/.+$/,
/^lodash\/.+$/,
/^@babel\/runtime\/.+$/,
({context, request}, callback) => {
// 避免 Button 重复打包
path.resolve(context, request) === path.join(__dirname, `src/components/Button`)
? callback(null, request) : callback();
}
]
}
# 输出后文件格式 dist/cjs/
.
├── components
│ ├── Alert
│ │ ├── index.js
│ │ └── style
│ │ └── index.css
│ ├── Button
│ │ ├── index.js
│ │ └── style
│ │ └── index.css
└── index.js
Webpack 上样式文件抽离是通过 mini-css-extract-plugin 这个插件来实现的
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: { sourceMap: true },
},
{
loader: 'postcss-loader',
options: { postcssOptions: { plugins: [autoprefixer({ env: BABEL_ENV })] } },
},
{ loader: 'less-loader', options: { lessOptions: { javascriptEnabled: true } } },
],
}
]
},
plugins: [
new MiniCssExtractPlugin({ filename: 'components/[name]/style/index.css'}),
]
}
理论上每个 entry 入口点都会有一个对应的 样式文件,除非这个入口点引入的模块中没有样式。输出指定目录结构的 样式文件很有必要,因为针对 cjs 组件库来讲,使用者要想实现按需加载一般会使用 babel-plugin-import 这个babel 插件会将对组件的引用,重新指向某个文件,同时引用其下的样式文件,以上配置输出的 组件中可能没有样式文件,这就需要我们后续通过其他方式生成了(例如 build 后通过脚本补齐一个空的样式文件)
)首先需要关闭掉 babel 配置中 @babel/preset-env 对于 es module 的转化功能(设置
modules: false
),将转化完全交给 webpack
webpack 提供 output.library 选项来配置输出 library
module.exports = {
mode: process.env.NODE_ENV || 'development', // 一般编译环境为 production
entry: entryFile,
output: {
path: path.resolve(__dirname, 'dist/umd/'),
filename: 'webpack-ui.production.min.js', // 或者 webpack-ui.development.js
globalObject: 'this', // 使得在 web node 都可用
umdNamedDefine: true, // 给生成的 umd 模块中的 amd 部分命名
library: {
type: 'umd', // 指定输出 的格式,与设置 libraryTarget 一个效果
name: 'WebpackUI', // umd 格式可以给浏览器script 直接引用,这里可以设置一个 浏览器环境的全局变量名称,浏览器环境可以直接通过 WebpackUI.Button 来使用组件
auxiliaryComment: '这里是插入的注释',
},
},
optimization: {
minimize: true, // production 环境的 压缩是自动开启的,如果需要输出非压缩文件要给这个设置为 false
minimizer: [
`...`, // webpack 5 提供用来继承已存在的 minimizer
new CssMinimizerPlugin(),
],
},
plugins: [
new MiniCssExtractPlugin({ filename: 'webpack-ui.production.min.css' }), // webpack-ui.development.css
]
};
一般来讲 cjs 或者 esm 的组件库输出,都会被第三方通过 npm install 来使用,所以这两种代码输出的可以是不压缩的代码,以保证一定的可读性;但是如果第三发使用也通过 webpack 作为打包工具,那么这里就会遇到问题
TypeError: __webpack_modules__[moduleId] is not a function
这个问题的原因是,一个使用 webpack 作为打包工具的第三方使用者,使用了我们通过 webpack 5.x 且没有被 terser 压缩过的 package 引起的 #11827,解决办法就是输出压缩后的代码(相应的,代码基本不可读)
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: {
index: {import: entryFile, filename: 'index.js'},
...componentEntryFiles // 以组件为维度的入口
},
output: {
path: path.resolve(__dirname, 'dist/cjs/'),
filename: 'components/[name]/index.js',
library: {
type: 'commonjs2',
auxiliaryComment: '这里是插入的注释kl',
},
},
plugins: [
new MiniCssExtractPlugin({ filename: 'components/[name]/style/index.css'}),
]
}
很遗憾,虽然 webpack 5.x 官网上说输出 esm library 已经支持 outputModule 但是经过试验,目前为止(webpack 5.15.0) 都不支持输出 esm 的 library, 会有如下的报错
经过调查 这个功能貌似还在开发中 #11827 只是预计将要在 Webpack 5.x 中实现,因此如果要实现输出 esm 类型的文件,我们不得不借助其他打包工具如 gulp rollup
根据 webpack 的 release v5.22.0 显示,esm 模块的输出正在逐步实现中
与 Rollup 类似的解决方案
虽然会用其他打包工具实现 输出 esm , 但是使用上是一样的
用户使用现代打包工具(Webpack, Rollup),引用我们的组件库时,会查找对应
"module": "dist/es/index.js"
字段的ES模块代码,只要他的打包工具开启了 tree-shaking 功能(Webpack production 模式自动开启, Rollup 自动开启)即可实现 tree-shaking 带来的 JS 按需加载能力
CSS 方面,用户需要在它们项目的入口引入
import '@mjz-test/rollup-ui/dist/es/style/index.css';
全量的样式。
用户需要使用 babel-plugin-import 作为按需加载的 babel 工具,其实现原理是将对组件的引用,重新指向所下载组件库目录下的某个文件,来实现“定向”的引用,顺便也可以将 css 也定向的引用
// 用户的 babel.config.js
module.exports = function () {
return {
plugins: [
[
'import', // 使用 babel-plugin-import
{
libraryName: '@mjz-test/rollup-ui',
libraryDirectory: 'dist/cjs',
camel2DashComponentName: false,
style: true,
},
'rollup-ui',
]
]
}
}