微应用如何实现自动更新提示

文章讨论了在微应用环境下,如何实现在后台服务更新后自动提示用户刷新页面。提出了两种检测服务更新的方法,包括监听远程文件和通过接口通知,并选择了Server-SentEvents(SSE)作为通知客户端更新的方式。另外,通过webpack插件在构建时生成唯一的版本号,用于判断是否需要更新。最后,介绍了客户端如何处理更新提示以及在不同场景下请求更新检查的策略。
摘要由CSDN通过智能技术生成

首先, 先讲一下本次文章所讲的场景, 经过调研, 公司内部使用后台, 当有需求功能迭代的时候, 通常使用者会没有感知, 使用者只会在浏览器内一直打开这个页面, 当需要使用的时候, 再切换这个tab来使用.

这就导致使用者一直不知道系统更新了, 一直没有访问最新的页面(由于最新页面需要刷新浏览器, 重新对静态资源进行读取)

其实这个时候刷新一下网页就可以实现去访问新资源了

效果如下:

什么是微应用

先来介绍一下微应用的特点.

微应用就是有一个主应用服务负责登录和管理各个子应用, 通常企业中的后台就是这样的

比如有一个主应用app, 主应用中有很多很多子后台, 比如: 管理订单的(orderCenter), 管理用户信息的(userCenter), 管理考勤的(attendanceCenter)

前端微应用框架中, qiankun最受欢迎, 使用量和star数量都很高

如它的介绍: Blazing fast, simple and complete solution for micro frontends.

快速、简单、完整的微前端解决方案。

介绍就到这里, 如果还需要详细了解, 可以查看这篇文章:

基于 qiankun 的微前端应用实践

何时自动更新

当然解决问题的之前, 需要考虑清楚本次需求, 也就是最终要实现的效果

我们当然希望在项目重新构建完成之后, 在构建之前打开的网页能够知道项目重新构建了,然后提示用户去更新页面.

但是这个时候要能够选择是否更新, 因为如果用户正在填写表单, 那一更新页面, 表单数据没有提交, 用户肯定这个时候不希望再重新填写一遍. 这个时候要能够忽略更新, 然后不在提示, 等用户填写完成之后, 手动刷新就可以了.

如何实现自动更新

这部分其实就存在两个难点:

1.如何知道服务重新部署了?

方案1

在node端监听远程文件是否更改

如何监听文件是否更改?

node端可以很轻易监听本地文件是否变化, chokidar读取文件更改,不能监听远程服务的文件

但是当node服务与要监听的文件不是在同一台服务器, 需要去监听远程文件的变化, 就不简单

解决方案

可以通过get请求去读取远程服务器下的文件夹,会返回一个html,html中有最后修改时间和文件名

可以通过请求响应头里面的last-modified来判断是否更新

缺点:

1.需要有访问文件的权限, 通常在nginx层设置了不允许访问

2.需要node端轮询去查文件夹的最后更改时间

方案2:

项目部署之后, 通知node服务哪些系统重新部署了

如何通知?

可以请求一个node服务的接口, 参数传递哪个系统重新部署, 什么时候部署

缺点: 需要其他部门配合

2.如何通知应用更新?

方案1:

接入socket

什么是socket?

Socket 是网络编程中一种低级别的网络通信方式,它可以实现双向通信和实时通信。

这里主应用肯定是通讯的一方, 还需要有另一方, 来监听服务重新部署,所以这个方案需要有一个node后端服务, 来做通讯的另一方, 这个node服务可以与主应用进行通信.

但是socket是双向通信, 其实我们只需要服务端通知客户端就行了, 客户端无需与服务端通信, 所以这个方案不符合场景.

所以就有了第二种方案.

方案2:

SSE 全称是 Server Sent Event,翻译过来的意思就是 服务器派发事件。

一个网页获取新的数据通常需要发送一个请求到服务器,也就是向服务器请求的页面。

使用 server-sent 事件,服务器可以在任何时刻向我们的 Web 页面推送数据和信息。

这些被推送进来的信息可以在这个页面上作为 Events [0] + data 的形式来处理。

服务端

import { Router } from "express";
// import fetch from "node-fetch";

const sseServer = Router();
const sendData = { time: '' }

sseServer.all("*", function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
})


sseServer.get('/events', (req, res) => {
  // 设置响应头
  res.setHeader('Content-Type', 'text/event-stream');
  // res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  setInterval(() => {
    // console.log(sendData)
    res.write(`data: ${JSON.stringify(sendData)}\n\n`);
  }, 5000);
});

客户端:

const configEnv = import.meta.env
const source = new EventSource(`${configEnv.VITE_BASE_API}/sseServer/events`);
let oldStr = ''
// 监听 message 事件
source.onmessage = event => {
  let time = JSON.parse(event.data).time
  if (oldStr.length === 0) {
    oldStr = time
    console.log('connect success')
  }else if(oldStr !== time) {
    window.alert('系统更新啦~~~')
    oldStr = time
  }
}

SSE 与 Socket 有什么区别?

方式协议交互通道内容编码重连事件类型总结
SSEHTTP服务端单向推送默认文本默认支持断线重连支持自定义消息类型轻量级
WebSocketWS(基于 TCP 传输层的应用层协议,RFC6455[1] 对于它的定义标准)双向推送默认二进制手动实现NO扩展性、功能性强大

缺点:

Server Sent Events(SSE)对服务器端确实有一定要求:

  1. 支持长连接 - SSE 依赖于 HTTP 长连接,服务器需要能够保持连接,定期推送数据。

  2. 高并发 - 由于是长连接,每个客户端都会占用服务器的连接资源,高并发场景下会对服务器产生较大压力。

  3. 数据序列化 - 服务器需要将数据序列化为 SSE 格式的文本流进行推送,这会带来一定的 CPU 和内存消耗。

  4. 心跳机制 - 为了防止长时间空闲的连接被中间件关闭,服务器需要定期推送空数据给客户端(心跳数据)。

以上方案流程如下:

前端服务轮询实现

上面的解决方案, 都是需要node服务的

但是有没有更加简洁和方便的方案呢?

比如说: 只需要前端服务进行修改就能实现这样的需求

然后我看到了一篇文章的解决方案: 前端重新部署如何通知用户刷新网页?

根据打完包之后生成的script src 的hash值去判断,每次打包都会生成唯一的hash值,只要轮询去判断不一样了,那一定是重新部署了.

缺点:

1.需要将所有子应用配置更改

2.需要轮询去查看文化hash值是否发生改变

最佳解决方案

基于上面这个方法, 我想到了一种更加可行的方案

服务build的时候肯定是有node环境的, node环境是可以写文件的, build就是将静态资源打包

那如果build的时候维护一个版本号, 这个版本号每次build都不一样, 那是不是就可以用来判断是否更新啦

而且build的时候可以将这个版本号跟静态资源一同保存起来

所以写一个打包插件, 就能够将最新的版本号保存起来.那什么作为版本号呢? 思来想去, 其实代码提交的时候就会有一个commitId, 这个id是唯一, 且不会重复的.

node端去读取git提交时候的commitId, 代码如下:

import { execSync } from 'node:child_process'
export function getGitCommitHash() {
  try {
    return execSync('git rev-parse --short HEAD').toString().replace('\n', '').trim()
  }
  catch (err) {
    console.warn(`[web-auto-notify-plugin] Not a git repository!`)
    throw err
  }
}

由于公司中微应用是vue搭建的, 所以打包工具是vue-cli, 所以只需要写一个webpack插件就可以了

import type { Options } from '../../core/src/index'
import {
  DIRECTORY_NAME,
  JSON_FILE_NAME,
  generateJSONFileContent,
  getVersion,
} from '../../core/src/index'
import type { Compilation, Compiler } from 'webpack'

const pluginName = 'WebAutoNotifyPlugin'

type PluginOptions = Options & {
  indexHtmlFilePath?: string
}


class WebAutoNotifyPlugin {
  options: PluginOptions
  constructor(options: PluginOptions) {
    this.options = options || {}
  }

  apply(compiler: Compiler) {
    const { publicPath } = compiler.options.output
    if (this.options.injectFileBase === undefined)
      this.options.injectFileBase = typeof publicPath === 'string' ? publicPath : '/'

    const { versionType, customVersion, silence } = this.options
    let version = ''
    if (versionType === 'custom')
      version = getVersion(versionType, customVersion!)
    else
      version = getVersion(versionType!)

    compiler.hooks.emit.tap(pluginName, (compilation: Compilation) => {
      // const outputPath = compiler.outputPath
      const jsonFileContent = generateJSONFileContent(version, silence)
      // @ts-expect-error
      compilation.assets[`${this.options.injectFileBase}${DIRECTORY_NAME}/${JSON_FILE_NAME}.json`] = {
        source: () => jsonFileContent,
        size: () => jsonFileContent.length,
      }
    })
  }
}

export { WebAutoNotifyPlugin }

webpack内置了很多生命周期钩子, 方便插件使用.

plugins是可以用自身原型方法apply来实例化的对象。apply只在安装插件被Webpack compiler执行一次。apply方法传入一个webpck compiler的引用,来访问编译器回调。

compiler.hooks.emit.tap中的回调就是创建一个json文件, 这个json文件中就保存一个version字段.

这样就解决了第一个难点, 如何知道服务器重新部署了.

接着来解决第二个问题, 如何通知应用更新?

在客户端引入一个npm依赖包, 然后在项目中引入就行.

更新的话, 肯定就要去请求这个静态资源了, 但是什么时候去请求呢?我觉得在以下几种情况就需要去请求了

1.首次加载页面时

2.静态资源获取失败(404)

3.页面refocus或者revisible

但是还有一种情况, 就是用户正在使用时, 这个时候也需要提示更新了, 所以要有个定时任务, 来请求

当这个页面隐藏时, 就关闭定时器就可以了.

如何判断静态资源404

// listener script resource loading error
window.addEventListener(
  "error",
  err => {
    const errTagName = (err?.target as any)?.tagName;
    if (errTagName === "SCRIPT") {
      checkSystemUpdate();
    }
  },
  true
);

最终的流程如下:

好了, 全部的代码这里就不贴了, 优点占篇幅, 所以我留下git仓库地址:

https://github.com/0522skylar/web-auto-notify

可以先下载npm包体验一下:

https://www.npmjs.com/package/web-auto-notify-webpack

https://www.npmjs.com/package/web-auto-notify-client

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值