0

    监控前端代码版本迭代实现页面自动刷新

    2023.07.13 | admin | 159次围观

    背景:

    前端版本迭代较为频繁的时候网页老是自动刷新,使用webpack对项目进行打包,虽然我们对js和css文件使用了chunkhash进行了文件缓存控制,但是项目的index.html文件在版本频繁迭代更新时,会存在被浏览器缓存的情况。在发版后,用户不强制刷新页面,浏览器会使用缓存的index.html文件,从而导致向服务器端请求了上个版本chunkhash的js和css文件,最终页面404(上个版本chunkhash的js和css在版本更新时已替换删除了)。

    output: {
      path: config.build.assetsRoot,
      filename: utils.assetsPath('js/[name].[chunkhash].js'),
      chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
    }
    复制代码

    解决思路:服务器端发版,上一个版本的代码不删掉;在每次打包生产代码时,在static下生产一个version.json的版本信息文件,在前端页面实时请求服务器端的version.json中的版本号和浏览器本地缓存的version.json进行对比,从而监控版本迭代更新,实现页面自动更新,获取新的index.html文件(前提是服务器端对index.html进行不缓存配置)。实现

    思路1:缺点是会随着频繁发版,服务器端前端项目文件会越来越多网页老是自动刷新,浪费空间,同时,旧页面的接口涉及到接口后端同学已经废弃了,引起报错;

    所以现在主要以思路2进行处理:

    配置环境

    // config/dev.env.js
    'use strict'
    const merge = require('webpack-merge')
    const prodEnv = require('./prod.env')
    module.exports = merge(prodEnv, {
      NODE_ENV: '"development"',
      VERSION: '""'
    })
    // config/prod.env.js
    'use strict'
    module.exports = {
      NODE_ENV: '"production"', // 区分开发/生产环境
      VERSION: '"v' + new Date().getTime() + '"' // 版本格式
    }
    复制代码

    自定义版本信息生成插件: 如何编写一个插件?

    'use strict';
    var FStream = require('fs');
    /**
     * 版本信息生成插件
     * @author guoqian.xu
     * @param options
     * @constructor
     */
    function VersionPlugin(options) {
      this.options = options || {};
      !this.options.versionDirectory && (this.options.versionDirectory = 'static');
    }
    // apply方法是必须要有的,因为当我们使用一个插件时(new somePlugins({})),webpack会去寻找插件的apply方法执行
    VersionPlugin.prototype.apply = function (compiler) {
      var self = this;
      compiler.plugin('compile', function (params) {
        // 生成版本信息文件路径
        // this.options.context:项目的绝对路径
        var dir_path = this.options.context + '/' + self.options.versionDirectory;
        var version_file = dir_path + '/version.json';
        var content = '{"version":' + self.options.env.VERSION + '}';
        FStream.exists(dir_path, function (exist) {
          if (exist) {
            writeVersion(self, version_file, content);
            return;
          }
          FStream.mkdir(dir_path, function (err) {
            if (err) throw err;
            console.log('\n创建目录[' + dir_path + ']成功');
            writeVersion(self, version_file, content);
          });
        });
      });
      // 编译器对'所有任务已经完成'这个事件的监听
      compiler.plugin('done', function (stats) {
        console.log('应用编译完成!');
      });
    };
    const writeVersion = (self, versionFile, content) => {
      console.log('\n当前版本号:' + self.options.env.VERSION);
      console.log('开始写入版本信息...');
      // 写入文件
      FStream.writeFile(versionFile, content, function (err) {
        if (err) throw err;
        console.log('版本信息写入成功!');
      });
    };
    module.exports = VersionPlugin;
    复制代码

    加载插件

    // webpack.prod.config.js
    // 版本信息生成
    const VersionPlugin = require('./version-plugin');
    ...
    const webpackConfig = merge(baseWebpackConfig, {
      ...
      plugins: [
        // 版本信息生成
        new VersionPlugin({
          path: config.build.assetsRoot,
          env: env,
          versionDirectory: 'static'
        }),
        ...
      ]
      ...
    });
    复制代码

    选择你需要的位置进行监控(路由钩子、特定页面、接口调用拦截等)

    // 版本监控
    async versionCheck() {
      if (NODE_ENV === 'development') return;
      const response = await this.$ajax.get(`../static/version.json`);
      if (VERSION !== response.data.version) {
        this.$alert('发现新版本,自动更新中...', '温馨提示', {
          confirmButtonText: '我知道了',
          type: 'warning',
          closeOnClickModal: false,
          closeOnPressEscape: false,
          showClose: false,
          callback: action => {
            window.location.reload(true);
          }
        });
      }
    }
    复制代码

    这里说一下在路由钩子的检测实现:

    // router/index.js
    import Vue from 'vue';
    import Router from 'vue-router';
    import routeInterceptor from './hooks';
    ...
    router.beforeEach(routeInterceptor);
    // hooks/index.js
    /* hooks 目录用于配置路由钩子 */
    import routerViewChange from './routerViewChange';
    ...
    const allHooks = [
      /* 放置全部的路由钩子 */
      routerViewChange
    ];
    const routeInterceptor = ({ path: toPath, query: toQuery, matched }, { path: fromPath }, next) => {
      // 路由拦截
      if (matched.length === 0) {
        // 404页面
        next({ path: '/error/404' });
      } else {
        // 找到匹配当前路由的钩子
        const hookMatched = allHooks.filter(({ path: hookPath }) => {
          if (hookPath instanceof RegExp) {
            const routerPathReg = hookPath;
            return routerPathReg.test(toPath);
          }
          return hookPath === toPath;
        });
        const hookLen = hookMatched.length;
        if (hookLen) {
          let hookActived = 0;
          // 匹配到路由钩子后, 触发该路由下的全部钩子函数
          hookMatched.forEach((hook /* ,  idx */) => {
            hook.action({
              toPath,
              fromPath,
              toQuery,
              next(params) {
                // 使用计数器控制 保证全部路由钩子均执行完毕 (包括异步调用 next 函数)后, 才继续路由导航
                if (hookActived < hookLen - 1) {
                  hookActived += 1;
                  return;
                }
                next(params);
              }
            });
          });
        } else {
          next();
        }
      }
    };
    export default routeInterceptor;
    // hooks/routerViewChange.js
    import { ajax } from '@/utils';
    const routerViewChange = {
      path: /\/\w+?\//, // 匹配所有路由
      action: async({ toPath, next }) => {
        if (NODE_ENV === 'development') return;
        const response = await ajax.get(`../static/version.json`);
        if (VERSION !== response.data.version) {
          window.confirm('发现新版本,自动更新中...');
          window.location.reload(true);
        }
        next();
      }
    };
    export default routerViewChange;
    复制代码

    作者使用的是nginx,这里实现nginx对index.html的不缓存处理

    location ~ .*\.(htm|html)?$ {
        add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
        # 指明root,如果不在外层上指明root,需要在这里指明
        root /data/server/ui/vue-project/dist/;
    }
    复制代码

    最后产出的结果:

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论