一、常用消息推送方案概述

Web 端实现更新弹窗的方式主要有以下几种:轮询、长轮询、WebSocket、Server-Sent Events(SSE)以及 Service Worker + Push API(PWA 推送),几种方案各有优劣,下面做一个简单对比。

1. 轮询(Polling)

  • 原理:Web端定时向服务器发送请求,询问是否有更新。
  • 实现:使用 setIntervalsetTimeout 定期发送 HTTP 请求,服务器返回更新状态或数据。
  • 优点:实现简单,兼容性好。
  • 缺点:延迟高(依赖轮询间隔)、浪费带宽、增加服务器压力。
  • 适用场景:低频更新或对实时性要求不高的场景。

2. 长轮询(Long Polling)

  • 原理:Web端发起请求后,服务器保持连接直到有数据或超时,再重新发起请求。
  • 实现:Web端发送请求,服务器挂起直到有数据,返回数据后Web端立即发起新请求。
  • 优点:比轮询实时性更高,减少无效请求。
  • 缺点:服务器需处理大量挂起连接,可能增加资源消耗。
  • 适用场景:中等实时性需求。

3. WebSocket

  • 原理:基于 TCP 的全双工通信协议,建立持久连接后双方可主动推送消息。
  • 实现:Web端通过 new WebSocket(url) 建立连接,服务器监听 WebSocket 请求并维护连接池,Web端通过 onmessage 事件接收数据。
  • 优点:实时性强、低延迟、节省带宽。
  • 缺点:需服务器支持 WebSocket 协议(如 Node.js 的 ws 库),复杂度较高。
  • 适用场景:高频双向通信。

4. Server-Sent Events(SSE)

  • 原理:基于 HTTP 的单向通信,服务器可主动推送数据到Web端。
  • 实现:Web端通过 EventSource API 订阅服务器事件流,服务器返回 Content-Type: text/event-stream 的响应,持续发送数据。
  • 优点:简单(仅需 HTTP)、自动重连、轻量级。
  • 缺点:仅支持文本数据、单向通信(服务器到Web端)。
  • 适用场景:单向实时更新。

5. Service Worker + Push API(PWA 推送)

  • 原理:通过浏览器后台服务(Service Worker)接收服务器推送,即使页面关闭也能显示通知。
  • 实现:Web端注册 Service Worker 并请求通知权限,服务器通过 Web Push Protocol 发送加密消息(需 VAPID 密钥),Web端Service Worker 的 push 事件触发通知。
  • 优点:离线推送、类似原生应用的体验。
  • 缺点:需 HTTPS、用户需授权通知权限、实现较复杂。
  • 适用场景:渐进式 Web 应用(PWA)的离线通知(如邮件提醒)。

通过以上各方案的对比,我们可以知道对于简单低频的更新,使用轮询或长轮询即可。实时性要求高优先选 WebSocket(双向)或 SSE(单向)。有离线推送需求可以使用 Service Worker + Push API。我们要实现的网页版本更新弹窗属于比较简单低频的更新,可能一周或者更长时间才会更新一个版本,所以这里使用轮询或长轮询就够了。

长轮询需要服务端支持,别人未必有空搭理我们,也不考虑。剩最后一个方案,轮询,那么如何在不需要服务端支持的情况下,实现这个功能呢?答案是,前端自己定义一个 json 文件,每次轮询都去请求这份文件,在打包构建阶段去更新文件里面的版本相关字段,这样部署完成后,我们请求到的最新版本号就是最新的了。

消息推送的方案我们有了,接着是如何去更新网页呢?我们知道有时候刷新一下,网页还是旧的,这就涉及到浏览器的缓存策略了。

二、网页更新方法概述

在日常开发中有时候会遇到一个问题,就是我的代码明明已经推到服务器上部署好了,刷新界面,还是旧的,有时再刷新一下就是新的页面了,但是有时刷新很多次也还是旧的界面,这里我们可能会想到去抓包,查看是否返回了 304, 是的话,我们再试试 ctrl+f5。

这里界面没更新,我们做的两件事情的依据就是浏览器的缓存机制。第一次刷新界面,可能命中了浏览器的强缓存,资源文件还是拿的本地的缓存,根本没有去请求浏览器资源。所以我们 f5 刷新一下,企图跳过强缓存。跳过强缓存有时候界面还是旧的,可能命中了协商缓存,返回 304,那我们就试试用 ctrl+f5 跳过他,直接拿服务器上的资源。

从以上例子可知,浏览器缓存策略分为:强缓存和协商缓存,强缓存优先级高于协商缓存。那么浏览器什么时候会缓存服务器返回的资源呢?当我们从地址栏跳到一个 url 时,浏览器会缓存请求返回的数据和缓存相关标识,下次请求时会根据对应的规则采用对应的策略拿数据。

通过以上分析,我们知道网页文件要更新到部署的最新版本,我们需要跳过浏览器缓存,直接从服务器请求资源,主要有以下几种方法:

1、代码构建时,文件名加哈希值

2、服务端配置默认不强缓存

# Nginx 示例:对 HTML 文件禁用缓存
location / {
if ($request_filename ~* .*\.(html|htm)$) {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}
}

关键头字段:

  • Cache-Control: no-cache:每次请求需向服务器验证资源是否最新(针对 index.html 这种入口文件)。
  • Cache-Control: max-age=31536000, immutable:对带哈希的静态资源(如 JS/CSS)设置长期缓存,避免重复请求

3、网页强制刷新

通过版本检测,调用 location.reload 强制页面刷新

三、基于轮询的网页更新弹窗实现

1. 逻辑概述

  • 版本检测:通过对比当前版本号和本地 version.json 文件中的最新版本号,检测版本是否有更新(在每次构建时向 version.json 写入最新版本号,请求的时候注意在 version.json 后面加上新的 query 值,避免请求到缓存的文件)
  • 网页更新:当版本不一致时显示模态弹窗,提示用户需要刷新才能用到最新版本,若用户确定刷新,刷新并将最新的版本号缓存到浏览器中,关闭弹窗,否则关闭弹窗

2. demo

  • version.json
{
"version": "2.2.0"
}
  • checkUpdate.js
class UpdateChecker {
constructor(options = {}) {
this.options = {
checkInterval: 1 * 60 * 1000, // 1分钟检查一次
versionFile: '/version.json?t=',
...options
};
const version = this.getVersion();
this.currentVersion = version || '0.0.0';
this.setVersion(this.currentVersion);
this.timer = null;
this.init();
}

setVersion(version) {
localStorage.setItem('version', version);
}

getVersion() {
return localStorage.getItem('version');
}

async init() {
// 首次检查版本
await this.checkVersion();
// 定时检查
this.timer = setInterval(
() => this.checkVersion(),
this.options.checkInterval
);
}

async checkVersion() {
try {
console.log(`Checking version: ${Date.now()}`);
const res = await fetch(this.options.versionFile + Date.now());
const data = await res.json();

if (data.version !== this.currentVersion) {
const dialog = document.querySelector('.update-dialog');
if (dialog) return;
this.showUpdateDialog(data.version);
}
} catch (error) {
console.error('Version check failed:', error);
}
}

showUpdateDialog(version) {
const dialog = document.createElement('div');
dialog.className = 'update-dialog';
dialog.innerHTML = `
<div class="dialog-content">
<h3>发现新版本 🚀</h3>
<p>请刷新页面获取最新功能</p>
<div class="actions">
<button class="refresh">立即更新</button>
<button class="later">稍后提醒</button>
</div>
</div>
`;

dialog.querySelector('.refresh').addEventListener('click', () => {
this.currentVersion = version;
this.setVersion(this.currentVersion);
clearInterval(this.timer);
window.location.reload(true);
});

dialog.querySelector('.later').addEventListener('click', () => {
document.body.removeChild(dialog);
});

document.body.appendChild(dialog);
}
}

// 初始化检测
new UpdateChecker();
  • dialog.css
.update-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

.dialog-content {
background: white;
padding: 2rem;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.actions {
margin-top: 1.5rem;
display: flex;
gap: 1rem;
justify-content: center;
}

button.refresh {
background: #1890ff;
color: white;
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}

button.later {
background: #f5f5f5;
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}

参考文献:

说说浏览器缓存机制