首先, 先讲一下本次文章所讲的场景, 经过调研, 公司内部使用后台, 当有需求功能迭代的时候, 通常使用者会没有感知, 使用者只会在浏览器内一直打开这个页面, 当需要使用的时候, 再切换这个tab来使用.
这就导致使用者一直不知道系统更新了, 一直没有访问最新的页面(由于最新页面需要刷新浏览器, 重新对静态资源进行读取)
其实这个时候刷新一下网页就可以实现去访问新资源了
效果如下:
什么是微应用
先来介绍一下微应用的特点.
微应用就是有一个主应用服务负责登录和管理各个子应用, 通常企业中的后台就是这样的
比如有一个主应用app, 主应用中有很多很多子后台, 比如: 管理订单的(orderCenter), 管理用户信息的(userCenter), 管理考勤的(attendanceCenter)
前端微应用框架中, qiankun最受欢迎, 使用量和star数量都很高
如它的介绍: Blazing fast, simple and complete solution for micro frontends.
快速、简单、完整的微前端解决方案。
介绍就到这里, 如果还需要详细了解, 可以查看这篇文章:
何时自动更新
当然解决问题的之前, 需要考虑清楚本次需求, 也就是最终要实现的效果
我们当然希望在项目重新构建完成之后, 在构建之前打开的网页能够知道项目重新构建了,然后提示用户去更新页面.
但是这个时候要能够选择是否更新, 因为如果用户正在填写表单, 那一更新页面, 表单数据没有提交, 用户肯定这个时候不希望再重新填写一遍. 这个时候要能够忽略更新, 然后不在提示, 等用户填写完成之后, 手动刷新就可以了.
如何实现自动更新
这部分其实就存在两个难点:
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 有什么区别?
方式 | 协议 | 交互通道 | 内容编码 | 重连 | 事件类型 | 总结 |
---|---|---|---|---|---|---|
SSE | HTTP | 服务端单向推送 | 默认文本 | 默认支持断线重连 | 支持自定义消息类型 | 轻量级 |
WebSocket | WS(基于 TCP 传输层的应用层协议,RFC6455[1] 对于它的定义标准) | 双向推送 | 默认二进制 | 手动实现 | NO | 扩展性、功能性强大 |
缺点:
Server Sent Events(SSE)对服务器端确实有一定要求:
-
支持长连接 - SSE 依赖于 HTTP 长连接,服务器需要能够保持连接,定期推送数据。
-
高并发 - 由于是长连接,每个客户端都会占用服务器的连接资源,高并发场景下会对服务器产生较大压力。
-
数据序列化 - 服务器需要将数据序列化为 SSE 格式的文本流进行推送,这会带来一定的 CPU 和内存消耗。
-
心跳机制 - 为了防止长时间空闲的连接被中间件关闭,服务器需要定期推送空数据给客户端(心跳数据)。
以上方案流程如下:
前端服务轮询实现
上面的解决方案, 都是需要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包体验一下: