背景
单页应用,项目更新时,部分用户会出更新不及时,导致异常的问题。
技术方案
给出版本号,项目每次更新时通知用户,版本已经更新需要刷新页面。
- 版本号更新方案
- 版本号变更后通知用户
- 哪些用户需要通知?
- 刷新页面进入的不通知。
- 第一次进入页面的不通知。
- 只有正在页面上使用的用户,项目发知变更时通知。
拉取最新版本号
1. 创建版本号文件
文件名 web.version.json
{
"name": "web-site",
"commitId": "GitCommitId",
"date": "BuildDate",
"nexttick": 10000
}
2. 更新版本号文件
每次部署时对版本号进行更新
commit ID 当前项目的最新的提交ID
当前日期
多久检查一次
#!/bin/bash
# 修改日期
sed -i "s/\"BuildDate\"/\"$(date '+%Y%m%d')\"/g" ./public/web.version.json;
# commit id
sed -i "s/\"GitCommitId\"/\"$(git rev-parse HEAD)\"/g" ./public/web.version.json;
替换完成的结果
{
"name": "web-site",
"commitId": "c09ecb450f4fb214143121769b0aa1546991dab6",
"date": "20241112",
"nexttick": 10000
}
3. 部署文件到 生产环境
内部上线流程,将文件部署到生产环境。 自由发挥这里不做细述。
4. 获取版本号文件
由于是跟静态文件直接部署在一起可以直接通过 http 请求获取到它。
4.1 http 轮询
- 获取版本文件
- 比对版本文件
- 通知用户消息弹窗
提取配置的主要代码
const LOCALVERSIONNAME = 'version';
interface IVersion {
name: string;
commitId: string;
date: string;
nexttick: number;
}
export const getRemoteVersion = () =>
fetch('/web.version.json', {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.blob();
})
.then((data) => {
return data.text().then((jsonStr) => {
return JSON.parse(jsonStr);
});
});
export const getLocalVersion = (current: IVersion): IVersion => {
const version = window.localStorage.getItem(LOCALVERSIONNAME);
if (version === null) {
window.localStorage.setItem(LOCALVERSIONNAME, JSON.stringify(current));
return current;
}
try {
return JSON.parse(version) as IVersion;
} catch {
return current;
}
};
export const checkVersion = () => {
return getRemoteVersion()
.then((remoteVersion: IVersion) => {
const localVersion = getLocalVersion(remoteVersion) as IVersion;
return { localVersion, remoteVersion };
})
.then(({ localVersion, remoteVersion }) => {
return new Promise((resolve, reject) => {
if (
localVersion.date !== remoteVersion.date ||
localVersion.commitId !== remoteVersion.commitId
) {
window.localStorage.setItem(
LOCALVERSIONNAME,
JSON.stringify(remoteVersion)
);
resolve(remoteVersion);
} else {
reject(remoteVersion);
}
});
});
};
export default { getRemoteVersion, getLocalVersion, checkVersion };
4.2 websocket 长链接通知。
配置 openrestry 主要逻辑
将 文件 web.version.json 返回回来。
location /ws {
content_by_lua_block {
local ws = require "resty.websocket.server"
local wss, err = ws:new()
if not wss then
ngx.log(ngx.ERR, "failed to new websocket: ", err)
return ngx.exit(500)
end
-- 函数用于读取文件内容
local function read_file_content(file_path)
local file, err = io.open(file_path, "r")
if not file then
ngx.log(ngx.ERR, "failed to open file: ", err)
return nil, err
end
local content = file:read("*all")
file:close()
return content
end
-- 文件路径
local file_path = "/data/web-site/dist/web.version.json"
-- 读取文件内容
local file_content, err = read_file_content(file_path)
if not file_content then
ngx.log(ngx.ERR, "failed to read file: ", err)
wss:send_close(1000, "file not found")
return
end
while true do
-- 接收客户端消息
local message, typ, err = wss:recv_frame()
if not message then
ngx.log(ngx.ERR, "failed to receive frame: ", err)
return ngx.exit(444)
end
if typ == "close" then
-- 当客户端发送关闭信号时,关闭连接
wss:send_close()
break
elseif typ == "text" then
-- 当客户端发送文本信息时,对其进行处理
ngx.log(ngx.INFO, "received message: ", message)
-- 发送文本消息给客户端
wss:send_text(file_content)
end
end
-- 关闭 WebSocket 连接
wss:send_close(1000, "bye")
}
}
客户端获取配置
通过 websocket 将配置获取回来
const socket = new WebSocket('ws://localhost:8055/ws')
socket.addEventListener("open", (event) => {
console.log(event);
socket.send('v')
});
socket.addEventListener('message', (event) => {
console.log(event.data)
})
通知用户
onMounted(() => {
const toCheckVersion = () =>
checkVersion()
.then(() => {
const id = `${Date.now()}`;
Notification.info({
id,
title: 'Site Update',
content: 'Please refresh the page to use the latest version',
duration: 0,
footer: () =>
h(Space, {}, [
h(
Button,
{
type: 'primary',
size: 'small',
onClick: () => window.location.reload(),
},
'OK'
),
]),
});
})
.catch((remoteVersion) => {
setTimeout(toCheckVersion, remoteVersion.nexttick);
});
toCheckVersion();
});
完整 openrestry 代码
# Based on https://www.nginx.com/resources/wiki/start/topics/examples/full/#nginx-conf
# user www www; ## Default: nobody
worker_processes auto;
error_log "/opt/bitnami/openresty/nginx/logs/error.log";
pid "/opt/bitnami/openresty/nginx/tmp/nginx.pid";
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log "/opt/bitnami/openresty/nginx/logs/access.log" main;
add_header X-Frame-Options SAMEORIGIN;
client_body_temp_path "/opt/bitnami/openresty/nginx/tmp/client_body" 1 2;
proxy_temp_path "/opt/bitnami/openresty/nginx/tmp/proxy" 1 2;
fastcgi_temp_path "/opt/bitnami/openresty/nginx/tmp/fastcgi" 1 2;
scgi_temp_path "/opt/bitnami/openresty/nginx/tmp/scgi" 1 2;
uwsgi_temp_path "/opt/bitnami/openresty/nginx/tmp/uwsgi" 1 2;
sendfile on;
tcp_nopush on;
tcp_nodelay off;
gzip on;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_proxied any;
gzip_types text/plain text/css application/javascript text/xml application/xml+rss;
keepalive_timeout 65;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 80M;
server_tokens off;
# HTTP Server
server {
# Port to listen on, can also be set in IP:PORT format
listen 8080;
location /status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
location /ws {
content_by_lua_block {
local ws = require "resty.websocket.server"
local wss, err = ws:new()
if not wss then
ngx.log(ngx.ERR, "failed to new websocket: ", err)
return ngx.exit(500)
end
-- 函数用于读取文件内容
local function read_file_content(file_path)
local file, err = io.open(file_path, "r")
if not file then
ngx.log(ngx.ERR, "failed to open file: ", err)
return nil, err
end
local content = file:read("*all")
file:close()
return content
end
-- 文件路径
local file_path = "/tmp/web.version.json"
-- 读取文件内容
local file_content, err = read_file_content(file_path)
if not file_content then
ngx.log(ngx.ERR, "failed to read file: ", err)
wss:send_close(1000, "file not found")
return
end
while true do
-- 接收客户端消息
local message, typ, err = wss:recv_frame()
if not message then
ngx.log(ngx.ERR, "failed to receive frame: ", err)
return ngx.exit(444)
end
if typ == "close" then
-- 当客户端发送关闭信号时,关闭连接
wss:send_close()
break
elseif typ == "text" then
-- 当客户端发送文本信息时,对其进行处理
ngx.log(ngx.INFO, "received message: ", message)
-- 发送文本消息给客户端
wss:send_text(file_content)
end
end
-- 关闭 WebSocket 连接
wss:send_close(1000, "bye")
}
}
}
}