导航菜单

缓存

tip

本指南继续沿用 起步、管理输出 和 代码分离 中的代码示例。

以上,我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。

此指南的重点在于通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

输出文件的文件名(output filename)

我们可以通过替换 output.filename 中的 substitutions 设置,来定义输出文件的名称。webpack 提供了一种使用称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。其中,[contenthash] substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash] 也会发生变化。

这里使用 起步 中的示例和 管理输出 中的 plugins 插件来作为项目基础,所以我们依然不必手动地维护 index.html 文件:

project

webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist|- /src |- index.js|- /node_modules

webpack.config.js

const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = {entry: './src/index.js',plugins: [ new HtmlWebpackPlugin({-title: 'Output Management',+title: 'Caching', }),],output: {- filename: 'bundle.js',+ filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true,}, };

使用此配置,然后运行我们的 build script npm run build,产生以下输出:

...AssetSize ChunksChunk Namesmain.7e2c49a622975ebd9b7e.js 544 kB0 [emitted] [big] main index.html 197 bytes [emitted]...

可以看到,bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后再次运行构建,我们以为文件名会保持不变。然而,如果我们真的运行,可能会发现情况并非如此:

...AssetSize ChunksChunk Namesmain.205199ab45963f6a62ec.js 544 kB0 [emitted] [big] main index.html 197 bytes [emitted]...

这也是因为 webpack 在入口 chunk 中,包含了某些 boilerplate(引导模板),特别是 runtime 和 manifest。(译注:boilerplate 指 webpack 运行时的引导代码)

warning

输出可能会因当前的 webpack 版本而稍有差异。与旧版本相比,新版本不一定有完全相同的问题,但我们仍然推荐的以下步骤,确保结果可靠。

提取引导模板(extracting boilerplate)

正如我们在 代码分离 中所学到的,SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 来为所有 chunk 创建一个 runtime bundle:

webpack.config.js

const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = {entry: './src/index.js',plugins: [ new HtmlWebpackPlugin({ title: 'Caching', }),],output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true,},+optimization: {+ runtimeChunk: 'single',+}, };

再次构建,然后查看提取出来的 runtime bundle:

Hash: 82c9c385607b2150fab2Version: webpack 4.12.0Time: 3027ms AssetSize Chunks Chunk Namesruntime.cc17ae2a94ec771e9221.js1.42 KiB0 [emitted] runtimemain.e81de2cf758ada72f306.js69.5 KiB1 [emitted] main index.html 275 bytes [emitted][1] (webpack)/buildin/module.js 497 bytes {1} [built][2] (webpack)/buildin/global.js 489 bytes {1} [built][3] ./src/index.js 309 bytes {1} [built]+ 1 hidden module

将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致。这可以通过使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 选项来实现。我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:

webpack.config.js

const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = {entry: './src/index.js',plugins: [ new HtmlWebpackPlugin({ title: 'Caching', }),],output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true,},optimization: { runtimeChunk: 'single',+ splitChunks: {+cacheGroups: {+ vendor: {+test: /[\\/]node_modules[\\/]/,+name: 'vendors',+chunks: 'all',+ },+},+ },}, };

再次构建,然后查看新的 vendor bundle:

... AssetSize Chunks Chunk Namesruntime.cc17ae2a94ec771e9221.js1.42 KiB0 [emitted] runtimevendors.a42c3ca0d742766d7a28.js69.4 KiB1 [emitted] vendorsmain.abf44fedb7d11d4312d7.js 240 bytes2 [emitted] main index.html 353 bytes [emitted]...

现在,我们可以看到 main 不再含有来自 node_modules 目录的 vendor 代码,并且体积减少到 240 bytes!

模块标识符(module identifier)

在项目中再添加一个模块 print.js:

project

webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist|- /src |- index.js+ |- print.js|- /node_modules

print.js

+ export default function print(text) {+console.log(text);+ };

src/index.js

import _ from 'lodash';+ import Print from './print'; function component() {const element = document.createElement('div');// lodash 是由当前 script 脚本 import 进来的element.innerHTML = _.join(['Hello', 'webpack'], ' ');+element.onclick = Print.bind(null, 'Hello webpack!');return element; } document.body.appendChild(component());

再次运行构建,然后我们期望的是,只有 main bundle 的 hash 发生变化,然而……

...AssetSize ChunksChunk Names runtime.1400d5af64fc1b7b3a45.js5.85 kB 0 [emitted] runtime vendor.a7561fb0e9a071baadb9.js 541 kB1 [emitted] [big] vendormain.b746e3eb72875af2caa9.js1.22 kB2 [emitted] main index.html 352 bytes [emitted]...

……我们可以看到这三个文件的 hash 都变化了。这是因为每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。简要概括:

main bundle 会随着自身的新增内容的修改,而发生变化。vendor bundle 会随着自身的 module.id 的变化,而发生变化。manifest runtime 会因为现在包含一个新模块的引用,而发生变化。

第一个和最后一个都是符合预期的行为,vendor hash 发生变化是我们要修复的。我们将 optimization.moduleIds 设置为 'deterministic':

webpack.config.js

const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = {entry: './src/index.js',plugins: [ new HtmlWebpackPlugin({title: 'Caching', }),],output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true,},optimization: {+ moduleIds: 'deterministic', runtimeChunk: 'single', splitChunks: {cacheGroups: { vendor: {test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all', },}, },}, };

现在,不论是否添加任何新的本地依赖,对于前后两次构建,vendor hash 都应该保持一致:

... AssetSize Chunks Chunk Namesmain.216e852f60c8829c2289.js 340 bytes0 [emitted] mainvendors.55e79e5927a639d21a1b.js69.5 KiB1 [emitted] vendorsruntime.725a1a51ede5ae0cfde0.js1.42 KiB2 [emitted] runtime index.html 353 bytes [emitted]Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js...

然后,修改 src/index.js,临时移除额外的依赖:

src/index.js

import _ from 'lodash';- import Print from './print';+ // import Print from './print'; function component() {const element = document.createElement('div');// lodash 是由当前 script 脚本 import 进来的element.innerHTML = _.join(['Hello', 'webpack'], ' ');-element.onclick = Print.bind(null, 'Hello webpack!');+// element.onclick = Print.bind(null, 'Hello webpack!');return element; } document.body.appendChild(component());

最后,再次运行我们的构建:

... AssetSize Chunks Chunk Namesmain.ad717f2466ce655fff5c.js 274 bytes0 [emitted] mainvendors.55e79e5927a639d21a1b.js69.5 KiB1 [emitted] vendorsruntime.725a1a51ede5ae0cfde0.js1.42 KiB2 [emitted] runtime index.html 353 bytes [emitted]Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js...

我们可以看到,这两次构建中,vendor bundle 文件名称,都是 55e79e5927a639d21a1b。

结论

缓存可能很复杂,但是从应用程序或站点用户可以获得的收益来看,这值得付出努力。想要了解

相关推荐: