PWA (Progressive Web App) 是一种结合网页和原生应用优点的应用技术。
| 特性 | 说明 |
|---|---|
| 📥 可安装 | 添加到主屏幕,像原生应用一样启动 |
| ⚡ 资源复用 | Service Worker 缓存部分资源 |
| 🔄 离线 | 基础功能可离线访问 |
| 🔄 自动更新 | 无需应用商店,自动更新 |
| 💾 小体积 | 无需下载大型安装包 |
在支持 WebAPK 且满足安装条件的 Android 浏览器中,安装后可以获得以下体验;实际安装形式由浏览器和系统决定:
- ✅ 出现在应用抽屉中
- ✅ 可在系统设置中管理
- ✅ 独立窗口启动,隐藏常规浏览器界面
- 应用名称、图标、主题色
- 独立显示模式(无浏览器UI)
- 快捷方式(快速添加密钥、扫描二维码)
- 协议处理器声明(
web+otpauth,是否注册取决于浏览器支持)
- 静态资源缓存: 主页面、Manifest 和应用图标
- CDN 资源缓存: jsQR、qrcode-generator
- 智能缓存策略: 主页网络优先、Favicon 缓存优先、CDN 缓存后后台更新
- 自动更新: 检测新版本并自动应用
- 清理旧缓存: 保持存储空间整洁
- 📴 离线数据修改: 支持离线添加、修改、删除和批量导入密钥
- 🔄 离线同步: 支持 Background Sync API,并在页面加载、网络恢复或 Service Worker 控制器变化时尝试同步;不支持后台同步时由打开的页面触发
- 📱 安装入口: “设置 → 偏好设置 → 安装到桌面”,浏览器触发安装事件后启用按钮,不自动显示安装横幅
- 📡 离线状态指示器: 实时显示在线/离线状态,离线时显示醒目的状态标识
- 🔄 状态同步反馈: 离线操作和同步状态实时反馈给用户
- 提供移动端视口、主题色和 Apple Web App 等元信息
- 安装方式、独立窗口和后台能力取决于浏览器及操作系统,不能仅凭 Meta 标签保证完整支持
- ✅ 查看已加载的密钥列表
- ✅ 生成 OTP 验证码(本地计算,无需网络)
- ✅ 添加新密钥(离线时保存,网络恢复后自动同步)
- ✅ 修改现有密钥(离线时保存,网络恢复后自动同步)
- ✅ 删除密钥(离线时保存,网络恢复后自动同步)
- ✅ 批量导入(离线时保存,网络恢复后自动同步)
-
离线时的操作
用户操作 → Service Worker 拦截 ↓ 保存到 IndexedDB 离线队列 ↓ 返回"操作已保存"提示(202 状态码) ↓ 显示"您处于离线状态,操作已保存,网络恢复后将自动同步" -
网络恢复时的同步
网络连接恢复 → Background Sync 或打开的页面触发同步 ↓ Service Worker 按时间顺序同步所有操作 ↓ 成功:删除队列中的操作,刷新页面数据 网络传输失败:保留队列,等待后续同步 登录过期:保留更改并暂停,登录成功后继续,不消耗重试次数 已无需执行(要删除的账户已不存在,或已有服务名、账户、密钥和验证码参数都相同的账户):按成功处理并移出队列 服务器拒绝(除 408、429 外的 4xx):立即标记为失败并显示原因 5xx、408、429 等失败:累计 5 次后标记为失败 ↓ 显示同步结果:"已同步 X 个离线操作"
| 组件 | 说明 |
|---|---|
| IndexedDB | 存储待同步的离线操作 |
| Background Sync API | 浏览器支持时调度后台同步 |
| 页面同步回退 | 页面加载、网络恢复及控制器变化时尝试同步 |
| Service Worker | 拦截 API 请求,管理离线队列 |
| 重试机制 | 登录失效暂停等待登录;服务器拒绝的请求(除 408、429 外的 4xx)立即停止并显示原因;5xx、408、429 累计失败 5 次后停止自动重试;网络传输失败不消耗额度 |
| 同步顺序 | 按操作时间戳依次发送,尚无跨设备冲突合并 |
有未完成操作时,主页显示“未同步更改”。登录过期会提示登录后继续,成功登录后自动恢复队列。其他失败项可展开查看失败原因,并单独重试或取消;被服务器拒绝的新增或编辑可点击“编辑后重试”,把内容回填到账户对话框修改后重新保存,保存成功后自动移除旧的失败项。
“编辑后重试”会显示排队内容中的密钥,因此只在当前页面已确认登录时可用:本页从服务器读到了账户列表、刚刚登录成功,或显示的是登录状态下保存的本地列表(退出登录和登录过期都会删除它)。否则页面在联网时先要求登录;离线时只提示需要联网并登录后才能编辑,不会打开无法离线完成的登录框。编辑的账户若已在别处删除,对话框会按新账户打开并说明原因,保存时新增账户,不会再编辑不存在的账户。保存修改后的内容前,页面会先占用原失败项;此时其他标签页不能重试、取消或替换它,会看到“正在其他页面中修改”的提示;若原失败项已在其他标签页同步或处理,本页不保存并说明原因,不会产生重复的更改。遇到这两种情况时,如果保存途中已关闭对话框,页面会带着修改后的内容重新打开它;此时若已打开其他账户的对话框,则只提示本次修改未保存。
取消只移除尚未同步的本地操作,不会删除服务器账户。联网后如果会话已过期,页面会清除显示的验证码并要求重新登录,离线队列保留,登录后自动继续同步。没有未同步操作时,此入口自动隐藏。
- 离线时无法获取最新的服务器数据;页面可使用已加载或本地缓存的列表
- 离线修改需由已激活并控制页面的 Service Worker 接收;首次登录、云端备份及恢复仍需要网络
- 如果多设备同时离线修改同一密钥,后同步的操作会覆盖先同步的
- 排队的新增若遇到服务名、账户和密钥都相同的已有账户:类型、位数和算法也相同(TOTP 还要周期相同,HOTP 不比较计数器和周期)时按已完成处理;参数不同时标记为失败,说明服务器上已有同名同密钥但参数不同的账户,可“编辑后重试”或取消。旧版服务器不比较参数,这种情况一律按失败处理并显示服务器原因
- 建议单设备使用,或避免在离线时修改相同的密钥
✅ 数据安全:
- 离线操作保存到浏览器 IndexedDB,正常关闭页面后仍可保留;清除站点数据或浏览器回收存储会删除这些记录
- 同步需要网络和有效登录会话;请确认同步成功后再清理站点数据
- 不支持 Background Sync 时,需要保持页面打开或联网后重新打开页面,才能触发同步
离线功能依赖 Service Worker 和 IndexedDB。应用会检测 Background Sync 是否可用;注册失败或不支持时,页面会发送 SYNC_OPERATIONS 消息请求同步。打开的页面在网络恢复时也会尝试同步,并非必须手动刷新。浏览器关闭后的同步时机由浏览器决定。
-
访问应用
https://your-worker.your-subdomain.workers.dev/ -
安装方式:
将示例地址替换为自己的 Workers 地址或已配置的自定义域名。
方式 1: 应用设置
- 打开“设置 → 偏好设置”
- 点击“安装到桌面”,按浏览器提示完成安装
- 按钮置灰表示浏览器尚未触发安装事件,可尝试浏览器菜单安装
- 在已安装的 PWA 窗口中,此设置项会隐藏
方式 2: 浏览器原生提示
- 浏览器地址栏可能会显示安装图标
- 点击「添加」或「安装」
方式 3: 手动安装
- 点击浏览器菜单(右上角 ⋮)
- 选择「添加到主屏幕」或「安装应用」
- 点击「安装」
-
验证安装
- 检查主屏幕或应用列表中的图标
- 从图标启动,检查是否以独立窗口运行
特别说明: 浏览器可能安装为 WebAPK 或创建主屏幕快捷方式,具体取决于平台和安装条件。
-
访问应用
https://your-worker.your-subdomain.workers.dev/ -
安装步骤:
- 使用自己的部署地址,点击浏览器的 分享按钮 (↑)
- 向下滚动,找到「添加到主屏幕」
- 点击「添加」
- 自定义应用名称(可选)
-
启动应用
- 从主屏幕点击图标启动
- 应用按系统支持以独立窗口运行
注意: iOS 的 PWA 支持有限:
- 离线功能依赖已缓存的页面、数据和系统允许的站点存储
- 不支持 Background Sync 时,由打开的页面在网络恢复后触发同步
-
访问应用
https://your-worker.your-subdomain.workers.dev/ -
安装方式:
使用自己的部署地址;也可通过“设置 → 偏好设置 → 安装到桌面”发起安装(按钮可用时)。
方式 1: 地址栏安装图标
- 满足浏览器安装条件时,地址栏可能出现 安装图标 (⊕ 或 💻)
- 点击图标
- 点击「安装」
方式 2: 菜单安装
- 点击浏览器菜单(右上角 ⋮)
- 选择「安装 2FA ...」
- 点击「安装」
-
验证安装
- 应用图标出现在桌面
- 应用出现在开始菜单(Windows)或应用程序文件夹(macOS)
- 可以独立窗口运行,无浏览器UI
当前 fetch 处理按以下规则执行:
| 资源 | 策略 | 网络异常时的行为 |
|---|---|---|
主页面 / |
Network First;完整成功响应更新缓存;有缓存最多等待 1.5 秒,无缓存最多等待 8 秒 | 返回匹配的页面缓存;无缓存时返回 503 离线提示页;明确的 401/403 不回退缓存 |
Favicon 代理 /api/favicon/ |
Cache First;成功响应写入缓存 | 未命中缓存且请求失败时返回空 404 |
| 白名单 CDN 库(jsQR、qrcode-generator) | Stale While Revalidate;命中缓存立即返回并在后台更新 | 有缓存时继续使用;无缓存且网络失败时加载失败 |
其他 /api/ 请求 |
网络请求,不缓存响应 | 受支持的密钥写操作可进入离线队列,成功入队返回 202;GET 和不受支持的写操作返回 503 |
| 其他外部资源 | Network Only;直接透传,不缓存 | 返回空 404 |
| 其他同源资源 | Network Only;不读取 Service Worker 缓存 | 返回 503 文本响应 |
安装阶段会尝试预缓存主页、Manifest 和应用图标;Manifest 和图标的后续请求目前仍走“其他同源资源”分支,没有 Service Worker 缓存回退。API 的离线队列只在网络请求抛出错误时启用,服务器返回的 HTTP 错误会直接传回页面。
页面会注册 Service Worker,并在打开期间每小时检查更新:
- 浏览器发现新版后下载并安装;
skipWaiting()和clients.claim()让新版尽快接管页面 - 已打开页面中的 HTML 和脚本不会因此自动重新加载,需要访问或刷新页面才能加载新的界面代码
- Service Worker 更新仅记录控制台日志,不弹出更新通知
离线状态指示器:实时显示网络状态:
- 📡 当网络断开时,页面顶部会显示"离线模式"横幅
- 🌐 网络恢复时,指示器自动消失,并显示"正在同步..."提示
- 离线期间的操作会自动保存到本地队列
- 网络恢复后自动同步所有离线操作
// Service Worker 生命周期
1. 安装 (Install)
↓
预缓存主页、Manifest 和应用图标
↓
skipWaiting() - 立即激活
2. 激活 (Activate)
↓
清理旧版本缓存
↓
clients.claim() - 立即控制页面
3. 拦截请求 (Fetch)
↓
根据资源类型应用不同缓存策略
↓
返回缓存或网络响应
4. 更新检查 (每小时)
↓
检测新版本 Service Worker
↓
安装新控制器并记录日志,不自动刷新当前页面Service Worker 版本由部署脚本自动注入。缓存名采用 2fa-cache-${SW_VERSION},每次发布会生成对应的新缓存并在激活阶段清理旧版本,无需手工维护固定的 v1 / v2 缓存名。
查看 Service Worker 状态:
-
Chrome DevTools:
- F12 → Application → Service Workers
- 查看注册状态和缓存内容
-
chrome://serviceworker-internals/:
- 查看所有已注册的 Service Worker
- 可以手动卸载或更新
原因:
- 未收到浏览器的
beforeinstallprompt事件(应用不会自动显示安装横幅) - Service Worker 未成功注册
- 未满足 PWA 安装条件
- 浏览器不支持 PWA
解决方案:
-
检查 HTTPS:
确保使用 HTTPS 访问(Cloudflare Workers 自动提供) -
检查 Service Worker:
- Chrome:
chrome://serviceworker-internals/ - 查找自己的部署域名,或在 DevTools → Application → Service Workers 查看当前站点
- 确认已注册并激活;Service Worker 空闲时停止运行是正常行为
- Chrome:
-
清除缓存并重新访问:
Chrome → 设置 → 隐私和安全 → 清除浏览数据 选择「缓存的图片和文件」→ 清除 -
检查浏览器安装入口:
- 等待 Service Worker 完成注册;没有固定的 30 秒安装触发条件
- 若设置中的按钮持续置灰,尝试浏览器菜单中的安装或“添加到主屏幕”选项
检查方法:
- 打开浏览器控制台(F12)
- 查看是否有错误信息
- 检查 Application → Service Workers
常见错误:
错误 1: Failed to register service worker
原因: Service Worker 文件无法访问
解决: 确认 /sw.js 可以正常访问
错误 2: DOMException: Only secure origins are allowed
原因: 未使用 HTTPS
解决: 使用 HTTPS 访问(Cloudflare Workers 自动提供)
检查清单:
- ✅ Service Worker 已注册并激活
- ✅ 至少访问过一次在线版本(预缓存资源)
- ✅ 已加载密钥列表或存在本地密钥缓存;需要的二维码库也已在线加载并缓存
可用的离线功能:
- ✅ 主页面UI
- ✅ 查看已保存的密钥
- ✅ 生成 OTP(基于本地时间)
- ✅ 二维码生成(CDN 库已缓存)
- ✅ 添加、编辑、删除及批量导入的请求排队(Service Worker 已控制页面,成功保存到 IndexedDB 后返回 202)
需要网络的功能:
- ❌ 将排队的密钥修改提交到服务器、获取最新密钥列表
- ❌ 云端备份和恢复(需要 API)
- ❌ 登录认证(需要 API)
手动触发更新:
-
浏览器刷新:
- 按
Ctrl+Shift+R(Windows/Linux) - 按
Cmd+Shift+R(macOS)
- 按
-
卸载并重新安装:
- Android: 设置 → 应用 → 2FA → 卸载
- iOS: 长按图标 → 删除
- 桌面: 右键图标 → 卸载
- 重新访问网站并安装
-
清除 Service Worker:
chrome://serviceworker-internals/ 找到自己的部署域名 点击「Unregister」 刷新页面重新注册
现象:
- 二维码扫描功能不可用
- 二维码生成功能不可用
- 控制台显示 CDN 资源加载错误
解决方案:
-
检查网络连接:
- 确保可以访问外部 CDN
-
清除缓存:
Chrome → F12 → Application → Cache Storage 只删除目标资源缓存,然后在线重新加载对应功能不要把清除缓存与清除全部站点数据混为一谈;后者还会删除尚未同步的 IndexedDB 操作。
-
检查 Service Worker 缓存:
Chrome → F12 → Application → Cache Storage 查看当前 `2fa-cache-*` 缓存 确认 jsQR.min.js 和 qrcode.min.js 已缓存
-
定期访问在线版本:
- 确保获得最新功能和安全更新
- 更新缓存资源
-
及时同步数据:
- 离线数据变更会先进入本地队列,仍需在网络恢复后完成同步
-
保持应用更新:
- 部署新版后在线刷新页面,加载最新界面代码;Service Worker 更新不会弹出通知
加载耗时受设备、网络、缓存状态和密钥数量影响,没有统一的时延保证。主页在线时使用 Network First,后续访问也会请求网络。可使用浏览器 DevTools 分别记录首次访问、已有缓存的在线访问及离线访问,比较实际耗时和资源命中情况。
| API | 项目实现状态 |
|---|---|
| Cache API | 已实现页面及指定资源缓存 |
| Fetch API | 已实现请求分流、离线回退和写入排队 |
| IndexedDB | 已实现持久化离线操作队列 |
| Background Sync | 已实现 sync-operations,含页面触发回退 |
| Push Notifications | 仅有接收和点击处理器,未提供完整订阅与发送流程 |
| Periodic Background Sync | 未实现 |
项目通过能力检测使用 Service Worker、安装事件和 Background Sync。上表描述代码中已实现的功能,并非对所有浏览器的兼容性承诺。请在目标浏览器验证安装入口、离线缓存和恢复联网后的同步;HTTPS(或本地开发的可信来源)是 Service Worker 的前提。
- 周期性后台同步(当前每小时检查 Service Worker 更新依赖页面保持运行)
- 跨设备离线修改的自动冲突检测与合并
- 完整的推送通知订阅与发送流程
离线操作队列、密钥增删改及批量导入请求的排队、Background Sync 和页面同步回退已经实现,不属于未来功能。服务端定时备份与浏览器周期性后台同步是不同机制。
Service Worker 使用部署时版本号自动管理缓存。排障时以 /sw.js 中的 SW_VERSION 和 Chrome DevTools 显示的当前控制器为准。