项目环境

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。

  1. dist/js/PermissionManager.d78b3ee9.js
  2. dist/js/addProject.abc9876c.js
  3. dist/js/NotFound.d0988ab.js

原因是 CLI 源码就是这么写的,没有暴露自定义配置,源码 cli-service/lib/config/prod.js

  1. const filename = getAssetPath(
  2. options,
  3. `js/[name]${isLegacyBundle ? `-legacy` : ``}${options.filenameHashing ? '.[contenthash:8]' : ''}.js`
  4. )

这样后端模板中资源地址就没办法通过一个变量控制了。就又变成了需要后端不断的发布新模板,这不是我们想要的。

最后,我们先尝试就是关闭 filenameHashing,后端添加时间戳,后端模板大致是这样的:
muban1.png

但当发布测试线后,我们有新的改动重新发布新资源到服务器,并且更新了时间戳,但是资源(css、js)没有更新,依旧是旧的。打开 network 查看资源的请求地址发现,预先加载(带有 preload、prefetch)的是带有时间戳的,并且也是最新的。但是由于懒加载,触达到每一个页面才加载的 js 和 css,却没有带上时间戳,也就是资源一直受浏览器的缓存的影响。看了打包出来的 app.js 中关于 js 和 css 生成链接的部分,确实没有带时间戳,这是正常的,因为我的时间戳上面讲了是由后端提供的。

企业微信截图_b10a6c43-d7af-4bb1-aa5e-1ca3a6ac7531.png

但令我不理解的是,为什么预先加载的资源是最新的,但路由懒加载到的资源虽然没带时间戳,但是一直是旧的?

解决方案

既然请求的资源一直是旧的,是因为懒加载资源地址没有携带类似时间戳的东西,如上图红圈部分。那么我们想到的是,那就直接在.js 和 .css 前加一段 hash 啊,这样每一次打包后的资源地址就都不一样,就不存在缓存了。
但是如果使用 filenameHashing,则会使得每一个资源生成不同的 hash,这样没办法固定后端模板,需要反复发布。那么如何解决这个问题呢?

filename & chunkFilename

webpack 为我们提供了两个输出(output)配置项,可以通过这两个属性来自定义 bundle 的名字,在 vue.config.js 中的使用方法:

  1. module.exports = {
  2. filenameHashing: false,
  3. css: {
  4. extract: false
  5. },
  6. configureWebpack: {
  7. output: {
  8. /**
  9. * 代理调试时使用,固定hash替换[hash:8],发布时请删除
  10. * filename: 'js/[name].代理 hash.js',
  11. * chunkFilename: 'js/[name].代理 hash.js'
  12. */
  13. filename: 'js/[name].[hash:8].js',
  14. chunkFilename: 'js/[name].[hash:8].js'
  15. }
  16. }
  17. }

这样,我们 build 出来的资源的文件名变成统一的 hash:

  1. dist/js/PermissionManager.d78b3ee9.js
  2. dist/js/addProject.d78b3ee9.js
  3. dist/js/NotFound.d78b3ee9.js

前端给后端的模板也变成了下面这样:
muban3.png

至于其中的 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。

  1. css: {
  2. extract: false // css inline
  3. }

新模板下如何联调

至此,路由懒加载引起的缓存问题我们就解决了。最后一个问题,由于每一次发布后,资源地址的 hash 值会更新。这样我们使用 Charles 的 Map Local 就没办法代理到本地资源了,因为线上和本地的资源名字对不上了。

rewrite

Charles 除了提供了 Map Local 代理外,还可以使用 rewrite 进行代理,rewrite 支持我们使用正则匹配地址,具体参考网上的使用方法。

固定 hash

我们最新发布的 hash 是每一次都可以知道的,那么我们在本地 npm run watch 进行调试,就可以将 vue.config.js 中的 filename 和 chunkFilename 固定:

  1. output: {
  2. /**
  3. * 代理调试时使用,固定hash替换[hash:8],发布时请删除
  4. * filename: 'js/[name].代理 hash.js',
  5. * chunkFilename: 'js/[name].代理 hash.js'
  6. */
  7. // filename: 'js/[name].[hash:8].js',
  8. // chunkFilename: 'js/[name].[hash:8].js'
  9. filename: 'js/[name].d78b3ee9.js',
  10. chunkFilename: 'js/[name].d78b3ee9.js'
  11. }

这样我们 Charles 的 Map Local 就能一直生效了,比较麻烦的就是每一次新发布,本地代理时都需要重新更改 vue.config.js。

minimist

如果你嫌上面手动修改配置文件比较麻烦,可以使用 minimist 命令行解析引擎。改造 watch 命令来,自动传入 代理 hash。

  1. "watch": "vue-cli-service build --mode dynamic --v ${V} --watch",
  1. const minimist = require('minimist')
  2. const options = minimist(process.argv.slice(2))
  3. const customHash = options.v
  4. const hashStuffix = /^[A-Za-z0-9]{8}$/.test(customHash) ? customHash : '[hash:8]'
  5. module.exports = {
  6. css: {
  7. extract: false
  8. },
  9. configureWebpack: {
  10. output: {
  11. /**
  12. * filename: 'js/[name].代理 hash.js',
  13. * chunkFilename: 'js/[name].代理 hash.js'
  14. */
  15. filename: `js/[name].${hashStuffix}.js`,
  16. chunkFilename: `js/[name].${hashStuffix}.js`
  17. }
  18. }
  19. }

这样你只需要执行:V=d78b3ee9 npm run watch。也能实现上面的效果了。

参考

filename & chunckFilename
extract-text-webpack-plugin
Extract