Skip to content

前端重新部署刷新网页

介绍

当我们重新部署前端应用或者后台服务时,需要一种机制来通知所有停留在系统的用户刷新页面。从而防止用户停留在系统改动过的页面或调用了调整过的接口中。

在网上有很多种解决方案,很我选择了最简单通俗易懂的方法。通过轮询脚本来告知用户,如果检测到应用有改动,则进行提示或强制刷新。

在我的开源项目 VAdmire Admin 也进行简单的监听前端项目更新部署通知的实现。

原理就是:在项目构建开始时生成一个当前构建项目的时间戳,并将其写入到 public/xx.json 文件中。之后每次进入系统时都会存储下当前系统构建时的时间戳。在次之后设置指定时间去请求该 .json 文件,进行时间戳对比。

如果发现时间戳不一致并且大于存储下的当前系统构建时的时间戳,则说明项目有更新,则进行提示或强制刷新。

而如果需要监听后端项目也可以与后台沟通,在项目构建时按照同样的方式记录下当前时间戳并且存储到一个前端可以访问地方,前端轮询的去请求对应的接口即可。

实现方案

以下代码是 VAdmire Admin 针对前端部署的实现方案

构建开始时,存储当前构建时的时间戳

ts
export default () => {
  return {
    plugins: [
      // Build Start Plugin
      {
        name: 'vite-plugin-vadmire-build-start',
        buildStart: () => {
          console.log('vite-plugin-vadmire-build-start:', useFormatDate('yyyy-MM-dd hh:mm:ss', buildTimestamp));
          writeFileSync(
            `${useGetCurrentPath()}/public/config.json`,
            `{
  "buildTime": ${buildTimestamp}
}`,
            { encoding: 'utf-8' },
          );
        },
      },
    ],
  };
};

通知部署刷新页面类 DeployReload

ts
/**
 * fetchUrl: 请求的地址
 * fetchKey: 获取的字段
 * isListening: 是否监听
 * checkTimeout: 多久检查一次
 * execute: 是否立即执行
 * reloadCallback: 更新执行回调
 */
interface DeployReloadOptions {
  fetchUrl?: string;
  fetchKey?: string;
  isListening?: boolean;
  checkTimeout?: number;
  execute?: boolean;
  reloadCallback?: () => void;
}

export class DeployReload {
  private fetchUrl: string;
  private fetchKey: string;
  private isListening: boolean;
  private stashBuildTime: number = 0;
  private checkTimeout: number; // Second
  private execute: boolean = false;
  private intervalInstance: any;

  private reloadCallback: () => void;

  constructor(options: DeployReloadOptions) {
    const defaultFetchUrl = `${window.location.origin}/config.json`;
    this.fetchUrl = options.fetchUrl || defaultFetchUrl;
    if (!this.fetchUrl) {
      console.error('fetchUrl is required in DeployReload');
    }

    this.fetchKey = options.fetchKey || 'buildTime';
    this.isListening = options.isListening || true;
    this.checkTimeout = options.checkTimeout || 5;
    this.execute = options.execute || false;

    this.reloadCallback = options.reloadCallback || (() => {});

    this.init();
  }

  private init() {
    this.getBuildTimeValue().then((value) => {
      this.stashBuildTime = value;
    });

    const timeoutValue = this.checkTimeout * 1000;
    const callback = this.checkBuildTime.bind(this);
    this.execute && callback();
    this.intervalInstance = setInterval(callback, timeoutValue);

    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        this.execute && callback();
        this.intervalInstance = setInterval(callback, timeoutValue);
      } else {
        clearInterval(this.intervalInstance);
      }
    });
  }

  private async getBuildTimeValue() {
    const response = await fetch(`${this.fetchUrl}?time=${new Date().getTime()}`);
    const result = (await response.json()) as Record<string, any>;
    return result[this.fetchKey] as number;
  }

  private async checkBuildTime() {
    if (this.isListening && this.fetchUrl) {
      const currentBuildTime = await this.getBuildTimeValue();
      if (currentBuildTime > this.stashBuildTime) {
        this.reloadCallback();
      }
    }
  }

  public open() {
    this.isListening = true;
  }

  public close() {
    this.isListening = false;
  }
}

前端提示或强制刷新

ts
// listening system whether updated
const dialog = useDialog();
const isOpenDialog = ref(true);
const { isOpenDeployReload } = storeToRefs(useVAdmireConfigStore());
const deployReload = new DeployReload({
  isListening: false,
  checkTimeout: 60 * 5,
  execute: true,
  reloadCallback() {
    if (isOpenDialog.value) {
      isOpenDialog.value = false;
      dialog.warning({
        title: '温馨提示',
        content: '检测到系统发生更新,请重新加载页面',
        positiveText: '更新',
        negativeText: '不更新',
        closable: false,
        closeOnEsc: false,
        maskClosable: false,
        onPositiveClick: () => {
          isOpenDialog.value = true;
          location.reload();
        },
        onNegativeClick: () => {
          isOpenDialog.value = true;
        },
      });
    }
  },
});

watch(
  () => isOpenDeployReload.value,
  (newVal) => {
    if (newVal) {
      deployReload.open();
    } else {
      deployReload.close();
    }
  },
  { immediate: true },
);