背景
单页应用,项目更新时,部分用户会出更新不及时,导致异常的问题。
技术方案
给出版本号,项目每次更新时通知用户,版本已经更新需要刷新页面。
- 版本号更新方案
- 版本号变更后通知用户
- 哪些用户需要通知?
- 刷新页面进入的不通知。
- 第一次进入页面的不通知。
- 只有正在页面上使用的用户,项目发知变更时通知。
拉取最新版本号
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 thenngx.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 thenngx.log(ngx.ERR, "failed to open file: ", err)return nil, errendlocal content = file:read("*all")file:close()return contentend-- 文件路径local file_path = "/data/web-site/dist/web.version.json"-- 读取文件内容local file_content, err = read_file_content(file_path)if not file_content thenngx.log(ngx.ERR, "failed to read file: ", err)wss:send_close(1000, "file not found")returnendwhile true do-- 接收客户端消息local message, typ, err = wss:recv_frame()if not message thenngx.log(ngx.ERR, "failed to receive frame: ", err)return ngx.exit(444)endif typ == "close" then-- 当客户端发送关闭信号时,关闭连接wss:send_close()breakelseif typ == "text" then-- 当客户端发送文本信息时,对其进行处理ngx.log(ngx.INFO, "received message: ", message)-- 发送文本消息给客户端wss:send_text(file_content)endend-- 关闭 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: nobodyworker_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 Serverserver {# Port to listen on, can also be set in IP:PORT formatlisten 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 thenngx.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 thenngx.log(ngx.ERR, "failed to open file: ", err)return nil, errendlocal content = file:read("*all")file:close()return contentend-- 文件路径local file_path = "/tmp/web.version.json"-- 读取文件内容local file_content, err = read_file_content(file_path)if not file_content thenngx.log(ngx.ERR, "failed to read file: ", err)wss:send_close(1000, "file not found")returnendwhile true do-- 接收客户端消息local message, typ, err = wss:recv_frame()if not message thenngx.log(ngx.ERR, "failed to receive frame: ", err)return ngx.exit(444)endif typ == "close" then-- 当客户端发送关闭信号时,关闭连接wss:send_close()breakelseif typ == "text" then-- 当客户端发送文本信息时,对其进行处理ngx.log(ngx.INFO, "received message: ", message)-- 发送文本消息给客户端wss:send_text(file_content)endend-- 关闭 WebSocket 连接wss:send_close(1000, "bye")}}}
}