likes
comments
collection
share

纯前端实现检测版本发布更新提示

作者站长头像
站长
· 阅读数 34

环境: Vue-cli、Vue2、JavaScript、webpack、Element-UI

背景

在用户正在访问单页面网站的情况下,突然发布了新的版本。而由于单页面中路由特性,或浏览器缓存的原因,并不会随着路由变化而重新加载前端资源,此时用户浏览器所运行的脚本,并非是最新的代码,从而可能引发一些问题。因此所引发了思考。如何在后端部署之后,提醒用户系统的版本更新,并且引导用户刷新页面,获取最新资源。

思路思考

可通过前端接收最新的版本信息,并且与本地的登陆时所保存的版本信息来进行比较,可使用轮训、websocket等技术来完成。

由于我的资历和能力有限,本文主要是以轮训为主,如果有更好的方案,请大佬在评论区多多指教!

轮询

说到前端轮询很多人就会想到用 setTimeout 或者 setInterval。定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。由于 setInterval执行的时间不准确,可能会导致的后果:某些间隔会被跳过、可能多个定时器会连续执行。所以往往会使用 setTimeout来实现setInterval

window.requestAnimationFrame()告诉浏览器———你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。回调函数执行次数通常是每秒 60 次。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。

纯前端实现思路

现在的前端项目都是工程化的,后端部署前都需要先打包,我们可以利用的是打包过程。

  • 利用webpack插件机制,在每次打包都可以先生成出一个文件,文件中保存的项目的唯一版本号;
  • 前端在系统登录或者首次加载时候,在首次获取到版本号,并且保存到本地,在前端利用轮询来间隔一段时间来获取版本号,并且与本地保存的版本号进行比较。
  • 不同则出现弹出提示刷新。

requestAnimationFrame 来实现 setInterval

const Interval = {
  timer: null,
  setInterval: function(callback, interval) {
    let startTime = new Date().valueOf();
    let endTime = new Date().valueOf();
    const self = this;
    const loop = function() {
      self.timer = requestAnimationFrame(loop);
      endTime = new Date().valueOf();
      if (endTime - startTime >= interval) {
        endTime = startTime = new Date().valueOf();
        callback && callback();
      }
    };
    this.timer = requestAnimationFrame(loop);
    return this.timer;
  },
  clearInterval: function() {
    cancelAnimationFrame(this.timer);
  },
};

实现webpack插件 —— 生成版本文件

大致内容就是:生成版本 json 文件并且加入到 public 文件夹下。

const fs = require("fs");
const path = require("path");
const Utils = require("./utils/index");

const NAME = "xkc-update-version";

function UpdateVersionWebpackPlugin(options) {
  this.options = {
    // json 版本文件名称
    versionFileName: "update_version.json",
    // json key 值
    keyName: "UPDATE_VERSION",
    ...options,
  };

  this.version = process.env[this.options.keyName] || `${Date.now()}.0.0`;
}

UpdateVersionWebpackPlugin.prototype.apply = function(compiler) {

  compiler.hooks.beforeRun.tap(NAME, () => {
    console.log(process.env.NODE_ENV);
    console.log("before run");
    
    // 生成的版本 json 文件建议放置在 public 文件夹下
    const filePath = path.resolve(Utils.resolveApp(), "public", this.options.versionFileName);
    console.log(filePath);
    
    // 生成文件
    generateFile(filePath, `{"${this.options.keyName}": "${this.version}"}`);
  });

  compiler.hooks.done.tap(NAME, () => {
    console.log("done ...");
  });
};

function generateFile(path, content) {
  fs.writeFileSync(path, content);
}

module.exports = UpdateVersionWebpackPlugin;

配置文件添加 webpack 插件

// .env
VUE_APP_OPEN_UPDATE_VERSION = 'false'
VUE_APP_UPDATE_VERSION = 'UPDATE_VERSION'
// vue.config.js
// 引入插件
const UpdateVersionWebpackPlugin = require("./src/js/plugins/UpdateVersionWebpackPlugin");

module.exports = {
    // ... ...
    

    if (process.env.VUE_APP_OPEN_UPDATE_VERSION === "true") {
      config
        .plugin("UpdateVersionWebpackPlugin")
        .use(new UpdateVersionWebpackPlugin({}));
    }
}

请求 JSON 文件方法

/**
 * 读取到更新json文件版本内容
 */
export function fetchUpdateVersionFile() {
  return new Promise((resolve, reject) => {
    // 注意:文件请求路径 /update_version.json,是相对于在 public 文件下的 index.html 的位置而言的,/update_version.json 代表 update_version.json 文件与 index.html 文件夹同目录。
    fetch("/update_version.json")
      .then((res) => {
        // console.log(res.body);
        return res.body;
      })
      .then((body) => {
        const reader = body.getReader();
        reader
          .read()
          .then((val) => {
            let str = "";
            for (let i = 0; i < val.value.length; i++) {
              str += String.fromCharCode(val.value[i]);
            }
            return JSON.parse(str);
          })
          .then((json) => {
            resolve(json);
          })
          .catch((err) => {
            reject(err);
          });
      })
      .catch((err) => {
        reject(err);
      });
  });
}

轮询请求并提示框提示

// await-to-js.js
export function to(promise, errorExt) {
  return promise
    .then((data) => [null, data])
    .catch((err) => {
      if (errorExt) {
        const parsedError = Object.assign({}, err, errorExt);
        return [parsedError, undefined];
      }

      return [err, undefined];
    });
}

export default to;

import { Interval } from "./index";
import { to } from "./await-to-js";

import { Notification } from "element-ui";


const UPDATE_VERSION = process.env.VUE_APP_UPDATE_VERSION;

export function openUpdateVersionNotify() {
  fetchUpdateVersionFile().then(
    (res) => {
      console.log("版本号:", res);
      if (!res[UPDATE_VERSION]) return;
      localStorage.setItem(UPDATE_VERSION, res[UPDATE_VERSION]);
      Interval.setInterval(async () => {
        const [err, res] = await to(fetchUpdateVersionFile());
        if (err) return;
        console.log(res);
        let currentVersion = localStorage.getItem(UPDATE_VERSION);
        if (res[UPDATE_VERSION] !== currentVersion) {
          console.log("版本更新了。。。");
          let notifyContainerDom = document.querySelectorAll(
            "#update_notify_container"
          );
          if (notifyContainerDom.length) return;
          const notify = Notification.warning({
            title: "系统更新提示",
            duration: 0,
            showClose: false,
            dangerouslyUseHTMLString: true,
            message: `
          <div id="update_notify_container">
            <button class="el-button el-button--primary el-button--mini update_notify_refresh_btn">
              刷新
            </button>
            <button class="el-button el-button el-button--mini update_notify_cancel_btn">取消</button>
          </div>`,
          });

          notify.$el.querySelector(
            ".update_notify_refresh_btn"
          ).onclick = () => {
            location.reload();
            notify.close();
          };

          notify.$el.querySelector(
            ".update_notify_cancel_btn"
          ).onclick = () => {
            Interval.clearInterval();
            notify.close();
          };
        }
      }, 1000 * 10);
    },
    (err) => {
      console.log("更新版本:", err);
    }
  );
}

项目的入口文件执行

// main.js

// 系统更新提示
import { openUpdateVersionNotify } from "@/js/utils/getUpdateVersion";
import Vue from "vue";
if (process.env.VUE_APP_OPEN_UPDATE_VERSION === "true") {
  openUpdateVersionNotify();
}

全局控制启动

通过修改 .env 文件中的 VUE_APP_OPEN_UPDATE_VERSION 值来控制是否开启此功能。

// .env
VUE_APP_OPEN_UPDATE_VERSION = 'false'  #关闭功能
VUE_APP_OPEN_UPDATE_VERSION = 'true'   #启动功能

有不同方案的大佬请在评论区多多指教!