這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. 場景 前端構建完上線,用戶還停留還在老頁面,用戶不知道網頁重新部署了,跳轉頁面的時候有時候js連接hash變了導致報錯跳不過去,並且用戶體驗不到新功能。 2. 解決方案 每次打包寫入一個json文件,或者對比生成的script的sr ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
1. 場景
前端構建完上線,用戶還停留還在老頁面,用戶不知道網頁重新部署了,跳轉頁面的時候有時候js連接hash變了導致報錯跳不過去,並且用戶體驗不到新功能。
2. 解決方案
- 每次打包寫入一個json文件,或者對比生成的
script的src引入的hash地址或者etag
不同,輪詢調用,判斷是否更新 - 前端使用websocket長連接,具體是每次構建,打包後通知後端,更新後通過websocket通知前端
輪詢調用可以改成在前置路由守衛中調用,無需控制時間,用戶有操作才去調用判斷。
3. 具體實現
3.1 輪詢方式
參考小滿的實現稍微修改下:
class Monitor { private oldScript: string[] = [] private newScript: string[] = [] private oldEtag: string | null = null private newEtag: string | null = null dispatch: Record<string, (() => void)[]> = {} private stop = false constructor() { this.init() } async init() { console.log('初始化') const html: string = await this.getHtml() this.oldScript = this.parserScript(html) this.oldEtag = await this.getEtag() } // 獲取html async getHtml() { const html = await fetch('/').then((res) => res.text()) return html } // 獲取etag是否變化 async getEtag() { const res = await fetch('/') return res.headers.get('etag') } // 解析script標簽 parserScript(html: string) { const reg = /<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi return html.match(reg) as string[] } // 訂閱 on(key: 'update', fn: () => void) { ;(this.dispatch[key] || (this.dispatch[key] = [])).push(fn) return this } // 停止 pause() { this.stop = !this.stop } get value() { return { oldEtag: this.oldEtag, newEtag: this.newEtag, oldScript: this.oldScript, newScript: this.newScript, } } // 兩層對比有任一個變化即可 compare() { if (this.stop) return const oldLen = this.oldScript.length const newLen = Array.from( new Set(this.oldScript.concat(this.newScript)) ).length if (this.oldEtag !== this.newEtag || newLen !== oldLen) { this.dispatch.update.forEach((fn) => { fn() }) } } // 檢查更新 async check() { const newHtml = await this.getHtml() this.newScript = this.parserScript(newHtml) this.newEtag = await this.getEtag() this.compare() } } export const monitor = new Monitor() // 路由前置守衛中調用 import { monitor } from './monitor' monitor.on('update', () => { console.log('更新數據', monitor.value) Modal.confirm({ title: '更新提示', icon: createVNode(ExclamationCircleOutlined), content: '版本有更新,是否刷新頁面!', okText: '刷新', cancelText: '不刷新', onOk() { // 更新操作 location.reload() }, onCancel() { monitor.pause() }, }) }) router.beforeEach((to, from, next) => { monitor.check() })
3.2 websocket方式
既然後端不好溝通,那就自己實現一個完整版。
具體流程如下:
3.2.1 代碼實現
服務端使用koa實現:
// 引入依賴 koa koa-router koa-websocket short-uuid koa2-cors const Koa = require('koa') const Router = require('koa-router') const websockify = require('koa-websocket') const short = require('short-uuid') const cors = require('koa2-cors') const app = new Koa() // 使用koa2-cors中間件解決跨域 app.use(cors()) const router = new Router() // 使用 koa-websocket 將應用程式升級為 WebSocket 應用程式 const appWebSocket = websockify(app) // 存儲所有連接的客戶端進行去重處理 const clients = new Set() // 處理 WebSocket 連接 appWebSocket.ws.use((ctx, next) => { // 存儲新連接的客戶端 clients.add(ctx.websocket) // 處理連接關閉事件 ctx.websocket.on('close', () => { clients.delete(ctx.websocket) }) ctx.websocket.on('message', (data) => { ctx.websocket(666)//JSON.stringify(data) }) ctx.websocket.on('error', (err) => { clients.delete(ctx.websocket) }) return next(ctx) }) // 處理外部通知頁面更新的介面 router.get('/api/webhook1', (ctx) => { // 向所有連接的客戶端發送消息,使用uuid確保不重覆 clients.forEach((client) => { client.send(short.generate()) }) ctx.body = 'Message pushed successfully!' }) // 將路由註冊到應用程式 appWebSocket.use(router.routes()).use(router.allowedMethods()) // 啟動伺服器 appWebSocket.listen(3000, () => { console.log('Server started on port 3000') })
前端頁面代碼:
websocket使用vueuse封裝的,保持個心跳。
import { useWebSocket } from '@vueuse/core' const { open, data } = useWebSocket('ws://dev.shands.cn/ws', { heartbeat: { message: 'ping', interval: 5000, pongTimeout: 10000, }, immediate: true, // 自動連接 autoReconnect: { retries: 6, delay: 3000, }, }) watch(data, (val) => { if (val.length !== '3HkcPQUEdTpV6z735wxTum'.length) return Modal.confirm({ title: '更新提示', icon: createVNode(ExclamationCircleOutlined), content: '版本有更新,是否刷新頁面!', okText: '刷新', cancelText: '不刷新', onOk() { // 更新操作 location.reload() }, onCancel() {}, }) }) // 建立連接 onMounted(() => { open() }) // 斷開鏈接 onUnmounted(() => { close() })
3.2.2 發佈部署
後端部署:
考慮伺服器上沒有安裝node環境,直接使用docker進行部署,使用pm2運行node程式。
- 寫一個DockerFile,發佈鏡像
// Dockerfile: # 使用 Node.js 作為基礎鏡像 FROM node:14-alpine # 設置工作目錄 WORKDIR /app # 複製 package.json 和 package-lock.json 到容器中 COPY package.json ./ # 安裝項目依賴 RUN npm install RUN npm install -g pm2 # 複製所有源代碼到容器中 COPY . . # 暴露埠號 EXPOSE 3000 # 啟動應用程式 CMD ["pm2-runtime","app.js"]
本地進行打包鏡像發送到docker hub,使用docker build -t f5l5y5/websocket-server-image:v0.0.1 .
命令生成鏡像文件,使用docker push f5l5y5/websocket-server-image:v0.0.1
推送到自己的遠程倉庫
- 伺服器拉取鏡像,運行
拉取鏡像:docker pull f5l5y5/websocket-server-image:v0.0.1
運行鏡像: docker run -d -p 3000:3000 --name websocket-server f5l5y5/websocket-server-image:v0.0.1
可進入容器內部查看:docker exec -it <container_id> sh # 使用 sh 進入容器
查看容器運行情況:
進入容器內部查看程式運行情況,pm2常用命令
此時訪問/api/webhook1
會找到項目的對應路由下,需要配置下nginx代理轉發
- 配置nginx介面轉發
map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name test-pms.shands.cn; client_max_body_size 50M; location / { root /usr/local/openresty/nginx/html/test-pms-admin; try_files $uri $uri/ /index.html; } // 將觸發的更新代理到容器的3000 location /api/webhook1 { proxy_pass http://localhost:3000/api/webhook1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } // websocket 配置 location /ws { # 反向代理到容器中的WebSocket介面 proxy_pass http://localhost:3000; # 支持WebSocket協議 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } }
3.2.3 測試
url請求api/webhook即可
4. 總結
主要實踐下兩種方案:
-
輪詢調用方案:輪詢獲取網頁引入的腳本文件的hash值或者etag來實現。這種方案的優點是實現簡單,但存在性能消耗和延遲較高的問題。
-
WebSocket版本方案:在前端部署的同時建立一個WebSocket連接,將後端構建部署完成的通知發送給前端。當後端完成部署後,通過WebSocket向前端發送消息,提示用戶刷新頁面以載入最新版本。這種方案的優點是實時性好,用戶體驗較好,但需要在前端和後端都進行相應的配置和代碼開發。