在构建支持离线能力的渐进式Web应用时,把消息、通知和会话数据放进客户端的SQLite数据库已经成为不少团队的实际选择。配合Badging API,应用可以把SQLite里计算出来的未读总数直接显示在系统桌面的图标角标上,让用户不打开页面也能感知待处理任务量。这种方案既降低了对服务端的轮询压力,也解决了纯内存状态在页面刷新后丢失的问题。

SQLite本地存储与徽章计数的数据模型设计
在一个典型的SQLite实战项目里,我们首先要定义清楚哪些数据会影响徽章数字。最常见的是一张会话表conversation和一张消息表message,其中message带有is_read字段和conv_id外键。徽章计数本质上就是所有会话中未读消息的总和,因此不需要单独建计数表,每次通过聚合查询就能拿到准确值。
为了避免重复计算,可以在conversation表上冗余一个unread_count字段,由写入消息的事务同步维护。这样徽章更新时只需SELECT SUM(unread_count) FROM conversation,而不用扫描全量消息。下面是用SQL创建表的示例,注意在SQLite中整数主键默认自增,且外键需要开启PRAGMA foreign_keys = ON才生效。
PRAGMA foreign_keys = ON; CREATE TABLE conversation ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, unread_count INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE message ( id INTEGER PRIMARY KEY, conv_id INTEGER NOT NULL, content TEXT NOT NULL, is_read INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, FOREIGN KEY (conv_id) REFERENCES conversation(id) );
当新消息插入时,我们用一个事务同时写message并递增conversation.unread_count。这种本地事务保证了徽章数据源的一致性。相比每次打开页面都请求服务端,本地SQLite让徽章计算完全在客户端完成,即使在飞机模式下也能正确显示之前同步下来的未读数量。
通过Badging API把SQLite查询结果映射到系统角标
Badging API的核心方法是navigator.setAppBadge(number)和navigator.clearAppBadge()。在支持该接口的浏览器中,我们只需要把SQLite聚合出的未读总数传进去。需要注意,传入0通常等价于清除角标,但部分浏览器实现要求显式调用清除方法,因此代码里要做兼容。
下面这段JavaScript展示了如何在SQLite查询完成后更新徽章。我们用sql.js或者原生sqlite3的WASM版本打开数据库,执行求和语句,再根据结果调用API。如果当前环境不支持Badging API,则静默降级,不影响其他功能。
async function refreshBadge(db) {
const res = db.exec('SELECT SUM(unread_count) AS total FROM conversation');
let total = 0;
if (res.length > 0 && res[0].values.length > 0) {
total = res[0].values[0][0] || 0;
}
if ('setAppBadge' in navigator) {
try {
if (total > 0) {
await navigator.setAppBadge(total);
} else {
await navigator.clearAppBadge();
}
} catch (e) {
console.warn('徽章更新失败', e);
}
}
}
实际项目中,徽章更新不应只发生在页面加载时。我们可以监听SQLite的写操作完成事件,或者利用visibilitychange在页面从隐藏变为显示时重新聚合。这样用户在其他标签页已读消息后,切回本页面角标会自动修正。另外,Badging API调用本身有节流,频繁设置相同值不会触发额外开销,但最好在JS层做一次相等判断以减少Promise创建。
在Web Worker中读取SQLite以避免主线程卡顿
当本地SQLite数据达到几万行时,聚合查询可能占用十几毫秒甚至更多,直接在主线程执行会让动画掉帧。更稳妥的做法是把数据库文件和查询逻辑放进Web Worker,主线程只负责发指令和接收徽章数字。Worker里加载WASM版SQLite,通过postMessage把SUM结果回传。
下面的代码演示了Worker端的简化结构。主线程发送{type: 'badge'},Worker打开已注入的数据库字节流,查询后把数字发回。由于Worker无法访问navigator.setAppBadge(部分浏览器限制),所以设置动作仍由主线程做,Worker纯粹承担计算。
// worker.js
importScripts('sqlite3.wasm.js');
onmessage = async function (ev) {
if (ev.data.type === 'badge') {
const db = new SQL.Database(ev.data.bytes);
const r = db.exec('SELECT SUM(unread_count) FROM conversation');
const total = r.length ? (r[0].values[0][0] || 0) : 0;
db.close();
postMessage({ type: 'badge_result', total: total });
}
};
这种架构下,即便用户在徽章计算期间快速滚动列表,也不会感到输入延迟。我们还可以在Worker里缓存上一次的total,仅当SQLite的conversation表发生UPDATE时才重算,用最少资源保持角标准确。对于多标签页场景,可用BroadcastChannel通知其他Worker失效缓存,确保各实例徽章不冲突。
综合来看,把SQLite作为徽章计数的唯一可信源,再结合Badging API与Worker离线计算,是PWA项目中兼顾性能与准确性的实用方案。它让客户端真正拥有独立的状态推导能力,也降低了服务端推送未读数的复杂度。
SQLiteBadging_API徽章计数修改时间:2026-08-16 22:22:29