想象小区门口有两家店。普通网页像一家没有仓库的杂货铺,顾客问“有没有酱油”,店员就得立刻跑去几公里外的批发市场进货;市场一打烊(断网),只能挂出“暂停营业”的牌子。而 PWA(渐进式 Web 应用)是一家“有心眼”的驿站:开张前就囤好常用货(缓存),雇了一位既当保安又当仓管的全能管理员(Service Worker),挂上正规招牌(Manifest),还办了营业许可(HTTPS)。市场关门它照样营业,顾客没网也能取到常用货;实在缺的货,伙计会先登记在“待发簿”(Background Sync),路一通就立刻补送。PWA 的整套思路,就是让网页不再把命运押在“网络是否在线”上,而是把离线变成一种服务能力——这对 AI 应用在弱网环境的落地体验至关重要。
对 AI 从业者而言,掌握 PWA 离线策略与背景同步,能让你的 AI Web 应用在信号不稳、断网频发的真实生产环境中依然可用、可达、可感知,是 AI 产品从 demo 走向交付的硬实力。
目录
可视化演示
展示Service Worker三种缓存策略对比、离线请求队列与背景同步流程、PWA安装与manifest配置,支持模式切换。
本节配有交互动效(进入视口后自动加载,约 3KB)。
缓存策略对比 Cache-First优先缓存命中,未命中时回退网络 Stale-While-Revalidate返回缓存同时后台更新 caches.match(req) || fetch(req) 三种缓存策略适应不同场景的离线体验
PWA 核心原理:给网页开一家“驿站”
想象小区门口有两家店。普通网页像一家没有仓库的杂货铺,顾客问什么,店员就得立刻跑去批发市场进货;市场打烊(断网)时,只能挂出“暂停营业”。而 PWA 是一家“有心眼”的驿站:开张前就囤好常用货(缓存),雇了一位既当保安又当仓管的全能管理员(Service Worker),挂上正规招牌(Manifest),还办了营业许可(HTTPS)。市场关门,它照样能对熟客营业。
PWA 的英文全称是 Progressive Web App,中文叫“渐进式 Web 应用”。它的灵魂是“渐进增强”:任何浏览器都能打开这个网页,但越现代的浏览器,用户能享受的体验越接近原生 App——比如离线打开、秒级启动、图标安装到桌面。这三件事分别由三个组件支撑:HTTPS 保证安全上下文,Service Worker 负责后台调度,Manifest 交代“我是谁、从哪开、长什么样”。
离线优先是 PWA 的默认哲学:首次访问时把 HTML、CSS、JS、图片等静态资源写进缓存,之后的每一次访问,先让缓存兜底,再悄悄用网络更新。网络不再是“生死依赖”,而是“锦上添花”——哪怕信号只剩一格,核心页面依然能打开。
所以 PWA 并不神秘——它只是把“开门做生意”拆解成许可、管理员、招牌三件套,再配上“先翻仓库再出门”的离线优先思想。驿站因此能在市场关闭时照常服务熟客,网页也因此能在断网一刻保住用户体验。
- Manifest(门面招牌)描述应用名称、图标、启动地址的 JSON 文件,让浏览器知道“这是什么应用”。
- Service Worker(管理员)独立于页面的后台脚本,拦截网络请求并调度缓存。
- HTTPS(营业许可)安全上下文是注册 Service Worker 与使用推送功能的前提。
服务工作线程:驿站的万能管理员
驿站管理员不是招来就能上岗的。他得先接受一遍培训(installing),领到工牌(installed),再到岗位上待命(activating),最后从老板手里接过仓库钥匙(activated);一旦被新人取代或培训失败,工牌作废(redundant)。这套“入职流程”就是 Service Worker 的生命周期(lifecycle)。每一步都有对应的浏览器事件通知开发者。
Service Worker 是浏览器在后台独立运行的 JavaScript 线程,不占页面主线程、不阻塞 UI。它像管理员一样,站在页面和网络之间,拦截页面的每一个请求。但正因为权力大——能决定请求走缓存还是走网络——浏览器要求它必须先安全走完生命周期,接管请求的资格才会生效。
开发者通常在 install 事件里做“预缓存”:把关键资源一次性装进仓库,为离线做准备;在 activate 事件里做“旧货清理”:删掉不再用的旧版本缓存,避免磁盘堆积;而 fetch、push、sync 事件,则是上岗后“值班接单”的三种业务。一个未被激活的 SW 不会收到任何请求事件,这就是注册后离线“不生效”的常见原因。
理解了入职流程,你就明白“为什么注册了 SW 离线还不生效”——因为管理员还没完成最终交接。给生命周期一点时间,等钥匙到手,离线能力才真正落地。
- installing(培训中)脚本下载并首次执行,此时可以预缓存静态资源。
- activated(接管钥匙)activate 事件触发,清理旧版本缓存并正式获得控制权。
- 任务循环(值班接单)activated 之后开始监听 fetch、push、sync 三种事件。
缓存策略:仓库备货的四种智慧
仓库怎么备货,暴露了驿站的经营风格。急性子老板喜欢“先翻自家仓库”(缓存优先),有货直接递;注重口碑的老板“先问最新价”(网络优先),问不到再翻仓库;精明的老板“旧货先给,新货偷偷补”(陈旧更新);而卖生鲜的档口“只卖刚到的货”(仅网络)。顾客要的东西不同,老板的备货逻辑也不同。
在 PWA 世界里,这些经营风格叫缓存策略(caching strategies),是 Service Worker 在 fetch 事件中回答“响应从哪来”的决策规则。四种策略各有权衡:缓存优先延迟最低、离线能力最强,但可能给用户过期内容;网络优先永远最新鲜,但更依赖网络;陈旧更新先返回旧缓存,再在后台请求新版本并更新缓存,兼顾速度与新鲜度;仅网络则把请求直接交给服务器,不做缓存兜底,适合支付、登录等必须实时的操作。
策略怎么选,看资源性质:图片、字体、版本号固定的 JS/CSS 适合缓存优先或陈旧更新;用户资料、商品价格适合网络优先或陈旧更新;登录、支付等写入型请求必须仅网络,失败的提交再交给背景同步兜底。实际项目往往是四种策略的组合,没有万能的单一一款。
就像驿站老板会为“米面粮油”和“当日生鲜”设置不同的备货节奏,PWA 开发者也要为静态资源和实时数据分别制定策略。策略选对,快与新鲜兼得;选错,用户要么看到旧价签,要么离线对着空白页叹气。
- 缓存优先命中即返回缓存响应,未命中则回源并写入缓存。
- 网络优先先请求网络,失败时降级读取缓存。
- 陈旧更新立刻返回旧缓存,同时在后台刷新缓存。
缓存 API:仓库的货架与台账
有了经营策略,仓库还得有货架和台账。Cache API 就是驿站仓库的标准化货架系统:打开仓库(caches.open)、查货(cache.match)、入库(cache.put)、清货(cache.delete)。每件货都带着一张“请求–响应”对账单,原样存取,不打折扣。
Cache API 是浏览器提供的一组 Promise 异步接口,以 Request 和 Response 为存取单元。caches.open('static-v1') 会打开一个命名缓存,不存在则自动创建;cache.match(request) 返回第一个匹配的响应;cache.put(request, response) 把一对请求响应写进去;cache.delete(request) 移除指定项。注意仓库有存储配额,塞太满会触发淘汰,所以不能无上限囤货。
实际开发中最常用的组合是 cache.addAll([...])——一次性请求并缓存一整个资源清单,常用于 install 事件里“装配离线弹药库”。配合版本号管理(static-v1 升级到 static-v2),在 activate 时删除旧版,就能实现用户无感的平滑升级。
货架系统本身不复杂,但配上演进版本的“旧货下架、新货上架”流程,驿站就能在升级装修时不让熟客吃闭门羹。这正是 Cache API 与生命周期配合的最佳实践。
- 打开仓库打开或创建命名缓存,返回一个 Cache 对象。
- 入库与查货把响应写入缓存,再用请求去匹配响应。
- 版本演进升级版本号并删除旧仓库,完成平滑迁移。
背景同步:断网也能接单的“待发簿”
断网时,顾客硬要寄一个急件。普通杂货铺店员只能摇头,但驿站伙计会麻利地把单子收进“待发簿”,登记好收件人和货物;等路一通,立刻骑着三轮车统一派送。顾客要的从来不是“立刻送达”,而是“不会丢件”。
Background Sync API 正是浏览器提供的“待发簿”。开发者在页面里通过 navigator.serviceWorker.ready.then(reg => reg.sync.register('tag')) 注册一个带标签的同步任务;当浏览器检测到网络恢复,就给 Service Worker 派发 sync 事件。开发者在这个事件里把离线期间积压的请求重新发出——表单、评论、埋点日志这些“欠着的账”终于可以结了。
有一个必须接受的现实:sync 事件的触发时机由浏览器启发式调度,取决于电量、网络质量、用户活跃度等因素,不是“断网→恢复→立刻”的精确时序。因此它应当被当成“保证最终送达”的机制,而不是定时器。请求量大时,建议维护队列,成功一条出队一条,失败则留在队里等待下一次同步——但重试有次数上限,超限后请求会被丢弃。
有了这本“待发簿”,驿站在市场关闭时依然敢接单,顾客的信任也就留住了。对 AI 应用而言,离线问答记录、模型使用日志、埋点上报,都靠它实现“迟到的送达”,一份数据都不丢。
- 登记待发在页面里注册带标签的同步任务。
- 自动触发网络恢复时,浏览器向 SW 派发 sync 事件。
- 逐条补发成功一条出队一条,失败等待下次同步。
应用清单:驿站的装修图纸
想让驿站被顾客当成“正经门店”而非流动摊贩,就得装修门面、挂招牌、印名片。Manifest 就是那份“装修图纸”:告诉操作系统——这个应用叫什么名字、用什么图标、从哪个页面启动、以什么形态展示。
manifest.json 是一个 JSON 文件,由 <link rel="manifest" href="/manifest.json"> 引入。关键字段包括:name 与 short_name(全名和桌面短名)、icons(至少提供 192px 与 512px 两种尺寸)、start_url(冷启动页面)、display(fullscreen、standalone、minimal-ui、browser 四种形态)、theme_color(顶栏主题色)。当站点同时满足 HTTPS、注册了 SW、提供合适图标和可访问的 start_url 时,Chrome 就会触发“添加到主屏幕”。
安装能力(Installability)是 PWA“像原生 App”的入口。display: standalone 让窗口隐藏浏览器地址栏,全屏体验更接近原生;theme_color 让状态栏与页面 UI 融为一体。这些细节看似不起眼,却正是“网址”与“应用”之间体验差距的来源。
装修图纸本身不产生离线能力,却是用户把驿站“装进口袋”的钥匙——没有它,再可靠的站点也只是一个“网址”;有了它,网页才有了名字、图标和全屏入口,像原生应用一样住在用户的主屏上。
- 门店名片名称与双尺寸图标,决定桌面图标的身份感。
- 启动地址指定点击图标后冷启动的页面路径。
- 展示形态在产品与浏览器之间的选择,决定沉浸感。
推送通知:驿站的主动派单
驿站不满足于“等人上门”,它还会主动给顾客发“您的包裹已到”的手机提醒。这需要两套设备:一本“订阅登记簿”(Push 订阅)记录该往哪儿发,一个“到货广播”(Notification)让前台把消息亮出来。两者配合,服务就从被动等待变成了主动触达。
Push 与 Notification 是 PWA 主动触达用户的两段式组合:首先,在用户授权后,浏览器向推送服务生成一个订阅对象,页面把其中的 endpoint 上报给你的服务器;随后,服务器向 endpoint 发送消息,浏览器唤醒沉睡的 Service Worker,触发 push 事件;最后,SW 调用 self.registration.showNotification(title, options) 展示通知,并监听 notificationclick 事件处理点击跳转——通常用 clients.openWindow(url) 打开落地页面。
权限体验是推送的关键:浏览器会抑制可疑的自动弹窗,所以授权请求必须放在用户点击“开启提醒”等明确交互之后,而不是一进页面就弹。另外,背景同步与推送天然互补:离线提交成功后再推送一条“同步完成”,整套“断网可用、联网必达、主动通知”的体验闭环就此打通。
推送让驿站的服务从被动等待变成主动触达。至此,仓库(缓存)、管理员(SW)、待发簿(背景同步)、装修(Manifest)和广播(推送)全部就位——一个接近原生体验的渐进式 Web 应用,正式交付。
- 订阅登记获取推送订阅对象,并把 endpoint 上报服务器。
- 服务器推送服务器向 endpoint 发送消息体,浏览器唤醒 SW。
- 前台广播SW 展示系统通知,并处理点击跳转。
AI 应用场景
AI 离线知识助手的弱网守护
阶段:应用部署与体验优化
解决的问题:传统 Web 端 AI 助手强依赖网络,弱网环境下知识检索卡顿、问答直接中断。通过 PWA 的缓存优先策略,把常用知识卡片、模型配置等静态资源预缓存,离线时用户仍能查阅历史问答、浏览常用提示词;离线产生的反馈和日志则交给背景同步,在网络恢复后自动补发。
具体案例:某制造业内部部署的 AI 知识助手采用 PWA 架构,车间现场网络极不稳定。借助 Service Worker 预缓存设备手册与故障排查流,工人在信号盲区也能离线查阅资料;提问记录通过 Background Sync 在联网后批量回传,用于后续模型微调的数据积累。
端侧 AI 推理的延迟数据回传
阶段:数据采集与模型迭代
解决的问题:边缘端 AI 设备(如巡检机器人)在信号盲区完成识别任务,结果需要回到云端才有效。传统方案用定时器疯狂重试,既耗电又容易丢数据;改用 PWA 的背景同步后,识别结果先“寄存”在本地队列,由浏览器智能调度、在网络恢复时统一批量上传,实现零人工干预的可靠回传。
具体案例:某园区巡检机器人内置 Web 端推理界面,在隧道与地下车库等盲区完成表计读数识别后,通过 Background Sync 自动上报识别结果与图像缩略图;同步完成后由推送通知告知运维人员“今日数据已全部入库”,形成完整的离线闭环。
Python 演示
"""PWA 缓存策略与背景同步模拟器 用 Python 模拟 Service Worker 面对请求的选择逻辑包含四种缓存策略与 Background Sync 的待发队列"""import random class CacheBox: """对应 Cache API 的简易货架""" def __init__(self): self.items = {} self.hit_count = 0 self.miss_count = 0 def match(self, key): if key in self.items: self.hit_count += 1 return self.items[key] self.miss_count += 1 return None def put(self, key, value): self.items[key] = value class Strategy: """四种缓存策略的决策器""" @staticmethod def cache_first(cache, key, online): cached = cache.match(key) if cached is not None or not online: return cached data = {"origin": "network", "key": key} cache.put(key, data) return data @staticmethod def network_first(cache, key, online): if online: data = {"origin": "network", "key": key} cache.put(key, data) return data return cache.match(key) @staticmethod def stale_while_revalidate(cache, key, online): cached = cache.match(key) fresh = {"origin": "network", "key": key} if online: cache.put(key, fresh) return cached if cached is not None else fresh @staticmethod def network_only(cache, key, online): if online: return {"origin": "network", "key": key} return None class SyncQueue: """模拟 Background Sync 的待发队列""" def __init__(self): self.pending = [] def register(self, task): self.pending.append(task) def flush(self, online): remained = [] for task in self.pending: if online: print(" 补发:", task) else: remained.append(task) self.pending = remained return len(remained) random.seed(42)cache = CacheBox()strategy = Strategy()stats = { "cache_first": 0, "network_first": 0, "stale": 0, "network_only": 0,}for step in range(100): online = step % 5 != 0 # 每 5 步中有 1 步离线 key = "/api/item" if strategy.cache_first(cache, key, online): stats["cache_first"] += 1 if strategy.network_first(cache, key, online): stats["network_first"] += 1 if strategy.stale_while_revalidate(cache, key, online): stats["stale"] += 1 if strategy.network_only(cache, key, online): stats["network_only"] += 1print("100 次模拟请求成功率:", stats) queue = SyncQueue()for i in range(5): queue.register({"url": "/api/log", "id": i})print("离线时待发队列:", queue.flush(False), "条")print("网络恢复后补发队列:", queue.flush(True), "条")
class CacheBox:对应 Cache API 的命名货架,管理请求响应对def match(self, key):查缓存,命中与未命中分别计数def put(self, key, value):把响应写回仓库,供后续离线使用def cache_first(cache, key, online):先翻仓库,没货再回源def network_first(cache, key, online):先问网络,离线降级读缓存online = step % 5 != 0用步数制造 20% 的离线时段def register(self, task):断网时把请求登记进待发簿def flush(self, online):网络恢复后统一补发队列中的请求
常见误区
误区我在页面里调用了 `serviceWorker.register()`,断网后刷新应该就能用了,但页面还是一片空白。
正解注册只让管理员“入职”,仓库(缓存)还是空的。真正的离线能力来自你手动把资源 caches.put 或 cache.addAll 进缓存。好比驿站开张了,但货架空空如也,顾客来了照样两手空空。要在 install 或 fetch 事件里完成缓存的写入与读取,离线才会生效。
为何常见:注册代码是官方文档的第一步,容易被误当成“一键离线开关”,而忽略了后续的缓存填充动作。
误区既然要追求离线体验,那所有请求都走缓存优先不就行了?又快又省流量。
正解缓存优先像“只卖存货”,用户可能看到过期的价格、陈旧的资讯,对实时性要求高的业务(余额、库存、汇率)是灾难。正确做法是按资源性质组合策略:静态资源用缓存优先或陈旧更新,实时数据用网络优先,写入请求仅网络加背景同步兜底。
为何常见:缓存优先代码最简单,离线演示效果肉眼可见,容易被误当成“一招鲜”。
误区我调用了 `sync.register`,按我的理解,网络一恢复它就该马上执行,可我测试时发现它半天不动。
正解同步事件的时机由浏览器启发式调度,涉及电量、网络质量、用户活跃度等因素,不是“断网→恢复→立刻”的精确响应。工程师应把它当作“保证最终送达”的机制,而非“准时触发”的定时器。尤其要避免在 sync 事件里做耗时过长的任务。
为何常见:教程示例省略了时序细节,让人误以为同步是即时行为。
误区PWA 能离线、能推送、能全屏,添加到主屏幕之后应该和原生 App 一样,什么系统能力都能调用。
正解PWA 能用的系统能力远超普通网页,但仍受 Web 平台 API 的能力边界限制——后台精确定位、原生级多线程、部分蓝牙传感器等仍不可用或受限。PWA 的目标是“接近原生体验”,不是“完全等于原生系统能力”。
为何常见:厂商宣传喜欢强调“类原生”体验,容易让人把体验上的接近误当成能力上的等同。
误区想提高订阅率,所以页面一加载就立刻弹授权框,结果用户拒绝率特别高。
正解浏览器会抑制可疑的自动弹窗;在用户没有任何交互时请求权限,往往被拒甚至被浏览器拦截。最佳实践是在用户点击“开启提醒”等明确操作后请求授权——就像服务员等顾客坐定再递菜单,而不是进门就塞广告。
为何常见:把“尽早触达”误解为“越早弹窗越好”,忽略了浏览器的权限卫生机制与用户心理。
面试题
Q1. PWA 的三大核心组件通常指什么?
- A.HTML、CSS 与 JavaScript 三件套
- B.清单、服务线程与 HTTPS 三件套← 正确答案
- C.Vue、React 与 Angular 三大框架
- D.HTTP2、HTTP3 与 WebSocket 协议
答案:B。PWA 的三根支柱是 Manifest(应用清单)、Service Worker(服务工作线程)和 HTTPS(安全上下文)。A 是普通网页的基础技术,C 是前端框架,D 是网络传输协议,它们都不构成 PWA 特有的离线能力支撑。
Q2. 下列哪个是 Service Worker 生命周期状态的正确顺序?
- A.安装中、已安装、激活中、已激活、废弃← 正确答案
- B.安装中、激活中、已激活、已安装、废弃
- C.激活中、已激活、废弃、安装中、已安装
- D.待命、安装、激活、终结、启用
答案:A。生命周期的固定顺序是 installing(安装中)→ installed(已安装)→ activating(激活中)→ activated(已激活)→ redundant(废弃)。就像驿站管理员先培训、领工牌、到岗、接钥匙,最后被取代。选项 B 把“已安装”和“已激活”顺序颠倒,C、D 完全打乱,均不正确。
Q3. 缓存优先(cache-first)策略最适合下列哪类资源?
- A.用户账户余额的实时查询
- B.静态图片与脚本资源加载← 正确答案
- C.在线聊天消息的实时收发
- D.支付表单的即时提交
答案:B。缓存优先以过期风险换速度和离线能力,适合版本号固定、几乎不变的静态资源。余额查询、聊天消息、支付提交都要求数据新鲜或必须实时到达,一旦命中旧缓存会带来业务风险,应改用网络优先或仅网络。
Q4. 关于陈旧更新(stale-while-revalidate)策略,下列描述正确的是?
- A.只读缓存,永不更新数据
- B.先请求网络再读取缓存
- C.先回旧缓存并后台刷新缓存← 正确答案
- D.仅走网络,失败即报错
答案:C。陈旧更新先立刻返回旧缓存让用户秒开,再在后台发起网络请求,拿到新响应后更新缓存。A 是无更新的纯缓存,B 是网络优先,D 是仅网络,只有 C 准确描述了“先旧后新、后台补新”的同步行为。
Q5. 注册 Background Sync 任务的正确入口是哪个?
- A.navigator.serviceWorker.ready 后再调用 sync.register← 正确答案
- B.caches.open 打开离线缓存仓库
- C.pushManager.subscribe 订阅推送服务
- D.在 window 上直接调用 sync 方法
答案:A。背景同步依赖注册好的 Service Worker,因此要先等 navigator.serviceWorker.ready 拿到 registration,再调用 reg.sync.register('tag')。B 是缓存 API,C 是推送订阅,D 中 sync 并不挂在 window 上,三者都与任务注册无关。
Q6. 下列哪些场景适合使用 Background Sync?
- A.离线提交的表单数据补发← 正确答案
- B.埋点日志的批量上报← 正确答案
- C.实时视频流的在线传输
- D.弱网环境下的评论发布← 正确答案
答案:ABD。背景同步的价值是“网络恢复后自动补发”,适合表单、日志、评论这类可以延迟的请求。实时视频流必须连续在线传输,无法通过延迟补发解决,因此 C 不适合。
Q7. Cache API 中用于“打开或创建命名缓存”的方法是?
- A.caches.create 创建新缓存
- B.caches.open 打开命名缓存← 正确答案
- C.cache.get 读取指定缓存
- D.caches.new 新建缓存仓库
答案:B。规范提供的是 caches.open(name),不存在则自动创建,返回 Promise<Cache>。A、C、D 都是臆造的 API 名称,实际浏览器并未提供 create、get、new 这样的全局缓存方法。
Q8. 下列哪一项是触发 PWA“可安装”能力的必要条件组合?
- A.HTTP 站点加上 manifest 清单
- B.HTTPS、SW、图标与启动地址← 正确答案
- C.HTTP 加强密码与双因素认证
- D.HTTP 加应用商店审核备案
答案:B。Chrome 的可安装条件要求:HTTPS 安全上下文、已注册 Service Worker、提供 192px 与 512px 图标、start_url 可访问。只加 manifest 不够,HTTP 站点无法注册 SW,双因素认证和商店审核与 PWA 安装无关。
Q9. 关于 Push 与 Notification 的配合流程,下列说法正确的是?
- A.推送后浏览器触发 SW push 事件← 正确答案
- B.SW 用 showNotification 展示通知← 正确答案
- C.点击通知后需 openWindow 落地← 正确答案
- D.用户无需授权即可收到推送
答案:ABC。完整链路是:服务器向 endpoint 发消息 → 浏览器唤醒 SW 触发 push 事件 → SW 调 showNotification 展示 → 用户点击触发 notificationclick → openWindow 打开落地页。Web 推送必须经过用户显式授权,D 说反了。
Q10. 网络优先策略在网络请求失败后,正确的降级行为是?
- A.请求直接报错并丢弃
- B.回退读取缓存的旧响应← 正确答案
- C.清空全部缓存再重试
- D.等待用户手动点击刷新
答案:B。网络优先的核心是“先网络、后缓存”:在线时用最新数据并写缓存,离线或请求失败时回退读取缓存的旧响应,牺牲一点新鲜度换取可用性。直接报错、清缓存或等用户刷新都没有利用已建立的缓存资产。