Skip to content
霞露小伙 — HfWang
On this page

前端实现系统更新提示实现

前言

对于 SPA 应用来说,在新功能迭代升级的过程中,存在这样的一种情况,就是新版本发布上线了,但是用户又刚好在使用系统(没有获取到最新的代码),这时用户进行一些操作的时候很有可能会出现异常。所以针对这种情况,我们希望在新功能上线的时候给到用户一个强提示,通知用户刷新下系统那么实现这个方法一般有两种方式:

  1. 前端和后端使用 webscoket 实现通信,在每次发版的时候通知前端给用户提示
  2. 纯前端实现,利用 协商缓存 的标识符实现更新提示

第一种方式由于需要前后端的配合,且每次发版都需要后端手动添加通知,相对来是不如第二种方式来的直接的,所以我们接下来说的就是第二种方式是如何实现的。

原理

主要理由协商缓存的这两个字段: etag 和 last-modified 关于这两个响应头字段,如果不熟悉的话,推荐阅读这两篇文章

省流:

  • etag :http 响应头字段表示资源的版本, 是由 Web 服务器分配给在 URL 中找到的特定版本资源的不透明标识符。如果该 URL 的资源表示发生了变化,则会重新分配一个新的 ETag
  • last-modified : http 响应头也是用来标识资源的有效性, 不同的是使用修改时间而不是实体标签

下面是一个真实请求的响应头数据:

image-2023-02-05-20-14-40

实现

代码比较简单,就直接贴代码 ( js 版本的去掉类型提示即可)

javascript
// src/utils/SystemUpdate.ts
const SYSTEM_UPDAT_FLAG = 'system_update_flag'

export default class SystemUpdate {
    preSystemUpdateFlag: string | null = locatStorage.getItem(SYSTEM_UPDAT_FLAG) || null // 上一次构建的标志
    pollingTime: number // 轮询时间间隔
    timer: NodeJS.Timer | null = null

    /**
     * @param _pollingTime 轮询时间间隔,单位秒
     */
    constructor(_pollingTime?: number) {
      	// 默认 60s
        this.pollingTime = (_pollingTime || 60)  * 1000
    }

    /**
     * 获取系统最后构建的标志
     */
    getSystemLastBuildTimestamp = async () => {
        const url = window.location.origin
        const res = await fetch(url, {
            method: 'HEAD',
            cache: 'no-cache',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
            },
        })

        if (res.ok) {
            const { headers } = res
            return headers.get('etag') || headers.get('last-modified')
        } else {
            throw new Error('获取缓存标识符失败')
        }
    }

    /**
     * 判断系统是否更新
     * @param fn 回调函数
     */
    getSystemLastModified = async (fn: Function) => {
        const etag = await this.getSystemLastBuildTimestamp()
        if (etag !== this.preSystemUpdateFlag) {
            fn()
            this.preSystemUpdateFlag = etag
        } else {
            return
        }
    }

    /**
     * 开启监听
     * @param fn 用户自定义的回调函数
     */
    on = (fn: Function) => {
        this.timer = setInterval(() => {
            this.getSystemLastModified(fn)
        }, this.pollingTime)
    }

    /**
     * 移除监听
     */
    destroy = () => {
      	localStroage.setItem(SYSTEM_UPDAT_FLAG, this.preSystemUpdateFlag)
        clearInterval(this.timer as NodeJS.Timer)
    }
}

有一点要说的就是 getSystemLastBuildTimestamp 这个函数中, fetch 使用的是 HEAD 的请求方式

Get 向特定资源发出请求(请求指定页面信息,并返回实体主体)

Head 与服务器索与 get 请求一致的相应,响应体不会返回,获取包含在小消息头中的原信息(与 get 请求类似,返回的响应中没有具体内容,用于获取报头)具体可以看这里 ---> 接口请求(get、post、head 等)详解

Q: 为什么请求方式要使用 head 而不使用 get

A: 因为我们是在浏览器中去发起的这个请求,如果使用 get 的话,当我们请求到的 html 中有 iife 或其他外部链接时,浏览器就会进行额外的操作,增加不必要的开销;而如果使用 head 的话,其返回的响应体只有头信息,不会增加额外的操作

使用

vue

vue2 + js

html
<!-- App.vue -->
<template>// ...........</template>
<script>
import SystemUpdate from '@/common/systemUpdate'
export default {
  data() {
    return {
      su: null,
      // .....
    }
  },
  created() {
    this.su = new SystemUpdate(2)
    this.su.on(() => {
      this.$Message.warning({
        content: '系统有更新,请重新清理下浏览器缓存,重新加载页面',
        duration: 10,
        closable: true,
      })
    })
  },
  unmounted() {
    this.su.destroy()
  },
}
</script>

vue3 + ts

html
<!-- App.vue -->
<template> // ........... </template>
<script setup lang="ts">
  import SystemUpdate from '@/common/systemUpdate'
  import { onMounted, onUnMounted, ref } from 'vue'
  import { ElMessage } from 'element-plus'

  const su = ref<SystemUpdate | null>(null)

  onMounted(() => {
    su.value = new SystemUpdate(30)
    su.value.on(() => {
      ElMessage({
        message: '系统有更新,请重新清理下浏览器缓存,重新加载页面',
        type: 'warning',
      })
    })
  )

  onUnMounted(() => {
    su.value.destroy()
  })
</script>

react

jsx
// App.tsx
import React, { useRef } from 'react'
import SystemUpdate from '@/common/systemUpdate'
import { message } from 'antd'

const _su = new SystemUpdate(30)

export default function App() {
  // 使用 useRef 避免每次都重新生成 su 对象
	const su = useRef<SystemUpdate>(_su)

  useEffect(() => {
    su.current.on(() => {
      message.warning('系统有更新,请重新清理下浏览器缓存,重新加载页面')
    })
  } [])

  return (
    // ...........
  )
}

项目实践

oms-web 分支: ftx_temp_check_system_update

修改文件

  • config/webpack.dev.config.js
  • src/App.vue
  • src/common/SystemUpdate.js

问题:部分 ie 浏览器不支持 fetch 这个 api

解决方式:如果需要支持 ie, 需要在 getSystemLastBuildTimestamp 函数中判断是否支持 fetch,不支持的话改用 XMLHttpRequest 实现

大致实现过程如下:

js
getSystemLastBuildTimestamp = async () => {
  const url = window.location.origin

  if (window.fetch) {
    const res = await fetch(url, {
      method: 'HEAD',
      cache: 'no-cache',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
      },
    })

    if (res.ok) {
      const { headers } = res
      return headers.get('etag') || headers.get('last-modified')
    } else {
      throw new Error('获取缓存标识符失败')
    }
  } else {
    const xhr = new XMLHttpRequest()
    xhr.open('head', url, true)
    xhr.onload = (res) => {
      const { header } = res
      return headers.get('etag') || headers.get('last-modified')
    }
    xhr.send()
  }
}

大致思路是这样的, 具体逻辑还需要实际操作一下(我自己测试的时候没有做 ie 兼容,因为微软自己都不维护 ie,我为什么还要去兼容它,--------越想越有道理-------)

思考

  1. SSR(服务端渲染) 应用中怎么使用?或者说需要怎么修改才能支持 SSR?常见的 SSR 框架 ○ vue 的 Nuxtjs ○ react 的 Nextjs 由于 SSR 是在服务器上渲染成 html 后直接发生给浏览器的,所以没有 localStorage,怎么实现缓存上一次的标识?

  2. 假设我们有一次的请求响应时长超过了设置的轮询时长或者出现其他异常时会不会出现问题? 即出现响应异常的时候是继续轮询还是停止轮询?

  3. 当浏览器切换到其他标签页时,是否需要关闭轮询,等重新回到标签页时在恢复轮询?

第 2/3 点老wang觉得可以参考 vueRequest 的实现

补充

这里主要是关于思考部分的补充

  1. 关于思考部分第三点的补充 (2023-01-29 补充)

页面可见性 api

标签页是否可见的判断可以用这个来判断 visibilitychange ,这是 html5 提供的新的 api, 浏览器标签页被 隐藏或显示 的时候会触发 visibilitychange 事件,页面可见性 API 对于节省资源和提到性能特别有用,它使页面在文档不可见时避免执行不必要的任务

那么我们只需要在轮询请求前判断页面是否可见:

  • 如果页面可见,不干涉接下去的操作
  • 如果不可见,不发起请求即可

这样我们就可以实现在页面不可见时停止轮询请求,在页面可见时又恢复轮询请求

具体实现:

TypeScript
复制代码
/**
 * 判断系统是否更新
 * @param fn 回调函数
 */
getSystemLastModified = async (fn: Function) => {
    // 判断页面是否可见,若不可见直接返回
    const isDomVisibility = window?.document?.visibilityState === 'visible';
    if (!isDomVisibility) return

    try {
      // ....
    } catch(err) {
      // ....
    }
}

Q: 可以通过 document.addEventListener 实现全局可见性的事件注册,从而判断页面是否可见,但这里为什么在是 getSystemLastModified 函数里面去手动判断页面是否可见

A:我们这里目前只需要用到的只是页面是否可见的状态判断,而不需要用到对于的事件处理。当然,如果我们将轮询的操作放在 visibilitychange 事件里面也是可以的,但是在组件移除的时候就需要我们再去注销这个全局事件,相对来说有点弃简求繁了

visibilitychange 事件的具体使用方式

JavaScript
document.addEventListener("visibilitychange", () => {
  // 用户离开了当前页面
  if (document.visibilityState === "hidden") {
      document.title = "页面不可见";
  }
  // 用户打开或回到页面
  if (document.visibilityState === "visible") {
      document.title = "页面可见";
  }
});

本站中引用到的其他资料,如有侵权,请联系本人删除