通过 webpack 打包文件,并根据不同环境独立出相应的配置文件,这是我们都会的。但,我们不能满足于能打包出文件就可以了,还应该思考怎么打包出更优的文件。比如,我们都知道浏览器有强缓存与协商缓存,如果能够让不变的文件得到有效缓存,变动的文件得到及时更新,这样就能在性能上有所提升。
建议:官网也有类似的说明,建议去官网阅读,官网提供了中文切换选项。中文网上的文档还没及时更新,有些写法变了。
如果有说的不对的地方,烦请指出,谢谢,我也是尝试总结写这样的问题。
分析问题
假如,我们有一下 webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
})
],
};
有这样的 index.js
import _ from 'lodash';
const component = function() {
let el = document.createElement('div');
el.innerHTML = _.join(['webpack', '基础配置'], ' ');
el.onclick = print;
return el;
}
const element = component();
document.body.appendChild(element);
执行 npm run build
打包出来后,有如下文件:
dist
├── app.bundle.js // 72.2 KiB
└── index.html // 206 bytes
注意上面的 app.bundle.js
中包含了 lodash
,如果 app.bundle.js
更新了,每次重新加载的时候,都需要重新加载这个不变的 lodash
,这样显然是浪费请求资源的。因此,这里就需要做两件事
第一,分离打包文件。第二,解决缓存问题
分离打包文件
常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用来分离代码。
之前的教程是通过 webpack.optimize.CommonsChunkPlugin
去分离打包文件的,在 webpack4.x
开始,官方移除了 commonchunk
插件,改用了 optimization
属性进行更加灵活的配置。想要了解 这两者之间的差别,可以参考webpack4:连奏中的进化。
下面,我们来配置如何分离打包文件:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ...
entry: {
common: ['lodash'],
app: './src/index.js',
},
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: "common",
chunks: "initial",
minChunks: 2
},
},
},
},
plugins: [
new CleanWebpackPlugin(), // 在后续的过程性会多次构建,添加这个插件
// ...
],
// ...
}
查看目录:
dist
├── app.bundle.js // 1.69 KiB
├── common.bundle.js // 71.1 KiB
└── index.html // 246 bytes
可以看到,包含在 common.bundle.js
中的 lodash
已经 从 app.bundle.js
中分离出来了,从他们的大小,跟之前的对比,也能证明这点。
现在,解决了 分离打包
问题,但是 缓存
的问题,还没解决。从上面打包出来的目录,我们注意到,每次打包出来的 文件名都一样 比如 app.bundle.js
。这样,浏览器可能会将这个文件缓存下来,当这个文件更新时,执行构建,它生成的依然是同样名字的文件,浏览器可能就会人会它没更新
,就会使用之前缓存的文件。
这里说明一点,关于上面说的 它没更新,我的理解是:假如有效期是无限,可能会出现这种情况,跟服务端 no-cache配置有关。这里讨论的方案是,不管你后端配没配置,有改动后,我生成一个新文件,这样就能保证更新。
所以下面,我们来配置如何每次构建生成新文件
[name].[hash].bundle.js
这里利用 output
中的 filename
字段,将文件重命名,就能实现每次构建生成不一样的文件名:
module.exports = {
entry: { /*...*/ },
output: {
filename: '[name].[hash].bundle.js',
path: path.resolve(__dirname, 'dist')
},
optimization: { /*...*/ },
plugins: [ /*...*/ ],
};
打包后的文件如下:
dist
├── app.d58e77cdc316b3ce1854.bundle.js // 1.69 KiB
├── common.d58e77cdc316b3ce1854.bundle.js // 71.1 KiB
└── index.html // 288 bytes
貌似,解决了这个问题。是,它是生成了hash,但是如果,我们修改 index.js
中的内容,再次执行构建:
dist
├── app.26935d7e055caac6c663.bundle.js // 1.69 KiB
├── common.26935d7e055caac6c663.bundle.js // 71.1 KiB
└── index.html // 288 bytes
观察打包后的文件,发现,我们只改了 index.js
,打包后,app.js
和 commo.js
的文件名都变了。这不是我们想要的,我们只需要有变动的 app.js
文件名变化,没有变动的 common.js
文件名不变化,这样就能利用缓存。
[name].[chunkhash].bundle.js
我们将配置稍微改下,将 [hash]
改成 [chunkhash]
根据,文件内容生成一串 hash。(注意,开发环境下,一般不要用 chunkhash 因为这会增加编译时间。而且,这个时候 HMR 将不起作用)
module.exports = {
// ...
output: {
filename: '[name].[chunkhash].bundle.js',
path: path.resolve(__dirname, 'dist')
},
// ...
}
打包:
dist
├── app.6f86a1c3055fcbb2f569.bundle.js
├── common.ed707d7fd801788845a1.bundle.js
└── index.html
这个时候,还看不出效果,修改 index.js
,打包:
dist
├── app.afc86616f6e622a167b5.bundle.js
├── common.ed707d7fd801788845a1.bundle.js
└── index.html
观察,发现,app.js
文件名变了,common.js
文件名没变,看起来,好像管用。
情况一
如果,我们将入口出的文件顺序颠倒一下:
module.exports = {
entry: {
app: './src/index.js',
common: ['lodash'],
},
}
再次打包:
dist
├── app.3c8b22be8868b0feb022.bundle.js
├── common.7310dc2ae1c70f3154f4.bundle.js
└── index.html
发现,两个文件名都变了。卒!
情况二
如果,在情况一的基础上(只要确保 app 入口,在 common 入口 前面,效果一样),在 index.js
中,引入 print.js
,
// index.js
// ...
import print from './print';
// ...
// print.js
import jquery from 'jquery';
export default function printMe() {
console.log(jquery.name);
}
打包:
dist
├── app.0d03e2f9e94a947297c1.bundle.js
├── common.147b8278aca7b0bd1b4a.bundle.js
└── index.html
发现,两个文件名又
都变了。卒!
manifest
上面,两种情况主要是由于 manifest 的原因。
我的理解是,在webpack 构建过程中,通过入口去分析依赖,在内部生成一个类似 manifest
的文件,这个文件记录了模块与文件的映射关系。
情况一,当修改入口顺序,生成的 manifest
表,会变化,而这个 manifest
会打包到文件中去,因此,前面两个文件名会变;
本来分析依赖是 common.js
-> index.js
现在变成 index.js
-> common.js
情况二,入口顺序不变,处于入口前面的文件中,如果添加依赖,或者删除依赖,也会使 manifest
发生变化,因此,两个文件名会变。
本来分析依赖是 index.js
-> common.js
现在变成 index.js
-> print.js
-> common.js
那怎么办呢?有办法,将 manifest
文件提出来就可以了。在 webpack4.x
之前可能是通过 webpack.optimize.CommonsChunkPlugin
提取,前面说过,这个插件已经被干掉了。webpack4.x
是通过如下方法提取:
module.export = {
// ...
optimization: {
// runtimeChunk: 'single',
runtimeChunk: {
name: 'manifest'
}
// ...
}
// ...
}
tip:
{
runtimeChunk: 'single' // 这个写法 相当于下面。所以 你生成的 runtime 文件就是 manifest
runtimeChunk: {
name: 'runtime'
}
}
这个时候,你去打包,再将 情况一
,情况二
做一遍,发现还是会变。怎么回事?
moduleIds
提取了 manifest
文件,依然文件名都会变,是因为 manifest
是个映射表(我的立即),形式是个 key value,[moduleIds] -> 'file'
。
默认情况下,是以递增的数字作为 moduleIds
比如 [1] -> index.js
[2] -> common.js
。
我们虽然提取出 manifest
,但是顺序变了后,数字 [1] 对应的那个文件也变了,因此,我们需要让 moduleIds
与 文件
都固定下来。·
webpack4.x
之前是通过,NamedModulesPlugin
或者 HashedModuleIdsPlugin
插件将 id 固定下来。
webpack4.x
可以通过 optimization.moduleIds
设置,可以设置为 named
hash
也可以达到同样的效果。
module.export = {
// ...
optimization: {
runtimeChunk: {
name: 'manifest'
},
moduleIds: 'named',
// ...
}
// ...
}
这个时候,操作情况一,情况二,应该是符合我们预期的。
完整代码
webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
common: ['lodash'],
},
output: {
filename: '[name].[chunkhash].bundle.js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
// runtimeChunk: 'single',
runtimeChunk: {
name: 'manifest'
},
moduleIds: 'named',
splitChunks: {
// chunks: 'all',
cacheGroups: {
commons: {
name: "common",
chunks: "initial",
minChunks: 2
},
},
},
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
}),
],
};
index.js
import _ from 'lodash';
import print from './print';
const component = function() {
let el = document.createElement('div');
el.innerHTML = _.join(['webpack', '基础配置11'], ' ');
el.onclick = print;
return el;
}
const element = component();
document.body.appendChild(element);
print.js
import jquery from 'jquery';
export default function printMe() {
console.log(jquery.name);
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。