前端应用中的 HTTP 缓存方案
🍑宝 network 面板加载资源的形式:
刷新页面或再次访问时大部分资源都命中了强缓存,唯独率先加载的 HTML 资源走了协商缓存
像 JS、CSS 等资源经过像 webpack 这样的打包工具打包后可以自动生成 hash 文件名,每次部署到服务器上后发生变化的资源 hash 名会更新,浏览器会当作一个新的资源去向服务器请求,没有更新的资源便会优先读取浏览器缓存。
而 HTML 不同,其文件名不会改变,我们期望浏览器每次加载时都应该向服务器询问是否更新,否则会出现新版本发布后浏览器读取缓存 HTML 文件导致页面空白报错(旧资源被删除)或应用没有更新(读取了旧资源)的问题。
根据 HTTP 缓存的规则最终可以总结出如下缓存方案:
- 频繁变动的资源,比如 HTML, 采用协商缓存
- CSS、JS、图片资源等采用强缓存,使用 hash 命名
那么关于如何让 HTML 文件走协商缓存,前提得先让浏览器强缓存失效,可以设置如下服务器响应报头:
Cache-Control: max-age=0
Last-Modified: Sat, 04 Sep 2021 08:59:40 GMT
在资源 0 秒就失效的情况下存在协商缓存触发条件的 Last-Modified 标识,这样每次访问加载的 HTML 资源就会确保是最新的,解决了 HTML 怕被浏览器强缓存的烦恼。
# Webpack 中的 Hash 模式
在 webpack 中 hash 可以分为三种类型:hash、chunkhash、contenthash,每一种类型 hash 的生成规则和作用也不同
# hash
hash 属于项目级别的 hash,意思就是整个项目中只要有文件改变该 hash 值就会变化,同时所有文件也都共用该 hash 值。
webpack 的简单配置如下:
module.exports = {
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[hash:8].js'),
chunkFilename: utils.assetsPath('js/[name].[hash:8].min.js'),
},
plugins:[
// 将 js 中引入的 css 进行分离
new ExtractTextPlugin({ filename: utils.assetsPath('css/[name].[hash:8].css'), allChunks: true }),
]
}
# chunkhash
chunkhash 与 hash 不同,其属于入口文件级别的 hash,会根据入口文件(entry)的依赖进行打包,同时为了避免一些公共库、插件被打包至入口文件中,我们可以借助 CommonsChunkPlugin 插件进行公共模块的提取:
module.exports = {
entry: utils.getEntries(),
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].min.js'),
},
plugins:[
// 将 js 中引入的 css 进行分离
new ExtractTextPlugin({ filename: utils.assetsPath('css/[name].[chunkhash:8].css') }),
// 分离公共 js 到 vendor 中
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', //文件名
minChunks: function(module, count) {
// 声明公共的模块来自 node_modules 文件夹,把 node_modules、common 文件夹以及使用了2次依赖的都抽出来
return (
module.resource &&
(/\.js$/.test(module.resource) || /\.vue$/.test(module.resource)) &&
(module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0 || module.resource.indexOf(path.join(__dirname, '../src/common')) === 0 || count >= 2)
);
}
}),
// 将运行时代码提取到单独的 manifest 文件中,防止其影响 vendor.js
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
chunks: ['vendor']
})
]
}
上述配置我们将需要抽离的公共模块提取到了 vendor.js 中,同时也将 webpack 运行文件提取到了 runtime.js 中,这些公共模块一般除了升级版本外永远不会改动,我们希望浏览器能够将其存入强缓存中,不受其他业务模块的修改导致文件 chunkhash 名称变动的影响。
最终我们打包出的模块拥有不同的 chunkhash 名称,重新打包只会影响有变动的模块重新生成 chunkhash。
# contenthash
contenthash 是属于文件内容级别的 hash,其会根据文件内容的变化而变化,一般用于解决以下问题: 比如某个js 中单独引入了.css 文件,那么当 js 文件被修改后,就算 css 文件并没有被修改,由于该模块发生了改变,同样会导致 css 文件也被重复构建。此时,针对 css 使用 contenthash 后,只要其内容不变就不会被重复构建。
module.exports = {
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].min.js'),
},
plugins:[
// 将 js 中引入的 css 进行分离,使用 contenthash 判断内容的改变
new ExtractTextPlugin({ filename: utils.assetsPath('css/[name].[contenthash:8].css'), allChunks: true }),
]
}