DLL是"动态链接库"的意思,假设项目中用了 react 、lodash,这些静态资源可以被单独打包,且独立于项目。这样每次主项目打包的时候,都可以直接使用过打包好的 react、lodash,节省了性能。
原理:
- 单独一份ddl config文件单独对react、lodash打包,会将打包产物和一个manifest文件放在某个目录下,manifest文件。
- 打包的产物是以全局变量的方式导出模块的,manifest文件用来告诉使用方react、lodash打包后生成的全局变量叫什么,
- 使用方在遇到react、lodash时候,就无需再次打包,将使用react和lodash模块的地方,根据manifest文件的指引去通过全局变量访问。
简单的例子:
单独开一个dll config文件 webpack.dll.config.js
js
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
// 将react和lodash以及你想要预打包的静态文件打包成一个名为vendor的bundle
// 这里没有路径,直接就一个react,就是去打包纯node_modules下的文件
// 当然我们没有loader,webpack默认只能打包js和json文件。如果这里出现一个css入口文件,必然会发生打包错误。
vendor: ['react', 'react-dom', 'lodash']
},
output: {
// 将生成的bundle生成在dll目录下
path: path.join(__dirname, 'dll'),
filename: '[name]_[hash].dll.js',
// 因为dll对打包生成的模块的导出方式是全局变量的方式,需要通过library告诉webpack应该将模块导出维护在哪个全局变量上。这个全局变量叫做 '[name]_[hash]',实际编译后即 vendor_<hash>
library: '[name]_[hash]'
},
plugins: [
new CleanWebpackPlugin(),
// 使用DllPlugin,这是webpack内置的plugin
new webpack.DllPlugin({
// 将生成的bundle生成在dll目录下,并且生成的manifest文件应该是[name]-manifest.json的形式
path: path.join(__dirname, 'dll', '[name]-manifest.json'),
// 表明这些模块的导出的全局变量名,必须和 output.library一致
name: '[name]_[hash]'
})
]
};
然后是主体项目的webpack文件 webpack.config.js
js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
// 应用方必须配合使用 DllReferencePlugin 才能完成工作
new webpack.DllReferencePlugin({
// 告诉manifest文件在哪
manifest: require('./dll/vendor-manifest.json')
}),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 需要注意的是,我们需要手动将react、lodash这样被预打包生成的bundle引入在index.html中
// 可以选择直接手动通过<scripe>标签的方式引入,但这样不如直接使用 AddAssetHtmlPlugin 享受工程化带来的福利,比如 publicPath,以及index.html删除后还能自动引入
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dll/vendor_*.dll.js'),
publicPath: './'
})
]
};
随后可以通过往package.json中加一些npm指令。在发布部署(CICD)时先npm run dll,然后npm run build
打包后的运行时状态(伪代码):
vendor.dll.js 伪代码,大概的原理
js
var vendor_2e8f1b = (function(modules) {
// ▼ 模块缓存机制
var installedModules = {};
// ▼ 模块加载实现 (类似__webpack_require__)
function __dll_require__(moduleId) {
if(installedModules[moduleId])
return installedModules[moduleId].exports;
var module = installedModules[moduleId] = {
id: moduleId,
loaded: false,
exports: {}
};
modules[moduleId].call(module.exports, module, exports, __dll_require__);
module.loaded = true;
return module.exports;
}
// ▼ 暴露全局变量
return __dll_require__(0);
})({
/*! ▼ 预编译的第三方库模块集合 */
0: function(module, exports) {
// React核心实现
module.exports = window.React = ... // 完整React库代码
},
1: function(module, exports) {
// ReactDOM实现
module.exports = window.ReactDOM = ...
},
2: function(module, exports) {
// lodash工具库
module.exports = window._ = ...
}
});
index.bundle.js 中访问lodash
js
// 在bundle中动态绑定DLL模块,通过访问直接变量的方式来读取react、lodash。也就是说打包后的react和lodash的源码将不会出现在 index.bundle.js 中。
__webpack_require__.d(exports, "lodash", () => window.vendor_2e8f1b[2]);
Manifest 文件
js
{
"name": "vendor_2e8f1b",
"content": { // 提供给使用方的预打包bundle信息
"./node_modules/react/index.js": {
"id": 1, // react的id为1,使用方的 import { useState } from 'react'; 打包后会通过 window.vendor_2e8f1b[1].useState 访问
"buildMeta": {"providedExports": true}
},
"./node_modules/lodash/lodash.js": {
"id": 2,
"buildMeta": {"usedExports": ["default"]}
}
}
}
学习Dll时思考的一些问题:
Q1:如果使用dll将react、lodash等第三方包预打包出去了,主项目对react和lodash是不是就无法启用treeshaking特性了?
A1: 是的,因为在主项目中直接绕过了react和lodash的打包,使用了现成的打包产物,并采用全局变量的方式去访问由dll分离出去的包,自然无法对react和lodash走一遍打包编译的过程,也就没有静态分析的过程,也就是无法treeshaking了。要知道treeshaking影响的是主项目打包后的产物,而不是主项目外的打包产物。webpack总不能会根据manifest文件去反向分析dll预打包的产物,然后修改dll下的产物吧。
Q2: dll能否解决Qiankun微前端资源共用问题?
A2:是的,dll可以解决微前端资源共用问题,因为在微前端中,对公共资源的处理有externals方案。externals原理也是将公共依赖的模块抛出打包工程,根据配置使用cdn链接,进而通过全局变量式使用外置模块。而dll原理类似,并且dll无需独立部署外置模块的cdn了,所以也可以。
大体思路流程:

这样的缺点就是子应用没法脱离主应用独立运行。当然如果有子应用需要独立运行的需求,可以有两种办法:
- 无论主应用还是子应用都通过addAssetHtmlPlugin实现将cdn下的所有js文件引入在html模板中。
- 或在子应用运行时,率先判断是否为独立运行状态,然后通过动态创建script标签的方式将所有cdn下的js文件引入,待script标签都加载完成后执行后续逻辑。
通过AddAssetHtmlPlugin实现对cdn下的文件
js
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 准备CDN链接数组
const cdnAssets = [
'https://cdn.example.com/library/vendor.react.js',
'https://cdn.example.com/library/vendor.lodash.js
];
module.exports = {
// ... 其他webpack配置
plugins: [
new HtmlWebpackPlugin(),
// 使用 AddAssetHtmlPlugin 并传入CDN链接数组
new AddAssetHtmlPlugin({
files: cdnAssets
})
]
};
Q3:dll能否替代external解决方案?
A3:还是不能完全替代external方案的。虽然两者具备相似的原理。

Q4:我的第三方库包含vue3,我希望转换为es5,但dll中的打包是es6的,那该怎么办?重新用dllplugin打包一个es5版本的么?
A4:是的。思路是需要把webpack.dll.config.js中增加babel-loader相关配置,来使得dll下的产物也是es5的。下面是代码仅描述意思,仅供参考:
js
module.exports = {
module: {
rules: [
{
test: /.js$/, // 匹配所有JavaScript文件
use: {
loader: 'babel-loader', // 必须使用loader而非plugin
options: {
presets: ['@babel/preset-env'] // 指定ES5转换规则
}
}
}
]
},
plugins: [
new webpack.DllPlugin({ /* DLL配置 */ }) // Plugin负责库生成,不处理文件转换
]
};