Javascript is required
[微信]前端版本过低引导弹窗方案分享

原文

https://mp.weixin.qq.com/s/PT0PZ3S1Cvh2nltcIKwa3g

方案

弹窗内容

弹窗的触发条件

本地版本号和云端版本号。本地版本号是用户请求到的前端页面的代码版本号,是用户访问页面时决定;云端版本号可以理解为最新前端版本号,它是每次开发者发布前端代码时决定的。

        flowchart LR

1["打开网页"] 
2["获取本地和云端版本号"]
3["是否相同"]
4["不做操作"]
5["提示更新"]
1 --> 2
2 --> 3
3 -->|是| 4
3 -->|否| 5

      

解决方案

版本号的生成

本地版本号是用户访问时决定的,那无疑页面的 html 文件就是这个版本号存在的最佳载体,我们可以在打包时通过 plugin 给 html 文件注入一个版本号。

云端版本号

云端版本号的选择则有很多方式了,数据库、cdn 等等都可以满足需求。不过考虑到实现成本和泳道的情况,想了一下两个思路一个是打包的同时生成一个 version.json 文件,配一个路由去访问;另一个是直接访问对应的 html 代码,解析出注入的版本号,二者各自有适合的场景。

微前端的适配

我们现在的大多数项目都包含了主应用和子应用,那其实不管是子应用的更新还是主应用的更新都应该有相关的提示,而且相互独立,但同时又需要保证弹窗不同时出现。

想要沿用之前的方案其实只需要解决三个问题。

  1. 主子应用的本地版本号标识需要有区分,因为 html 文件只有一个,需要能在 html 文件中区分出哪个应用的版本是什么,这个我们只需在 plugin 中注入标识即可解决。
  2. 云端版本号请求时也要请求对应的云端版本号,这个目前采用的方案是主应用去请求唯一的 version.json 文件,因为主应用路由是唯一的,子应用则去请求最新的 html 资源文件,解析出云端版本号。
  3. 不重复弹窗我们只需要在展示弹窗前,多加一个是否已经有弹窗展示的判断即可了。

实现

监听时机和频控逻辑

代码

vue2 cli4.x

// npm
npm install --save-dev webpack-shell-plugin
// vue.config.js
const WebpackShellPlugin = require('webpack-shell-plugin');

module.exports = {
    configureWebpack: {
        plugins: [
           new WebpackShellPlugin({
             onBuildEnd: ['node scripts/createVersion.js']
          })
        ]
    }
};
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const packageJson = require('../package.json');


class VersionPlugin {
  constructor(params) {
    this._version = new Date().getTime(); // 版本时间
    this._name = params?.name || 'dist'; // 打包目录
    this._subName = params?.subName || packageJson.name // 前缀
  }
  apply(){
    try {
      const filePath = path.resolve(`./${this._name}/version.json`);
      // 1. version.json
      fs.writeFileSync(filePath, JSON.stringify({
        version: this._version,
      }, null, 2));
      // 2. index.html
      const htmlPath = path.resolve(`./${this._name}/view.html`);
      const data = fs.readFileSync(htmlPath);
      const $ = cheerio.load(data);
      $('body').append(`<div id="${this._subName}-versionTag" style="display: none">${this._version}</div>`);
      fs.writeFileSync(htmlPath, $.html());
    }catch (err) {
      console.log(err);
    }
  }
}


new VersionPlugin().apply()

弹窗

<template>
  <div>

  </div>
</template>
<script>
import moment from 'moment'
export default {
  name: 'checkVersion',
  data(){
    return {
      expireMinutes: 1,
      timer: null,
    }
  },
  methods: {
    visibleFn(e) {
      if(document.visibilityState === 'visible'){
        this.fetchVersion();
      }
    },
    compireVersion(latestVersion, currentVersion){
      console.log(latestVersion, currentVersion)
      try {
        if (latestVersion && currentVersion && latestVersion > currentVersion) {
          // 提醒过了就设置一个更新提示过期时间,一天内不需要再提示了,弹窗过期时间暂时全局只需要一个!!
          localStorage.setItem(`versionUpdateExpireTime`, moment().endOf('day').format('x'));
          this.notify()
        }
        // 设置下次比较时间
        localStorage.setItem(`versionInfoExpireTime`,  moment().add(this.expireMinutes, 'minutes').format('x'));
      }catch (e) {
        console.log(e)
      }
    },
    routerFn(e) {
      console.log('popstate', e)
    },
    notify(){
      this.$notify({
        title: '版本更新提示',
        dangerouslyUseHTMLString: true,
        position: 'bottom-right',
        message: `您已经长时间未使用此页面,在此期间平台有过更新,如您此时在页面中没有填写相关信息等操作,请点击刷新页面使用最新版本! <br/>
  <div style="display: flex; justify-content: space-between">
    <div></div>
    <button type="button" class="el-button el-button--primary el-button--mini" onclick="refresh()">
    刷新
</button>
</div>
`,
      });
    },
    fetchVersion(){
      /**
       *
       * @desc 为了防止打扰,版本更新每个应用 10分钟提示
       */
      // if (Number(localStorage.getItem(`versionUpdateExpireTime`) || 0) >= new Date().getTime()) {
      //   return;
      // }
      /**
       * @desc 不需要每次切换页面都去判断资源,每次从服务器获取到的版本信息,给半个小时的缓存时间,需要区分子应用
       */
      if (Number(localStorage.getItem(`versionInfoExpireTime`) || 0) > new Date().getTime()) {
        return;
      }
      const dom = document.getElementById('versionTag');
      if(dom){
        const currentVersion = Number(dom.innerText) || undefined;
        fetch(`/version.json?timestamp=${new Date().getTime()}`).then(res=>{
          if(res.status==200){
            return res.json();
          }
        }).then(res=>{
          if(Reflect.has(res, 'version')){
            const latestVersion = res.version;
            this.compireVersion(latestVersion, currentVersion);
          }
        })
      }
    },
  },
  destroyed() {
    document.removeEventListener('visibilitychange', this.visibleFn);
    window.removeEventListener('popstate', this.routerFn);
    if(this.timer){
      window.clearInterval(this.timer)
    }
  },
  mounted() {
    this.fetchVersion();
    window.refresh = ()=>{
      window.parent.location.reload();
    }
    this.timer = setInterval(()=>{
     this.fetchVersion();
    }, 1000*60*this.expireMinutes)
    document.addEventListener('visibilitychange', this.visibleFn);
    window.addEventListener('popstate', this.routerFn);
  },
}
</script>

vue3 vite4.x

import cheerio from 'cheerio';
import fs from 'fs';
import path from 'path';


interface OptionProps {
    outDir?: string;
    version?: string;
}

export default (option: OptionProps) => {
    let {
        outDir = 'dist',
        version = new Date().getTime(),
    } = option ?? {}

    return {
        name: 'vate-plugin-add-version',
        transformIndexHtml(html: string) {
            const $ = cheerio.load(html);
            $('body').append(`<div id="versionTag" style="display: none">${version}</div>`);
            return $.html();
        },
        writeBundle() {
            const versionData = {
                version
            };
            if (!fs.existsSync(outDir)) {
                fs.mkdirSync(outDir, { recursive: true });
            }
            fs.writeFileSync(path.join(outDir, 'version.json'), JSON.stringify(versionData, null, 2));
        },
    }
}
// vite.config.ts

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    createVersion({
      outDir: 'dist',
    })],
  build: {
    outDir: 'dist'
  },
  server: {
    port: 3000,
    host: "0.0.0.0"
  },
})

调试

发布成功后,可以根据如下步骤测试

  1. 删除 localstorage 中相关的 value
  2. 修改 html 中的 version,改成一个比较小的数值即可
  3. 切换路由,或者隐藏/打开页面,出现弹窗