Skip to content
On this page

前端部署通知用户刷新网页

业务场景

前端打包后,部署上线,用户有可能在部署前是停留在前版本页面上,不清楚网页重新部署了,点击其他菜单时,有时候 JavaScript 中 hash 变了导致报错,无法跳转至新的页面。

考虑方案

  • 通过每次发版更新 version,前端轮询 version 判断是否已更新版本

  • 根据打包完生成的 script hash 值轮询对比判断是否一样;不一样,则更新了版本

第一种方案

考虑每次打包自动生成 version 文件,版本号根据打包时间撮生成。

自动写入 version.json 文件

新建 webpack 插件 version-plugin.js

js
const fs = require('fs');
const path = require('path');

module.exports = class VersionPlugin {
    constructor({ rootDir = 'public', ...options }) {
        this.options = options;
        this.rootDir = rootDir;
    }

    writeVersionFile() {
        const version = {
            version: new Date().getTime(),
            ...this.options
        };
        const data = `${JSON.stringify(version)}`;
        fs.writeFileSync(
            path.resolve(__dirname, `../${this.rootDir}/version.json`),
            data
        );
    }

    apply() {
        this.writeVersionFile();
    }
};

在项目中添加相关插件

js
configureWebpack: config => {
    config.plugins.push(
        new VersionPlugin({
            tag: 0.01
        })
    );
},

当编译打包时,会在默认 public 文件夹内自动生成 version.json 文件

json
{"version":1673330288607,"tag":0.01}

创建通知更新逻辑

查看代码
js
/**
 * 通知更新版本
 */
import { http } from './request';

class NoticeVersion {
    oldVersion = '';
    newVersion = '';
    dispatch = {};
    constructor({ timer }) {
        this.oldVersion = '';
        this.newVersion = '';
        this.dispatch = {};
        this.polling = null;

        this.init();
        this.timering(timer);

    }

    async init() {
        const { version } = await this.getVersion();
        this.oldVersion = version;
    }

    async getVersion() {
        try {
            const uri = process.env.VUE_APP_PUBLIC_PATH + 'version.json';
            const { data } = await http.get(uri);
            return data;
        } catch (error) {
            console.log('获取版本信息错误', JSON.stringify(error));
            this.clear();
        }
    }

    on(key, func) {
        if (!this.dispatch[key]) {
            this.dispatch[key] = [];
        }
        this.dispatch[key].push(func);
        return this;
    }

    compare() {
        if (this.oldVersion != this.newVersion) {
            this.dispatch['update'].forEach((func) => {
                func();
            });
        } else {
            this.dispatch['no-update'].forEach((func) => {
                func();
            });
        }
    }

    timering(timer = 1000) {
        this.polling = setInterval(async () => {
            const { version } = await this.getVersion();
            this.newVersion = version;
            this.compare();
        }, timer);
    }

    clear() {
        if (!this.polling) {
            clearInterval(this.polling);
        }
        this.polling = null;
    }
}

export default {
    install() {
        //实例化该类
        const up = new NoticeVersion({
            timer: 2000,
        });
        //未更新通知
        up.on('no-update', () => {
            console.log('未更新');
        });
        //更新通知
        up.on('update', () => {
            console.log('更新了');
        });
    },
};

第二种方案

第二种其实和第一种差不多,不过不需要 webpack 插件,只需要获取 html 页面的 script

js
export class NoticeVersion {
    oldScript = []
    newScript = []
    dispatch = {}
    constructor(options) {
        this.oldScript = [];
        this.newScript = []
        this.dispatch = {}
        this.init()
        this.timering(options?.timer) // 轮询
    }


    async init() {
        const html = await this.getHtml()
        this.oldScript = this.parserScript(html)
    }

    async getHtml() {
        const html = await fetch('/').then(res => res.text()); // 读取index html
        return html
    }

    parserScript(html) {
        const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/ig)
        return html.match(reg) // 匹配script标签
    }

    //发布订阅通知
    on(key, func) {
        if (!this.dispatch[key]) {
            this.dispatch[key] = [];
        }
        this.dispatch[key].push(func);
        return this;
    }

    compare(oldArr, newArr) {
        const base = oldArr.length
        const arr = Array.from(new Set(oldArr.concat(newArr)))
        // 如果新旧length 一样无更新
        if (arr.length === base) {
            this.dispatch['no-update'].forEach(fn => {
                fn()
            })
        
        } else {
            // 否则通知更新
            this.dispatch['update'].forEach(fn => {
                fn()
            })
        }
    }

    timering(time = 10000) {
         //轮询
        setInterval(async () => {
            const newHtml = await this.getHtml()
            this.newScript = this.parserScript(newHtml)
            this.compare(this.oldScript, this.newScript)
        }, time)
    }

}

第三种方案

或者直接使用 Vue.config.errorHandler 错误捕获了某种错误直接强制刷新页面

js
Vue.config.errorHandler = (err, vm, info) => {
  console.log(err)
  let isNeedRefreshErr = false;
  if (err.message.indexOf('Failed to fetch dynamically imported module') > -1 || err.message.indexOf('\'text/html\' is not a valid JavaScript MI') > -1) {
    isNeedRefreshErr = true
  }

  if (err.message && isNeedRefreshErr) {
    // TODO
  }
}

去掉 HTML 缓存

HTMLmeta 上设置不缓存

HTML
<meta http-equiv="Pragma" content="no-cache" />  // 旧协议
<meta http-equiv="Expires" content="0" /> // 旧协议
<meta http-equiv="Cache-Control" content="no-cache" /> // 目前主流

nginx 配置禁用 HTML 缓存

conf
location / {
    root **;  
    # 配置页面不缓存html和htm结尾的文件
    if ($request_filename ~* .*.(?:htm|html)$) 
    {
        add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
    }
    index  index.html index.htm;
}
  • Private 会影响到 CDN 缓存命中率,但本身 CDN 缓存也会存在版本问题,量不大的情况下也可以禁掉
  • No-cache 可以使用缓存,但是使用前必须到服务端验证,可以是 no-cache,也可以是 max-age=0
  • No-store 完全禁用缓存
  • Must-revalidateno-cache 类似,强制到服务端验证,作用于一些特殊场景,例如发送了校验请求,但发送失败了之类
  • Proxy-revalidate 与上面类似,要求代理缓存服务验证有效性

总结

推荐使用第一种版本号来控制,可以生成其他版本信息,更清晰查看当前版本描述。

也可以使用 plugin-web-update-notification 插件。