1. 简介
本节主要介绍如何利用 dll 相关技术来提高打包效率。
2. 准备代码
// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
entry: {
index: './src/index.jsx',
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node-modules/,
use: ['babel-loader'],
},
{
test: /\.(jpg|jpeg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 2048,
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(eot|svg|ttf|woff)$/,
use: 'file-loader',
},
],
},
optimization: {
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
},
},
},
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new CleanWebpackPlugin(),
],
};
// build/webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
overlay: true,
open: true,
port: 3000,
hot: true, // 开启热更新
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:3600',
changeOrigin: true,
},
},
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
};
module.exports = merge(commonConfig, devConfig);
// build/webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
};
module.exports = merge(commonConfig, prodConfig);
// src/index.jsx
import React, { Component } from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './home';
import List from './list';
class App extends Component {
render() {
return (
<BrowserRouter>
<>
<Route path="/" exact component={Home} />
<Route path="/list" component={List} />
</>
</BrowserRouter>
);
}
}
ReactDom.render(<App />, document.getElementById('root'));
<!--src/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>esmodule-oop</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
// src/home.jsx
import React, { Component } from 'react';
class Home extends Component {
render() {
return <div>home</div>;
}
}
export default Home;
// src/list.jsx
import React, { Component } from 'react';
class List extends Component {
render() {
return <div>List</div>;
}
}
export default List;
"scripts": {
"dev": "webpack --config ./build/webpack.dev.js --watch",
"dev-analyse": "webpack --config ./build/webpack.dev.js --profile --json > stas.json",
"build-analyse": "webpack --config ./build/webpack.prod.js --profile --json > stas.json",
"dev-server": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},
3. 普通打包
首先我们直接使用 dev 命令打包,(这里暂不使用 dev-server )
npm run dev
我们多打几次看看,最终耗时我这边大概是 3500ms 左右。
![](https://img.haomeiwen.com/i4761597/523090f05dedae42.png)
然后我们修改一下 home.jsx 代码,增加一个三方库,lodash。我们装一下 lodash 然后引入:
// src/home.jsx
import React, { Component } from 'react';
import _ from 'lodash';
class Home extends Component {
render() {
return <div>{_.join(['home', 'page'], ' ')}</div>;
}
}
export default Home;
然后打几次包,平均耗时大概 3900ms 左右:
![](https://img.haomeiwen.com/i4761597/8ab77ab477e7a9f6.png)
可以看到新增 node_modules 以后,vendors 体积和打包耗时都显著增加了。
4. dll 打包
我们可以看到,node_modules 或造成打包耗时显著增加。可事实上,我们很多 node_modules 都是稳定的,很少更新,每次重新打包 node_modules 并没有意义。如果我们有一个方案,将不变的那些 node_modules 打包一次后存起来,下次打包的时候直接使用已经打包好的这些 node_modules 代码,是不是就会快很多呢?
事实上,webpack 提供了这样的插件来实现上述的功能,就是dll-plugin和dllreferenceplugin。我们来看一下如何使用:
4.1 将需要抽取的 common 文件单独打包
// build/webpack.dll.js
const path = require('path');
module.exports = {
mode: 'development', // 这里为了演示,使用 development
entry: {
vendors: ['react', 'react-dom', 'lodash'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]',
},
};
"vendors-dll": "webpack --config ./build/webpack.dll.js"
运行上述 vendors-dll 打包命令,生成如下文件:
![](https://img.haomeiwen.com/i4761597/51d70356eaf8b96b.png)
4.2 引入打包后的 vendors.dll.js
打包后的文件,还需要让 html 引用。这里我们可以使用 add-asset-html-webpack-plugin插件,来帮我们实现 html 自动引用。
// build/webpack.common.js
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../dll/vendors.dll.js') }),
new CleanWebpackPlugin(),
],
运行 npm run dev 后,打开 index.html,控制台可以输出 vendors:
![](https://img.haomeiwen.com/i4761597/3e2a86148671dec1.png)
![](https://img.haomeiwen.com/i4761597/130a05061ddf3dd2.png)
但是目前我们的打包体积和时间都没有减小。
4.3 dll-plugin 和 dllreferenceplugin
dll-plugin
是在一个额外的独立的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。 这个插件会生成一个名为 manifest.json
的文件,这个文件是用来让 DLLReferencePlugin
映射到相关的依赖上去的。
DLLReferencePlugin
是在 webpack 主配置文件中设置的, 这个插件把只有 dll 的 bundle(s)(即:dll-only-bundle(s)) 引用到需要的预编译的依赖。
简言之,DllPlugin 和 DLLReferencePlugin 允许用户提前为所有那些不需要关心的 npm 模块创建一个单独的包,教会 Webpack 将它们引用到该包,大大减少了 Webpack 构建包(以及重构包)所需的时间。
// build/webpack.dll.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'development', // 这里为了演示,使用 development
entry: {
vendors: ['react', 'react-dom', 'lodash'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '../dll', '[name].manifest.json'),
name: '[name]',
}),
],
};
// build/webpack.common.js
...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../dll/vendors.dll.js') }),
new webpack.DllReferencePlugin({
// 注意: DllReferencePlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
context: path.resolve(__dirname, '../'),
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json'),
}),
new CleanWebpackPlugin(),
],
运行 npm run vendors-dll,然后运行几次 npm run dev,如下:
![](https://img.haomeiwen.com/i4761597/6a26b411227c75e9.png)
时间大幅缩短。我们运行一下 npm run dev-server,看一下页面是否 okay:
![](https://img.haomeiwen.com/i4761597/337b265d895d5ba3.png)
页面运行正常,且打包时间大大缩短
5. 处理多个 dll 文件
并不是只有 node_modules 才可以抽成 dll,一些经常不做变更的 libs 模块也可以。大型项目中,存在很多可以抽离的地方,为了单独更新,也为了控制文件的大小,我们并不会将他们全部放在一个 dll 中。这个时候我们就要多次调用 AddAssetHtmlPlugin 和 DllReferencePlugin,让配置文件显得冗长而且不易管理。这个时候,可以使用 node_modules 动态生成 plugins。
// build/webpack.dll.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'development', // 这里为了演示,使用 development
entry: {
react: ['react', 'react-dom'],
lodash: ['lodash'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '../dll', '[name].manifest.json'),
name: '[name]',
}),
],
};
// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');
const plugins = [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new CleanWebpackPlugin(),
];
fs.readdirSync(path.resolve(__dirname, '../dll')).forEach(file => {
if (/.*.dll.js$/.test(file)) {
plugins.push(new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, '../dll', file),
}));
}
if (/.*.manifest.json$/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file),
}));
}
});
module.exports = {
entry: {
index: './src/index.jsx',
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node-modules/,
use: ['babel-loader'],
},
{
test: /\.(jpg|jpeg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 2048,
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(eot|svg|ttf|woff)$/,
use: 'file-loader',
},
],
},
optimization: {
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
},
},
},
},
plugins,
};
![](https://img.haomeiwen.com/i4761597/c7c236a36ec5c453.png)
6. 利用 autodll-webpack-plugin 简化配置
上面我们可以看到,引入 dll 需要非常多的配置,一旦 node_modules 发生变化还需要我们去手动重新编译模块。那么有没有更简单的方式去实现 dll 配置呢?当然是有的,这就是我们要介绍的 autodll-webpack-plugin 插件。
AutoDllPlugin 是 DllPlugin 和 DllReferencePlugin 的高级插件,隐藏了它们的大部分复杂性。
-
缓存
:当用户第一次构建 bundle 时,AutoDllPlugin 会为您编译 DLL,并将所有指定的模块从 bundle 引用到 DLL。下一次编译代码时,AutoDllPlugin 将跳过构建并从缓存中读取。 -
监听变动自动构建
:每当更改插件的配置,安装或删除一个 node_module 时,AutoDllPlugin 将重新构建dll。 -
内存
:当使用Webpack的Dev服务器时,包被加载到内存中,以防止从文件系统进行不必要的读取。 -
html 自动引入
:按照 DLLPlugin 的工作方式,必须先加载 DLL 包,然后再加载自己的包。这通常是通过向 HTML 添加额外的脚本标记来实现的。因为这是一个非常常见的任务,AutoDllPlugin可以为用户完成(与HtmlPlugin一起)。
下面我们看下如何来使用:
首先安装该插件,这里要注意 webpack4 和 webpack2/3 安装的版本并不一样。
webpack 4
npm install --save-dev autodll-webpack-plugin
webpack 2 / 3
npm install --save-dev autodll-webpack-plugin@0.3
// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const AutoDllPlugin = require('autodll-webpack-plugin');
const path = require('path');
module.exports = {
entry: {
index: './src/index.jsx',
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node-modules/,
use: ['babel-loader'],
},
{
test: /\.(jpg|jpeg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 2048,
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(eot|svg|ttf|woff)$/,
use: 'file-loader',
},
],
},
optimization: {
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
},
},
},
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new AutoDllPlugin({
inject: true, // will inject the DLL bundle to index.html
filename: '[name]_[hash].dll.js',
path: './dll',
entry: {
react: [
'react',
'react-dom',
],
lodash: [
'lodash',
],
},
}),
new CleanWebpackPlugin(),
],
};
npm run dev 打包如下:
![](https://img.haomeiwen.com/i4761597/c510b4836880614a.png)
我们不管打包多少遍,可以看到 dll 目录下的文件都是不变的。然后我们试着重新安装 lodash,现在我们用的是 4.17.15,我们切换一下:
npm i lodash@4.17.14 -S
然后 npm run dev
![](https://img.haomeiwen.com/i4761597/8fad2f57d26fd58e.png)
可以看到 dll 下的文件更新了。
7. 即将被抛弃的 dll
上面可以看到,使用 dll 能极大提升构建速度,可是 dll 本身就是为了弥补 webpack 打包的不足而出现的,随着 webpack 的升级和优化,额外使用插件实现 dll 带来的提升已经越来越小。
在 vue-cli 的某次 pr 中,尤雨溪提到vue-cli/pull/1002 不在维护 dll。并且在一次 commit中删除了对 dll 的支持。
![](https://img.haomeiwen.com/i4761597/ea4c5df10424a500.png)
不过可以看到,vue-cli 本来使用的就是 autodll-webpack-plugin 插件,其可靠性是很有保证的,而且,在 webpack4 来讲,还是有很好效果的。
不过对于 webpack5 来说,这个插件确实就派不上用场了,这不是我说的,这是插件的文档写的。
Now, that webpack 5 planning to support caching out-of-the-box, AutoDllPlugin will soon be obsolete.
In the meantime, I would like to recommend Michael Goddard's hard-source-webpack-plugin, which seems like webpack 5 is going to use internally.
作者明确告诉我们 webpack5 计划支持磁盘缓存,本插件即将废弃。并且向我们推荐了另一款插件,可能会被内置在 webpack5 中。这款插件就是 hard-source-webpack-plugin
。
8. hard-source-webpack-plugin
HardSourceWebpackPlugin 是 webpack 的一个插件,用于为模块提供中间缓存步骤。为了看到结果,您需要使用这个插件运行两次 webpack: 第一次构建将花费正常的时间。第二个版本的速度将明显加快。
安装:
npm install --save-dev hard-source-webpack-plugin
用法如下:
// build/webpack.common.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
context: // ...
entry: // ...
output: // ...
plugins: [
new HardSourceWebpackPlugin()
]
}
![](https://img.haomeiwen.com/i4761597/b92ddba24651d198.png)
这速度简直逆天了,配置也超级简单。当然还有更多的配置,可以参考文档。
如果我们修改 node_modules,再打包,发现 vendors 也会自动更新。
![](https://img.haomeiwen.com/i4761597/e697e81d341deacd.png)
至于文件更新以后,如何清除浏览器缓存的影响,保证代码的更新,只要在 filename 加入 hash 即可:
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[hash].js',
},
9. 小结
本节其实主要讲的就是如何处理打包过程中的磁盘缓存,并介绍了dll-plugin和dllreferenceplugin ,autodll-webpack-plugin 和 hard-source-webpack-plugin 等缓存相关的优秀插件。
推荐大家直接使用 hard-source-webpack-plugin 即可。
参考
你是否需要webpack dll
webpack使用-详解DllPlugin
webpack打包指位置Dll打包方式
使用 happypack 提升 Webpack 项目构建速度
Webpack支撑大规模应用开发最佳实践
webpack.DllPlugin和webpack.DllReferencePlugin静态资源预编译插件
辛辛苦苦学会的 webpack dll 配置,可能已经过时了
webapack-doc/dll-plugin
autodll-webpack-plugin
网友评论