项目环境
VUE CLI3.0
VUE-ROUTER ^3.0.2
开发调试现状
该项目现在是前后端分离的模式,后端只负责提供接口,前端通过接口接收数据渲染内容。cli 打包后,将 build 的 index.html 页面给后端套模板,发布在后端的服务器上,而 build 出来的静态资源(如:js、css、字体等)则是通过公司的发布系统,发布在 CDN 服务器上。
测试线联调,则是当前端资源和后端服务都发布上去后,前端通过 charles 代理,将测试线资源代理到本地进行调试,好处是不同处理跨域。问题是需要保证每一次发布,项目都会去请求最新的资源,这时需要时间戳或者hash。
问题描述
首先,项目中为了分包按需加载,使用了路由懒加载。并且 vue.config.js 中 filenameHashing 设置为 false,这样打出来的资源不会带有 8位 hash,例如 ProjectConstruct.d78b3ee9.js,而是单纯的 ProjectConstruct.js。这么做的目的是,由于项目的模板首页是套在后端代码中的,不想每一次前端发布都需要后端发布。我们只需要让后端在模板中每一个资源后面加一个时间戳,抛出一个路由供更新时间戳就搞定了,像这样 ProjectConstruct.d78b3ee9.js?v=98097889312345。
说说为什么不用 filenameHashing,如果使用,那么打包后生成的资源会携带不同的 hash。
dist/js/PermissionManager.d78b3ee9.js
dist/js/addProject.abc9876c.js
dist/js/NotFound.d0988ab.js
原因是 CLI 源码就是这么写的,没有暴露自定义配置,源码 cli-service/lib/config/prod.js
const filename = getAssetPath(
options,
`js/[name]${isLegacyBundle ? `-legacy` : ``}${options.filenameHashing ? '.[contenthash:8]' : ''}.js`
)
这样后端模板中资源地址就没办法通过一个变量控制了。就又变成了需要后端不断的发布新模板,这不是我们想要的。
最后,我们先尝试就是关闭 filenameHashing,后端添加时间戳,后端模板大致是这样的:
但当发布测试线后,我们有新的改动重新发布新资源到服务器,并且更新了时间戳,但是资源(css、js)没有更新,依旧是旧的。打开 network 查看资源的请求地址发现,预先加载(带有 preload、prefetch)的是带有时间戳的,并且也是最新的。但是由于懒加载,触达到每一个页面才加载的 js 和 css,却没有带上时间戳,也就是资源一直受浏览器的缓存的影响。看了打包出来的 app.js 中关于 js 和 css 生成链接的部分,确实没有带时间戳,这是正常的,因为我的时间戳上面讲了是由后端提供的。
但令我不理解的是,为什么预先加载的资源是最新的,但路由懒加载到的资源虽然没带时间戳,但是一直是旧的?
解决方案
既然请求的资源一直是旧的,是因为懒加载资源地址没有携带类似时间戳的东西,如上图红圈部分。那么我们想到的是,那就直接在.js 和 .css 前加一段 hash 啊,这样每一次打包后的资源地址就都不一样,就不存在缓存了。
但是如果使用 filenameHashing,则会使得每一个资源生成不同的 hash,这样没办法固定后端模板,需要反复发布。那么如何解决这个问题呢?
filename & chunkFilename
webpack 为我们提供了两个输出(output)配置项,可以通过这两个属性来自定义 bundle 的名字,在 vue.config.js 中的使用方法:
module.exports = {
filenameHashing: false,
css: {
extract: false
},
configureWebpack: {
output: {
/**
* 代理调试时使用,固定hash替换[hash:8],发布时请删除
* filename: 'js/[name].代理 hash.js',
* chunkFilename: 'js/[name].代理 hash.js'
*/
filename: 'js/[name].[hash:8].js',
chunkFilename: 'js/[name].[hash:8].js'
}
}
}
这样,我们 build 出来的资源的文件名变成统一的 hash:
dist/js/PermissionManager.d78b3ee9.js
dist/js/addProject.d78b3ee9.js
dist/js/NotFound.d78b3ee9.js
前端给后端的模板也变成了下面这样:
至于其中的 hash 具体的值,则需要我们利用执行清理缓存接口时,传给后端,例如:https:h.xx.cn/admin/papi/clearVersion?hash=d78b3ee9,这样模板中的 {{hash}} 就会替换成 d78b3ee9。这样我们保证了后端模板的固定,只需要清理缓存时,每次将最新资源的 hash 值告诉后端,那么请求的资源地址永远保持最新状态。
当我们发布到测试线后,发现 js 和我们想的一样,请求都是带上最新 hash 的:PermissionManager.d78b3ee9.js。每次有新的功能更新,将新资源发布到测试线,更新模板 hash 后,测试线的页面变成新的了。这样 js 缓存问题就解决了。
但是我们还是发现,js 资源没问题,但 css 却出了问题,还是请求了没带 hash 的资源。我使用 extract-text-webpack-plugin 插件,也是统一了 hash。看了下打包出来的 css,发现 dist/css/ 文件夹下,打了两套 css,一套带有 hash,一套却没带。又看了 app.js 中关于 css 资源地址拼接部分代码,发现确实有两段这样代码,一段拼接了 hash,一段没有拼接。但是在测试线路由懒加载中请求的却是一直没有带 hash 的,这个问题我后来没有管了。
关于 css 我的解决方法是使用 extract 设置为 false,这样所有的 css 都会以行内的形式陷入html,这样会增加一些 bundle 的大小,但是能解决上面的问题,保证整体路由懒加载能用。 这样,给后端的模板就可以删除所有关于 css 的资源链接了,只保留 js。
css: {
extract: false // css inline
}
新模板下如何联调
至此,路由懒加载引起的缓存问题我们就解决了。最后一个问题,由于每一次发布后,资源地址的 hash 值会更新。这样我们使用 Charles 的 Map Local 就没办法代理到本地资源了,因为线上和本地的资源名字对不上了。
rewrite
Charles 除了提供了 Map Local 代理外,还可以使用 rewrite 进行代理,rewrite 支持我们使用正则匹配地址,具体参考网上的使用方法。
固定 hash
我们最新发布的 hash 是每一次都可以知道的,那么我们在本地 npm run watch 进行调试,就可以将 vue.config.js 中的 filename 和 chunkFilename 固定:
output: {
/**
* 代理调试时使用,固定hash替换[hash:8],发布时请删除
* filename: 'js/[name].代理 hash.js',
* chunkFilename: 'js/[name].代理 hash.js'
*/
// filename: 'js/[name].[hash:8].js',
// chunkFilename: 'js/[name].[hash:8].js'
filename: 'js/[name].d78b3ee9.js',
chunkFilename: 'js/[name].d78b3ee9.js'
}
这样我们 Charles 的 Map Local 就能一直生效了,比较麻烦的就是每一次新发布,本地代理时都需要重新更改 vue.config.js。
minimist
如果你嫌上面手动修改配置文件比较麻烦,可以使用 minimist 命令行解析引擎。改造 watch 命令来,自动传入 代理 hash。
"watch": "vue-cli-service build --mode dynamic --v ${V} --watch",
const minimist = require('minimist')
const options = minimist(process.argv.slice(2))
const customHash = options.v
const hashStuffix = /^[A-Za-z0-9]{8}$/.test(customHash) ? customHash : '[hash:8]'
module.exports = {
css: {
extract: false
},
configureWebpack: {
output: {
/**
* filename: 'js/[name].代理 hash.js',
* chunkFilename: 'js/[name].代理 hash.js'
*/
filename: `js/[name].${hashStuffix}.js`,
chunkFilename: `js/[name].${hashStuffix}.js`
}
}
}
这样你只需要执行:V=d78b3ee9 npm run watch
。也能实现上面的效果了。