想象小区门口有两家店。普通网页像一家没有仓库的杂货铺,顾客问“有没有酱油”,店员就得立刻跑去几公里外的批发市场进货;市场一打烊(断网),只能挂出“暂停营业”的牌子。而 PWA(渐进式 Web 应用)是一家“有心眼”的驿站:开张前就囤好常用货(缓存),雇了一位既当保安又当仓管的全能管理员(Service Worker),挂上正规招牌(Manifest),还办了营业许可(HTTPS)。市场关门它照样营业,顾客没网也能取到常用货;实在缺的货,伙计会先登记在“待发簿”(Background Sync),路一通就立刻补送。PWA 的整套思路,就是让网页不再把命运押在“网络是否在线”上,而是把离线变成一种服务能力——这对 AI 应用在弱网环境的落地体验至关重要。

岗位关联

对 AI 从业者而言,掌握 PWA 离线策略与背景同步,能让你的 AI Web 应用在信号不稳、断网频发的真实生产环境中依然可用、可达、可感知,是 AI 产品从 demo 走向交付的硬实力。

目录
  1. 可视化演示
  2. PWA 核心原理:给网页开一家“驿站”
  3. 服务工作线程:驿站的万能管理员
  4. 缓存策略:仓库备货的四种智慧
  5. 缓存 API:仓库的货架与台账
  6. 背景同步:断网也能接单的“待发簿”
  7. 应用清单:驿站的装修图纸
  8. 推送通知:驿站的主动派单
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示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 并不神秘——它只是把“开门做生意”拆解成许可、管理员、招牌三件套,再配上“先翻仓库再出门”的离线优先思想。驿站因此能在市场关闭时照常服务熟客,网页也因此能在断网一刻保住用户体验。

PWA 体验=Manifest⏟门面招牌+Service Worker⏟管理员+HTTPS⏟营业许可\text{PWA 体验} = \underbrace{\text{Manifest}}_{\text{门面招牌}} + \underbrace{\text{Service Worker}}_{\text{管理员}} + \underbrace{\text{HTTPS}}_{\text{营业许可}}
三个组件合起来,网页才能在断网时依然开门营业。
  1. Manifest(门面招牌)描述应用名称、图标、启动地址的 JSON 文件,让浏览器知道“这是什么应用”。
    manifest: {name, icons, start_url, display}\text{manifest}:\ \{ \text{name},\ \text{icons},\ \text{start\_url},\ \text{display} \}
  2. Service Worker(管理员)独立于页面的后台脚本,拦截网络请求并调度缓存。
    SW: fetch→cache⇌network\text{SW}:\ \text{fetch} \to \text{cache} \rightleftharpoons \text{network}
  3. HTTPS(营业许可)安全上下文是注册 Service Worker 与使用推送功能的前提。
    secure context⇒SW / Push 可用\text{secure context} \Rightarrow \text{SW / Push 可用}

服务工作线程:驿站的万能管理员

驿站管理员不是招来就能上岗的。他得先接受一遍培训(installing),领到工牌(installed),再到岗位上待命(activating),最后从老板手里接过仓库钥匙(activated);一旦被新人取代或培训失败,工牌作废(redundant)。这套“入职流程”就是 Service Worker 的生命周期(lifecycle)。每一步都有对应的浏览器事件通知开发者。

Service Worker 是浏览器在后台独立运行的 JavaScript 线程,不占页面主线程、不阻塞 UI。它像管理员一样,站在页面和网络之间,拦截页面的每一个请求。但正因为权力大——能决定请求走缓存还是走网络——浏览器要求它必须先安全走完生命周期,接管请求的资格才会生效。

开发者通常在 install 事件里做“预缓存”:把关键资源一次性装进仓库,为离线做准备;在 activate 事件里做“旧货清理”:删掉不再用的旧版本缓存,避免磁盘堆积;而 fetch、push、sync 事件,则是上岗后“值班接单”的三种业务。一个未被激活的 SW 不会收到任何请求事件,这就是注册后离线“不生效”的常见原因。

理解了入职流程,你就明白“为什么注册了 SW 离线还不生效”——因为管理员还没完成最终交接。给生命周期一点时间,等钥匙到手,离线能力才真正落地。

installing→installed→activating→activated→redundant\text{installing} \to \text{installed} \to \text{activating} \to \text{activated} \to \text{redundant}
生命周期的顺序不能乱:先装好货,再上岗。
  1. installing(培训中)脚本下载并首次执行,此时可以预缓存静态资源。
    install: cache.addAll(预缓存清单)\text{install}:\ \text{cache.addAll}(\text{预缓存清单})
  2. activated(接管钥匙)activate 事件触发,清理旧版本缓存并正式获得控制权。
    activate: 删除旧版本缓存⇒获得控制权\text{activate}:\ \text{删除旧版本缓存} \Rightarrow \text{获得控制权}
  3. 任务循环(值班接单)activated 之后开始监听 fetch、push、sync 三种事件。
    idle⇌{ fetch, push, sync }\text{idle} \rightleftharpoons \{\ \text{fetch},\ \text{push},\ \text{sync}\ \}

缓存策略:仓库备货的四种智慧

仓库怎么备货,暴露了驿站的经营风格。急性子老板喜欢“先翻自家仓库”(缓存优先),有货直接递;注重口碑的老板“先问最新价”(网络优先),问不到再翻仓库;精明的老板“旧货先给,新货偷偷补”(陈旧更新);而卖生鲜的档口“只卖刚到的货”(仅网络)。顾客要的东西不同,老板的备货逻辑也不同。

在 PWA 世界里,这些经营风格叫缓存策略(caching strategies),是 Service Worker 在 fetch 事件中回答“响应从哪来”的决策规则。四种策略各有权衡:缓存优先延迟最低、离线能力最强,但可能给用户过期内容;网络优先永远最新鲜,但更依赖网络;陈旧更新先返回旧缓存,再在后台请求新版本并更新缓存,兼顾速度与新鲜度;仅网络则把请求直接交给服务器,不做缓存兜底,适合支付、登录等必须实时的操作。

策略怎么选,看资源性质:图片、字体、版本号固定的 JS/CSS 适合缓存优先或陈旧更新;用户资料、商品价格适合网络优先或陈旧更新;登录、支付等写入型请求必须仅网络,失败的提交再交给背景同步兜底。实际项目往往是四种策略的组合,没有万能的单一一款。

就像驿站老板会为“米面粮油”和“当日生鲜”设置不同的备货节奏,PWA 开发者也要为静态资源和实时数据分别制定策略。策略选对,快与新鲜兼得;选错,用户要么看到旧价签,要么离线对着空白页叹气。

r={cache缓存优先 / 陈旧更新network⇒cache网络优先network only仅网络r =\begin{cases}\text{cache} & \text{缓存优先 / 陈旧更新} \\\text{network} \Rightarrow \text{cache} & \text{网络优先} \\\text{network}\ \text{only} & \text{仅网络}\end{cases}
没有全能的策略,只有最合适的场景。
  1. 缓存优先命中即返回缓存响应,未命中则回源并写入缓存。
    r=cache.match(x)⇒c,否则 f(x)→putr = \text{cache.match}(x) \Rightarrow c,\quad \text{否则}\ f(x) \to \text{put}
  2. 网络优先先请求网络,失败时降级读取缓存。
    r=fetch(x) 失败⇒cache.match(x)r = \text{fetch}(x)\ \text{失败} \Rightarrow \text{cache.match}(x)
  3. 陈旧更新立刻返回旧缓存,同时在后台刷新缓存。
    rnow=cache,fetch(x)→cache.put(x)r_{\text{now}} = \text{cache},\quad \text{fetch}(x) \to \text{cache.put}(x)

缓存 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 与生命周期配合的最佳实践。

chain: open(name)→put仓库→match命中响应\text{chain}:\ \text{open(name)} \xrightarrow{\text{put}} \text{仓库} \xrightarrow{\text{match}} \text{命中响应}
Cache API 的四个动作,就是仓库管理的基本功。
  1. 打开仓库打开或创建命名缓存,返回一个 Cache 对象。
    cache=await caches.open(’static-v1’)\text{cache} = \text{await}\ \text{caches.open}(\text{'static-v1'})
  2. 入库与查货把响应写入缓存,再用请求去匹配响应。
    put(req,res),res=await match(req)\text{put}(\text{req}, \text{res}), \qquad \text{res} = \text{await}\ \text{match}(\text{req})
  3. 版本演进升级版本号并删除旧仓库,完成平滑迁移。
    delete(’static-v1’),open(’static-v2’)\text{delete}(\text{'static-v1'}),\quad \text{open}(\text{'static-v2'})

背景同步:断网也能接单的“待发簿”

断网时,顾客硬要寄一个急件。普通杂货铺店员只能摇头,但驿站伙计会麻利地把单子收进“待发簿”,登记好收件人和货物;等路一通,立刻骑着三轮车统一派送。顾客要的从来不是“立刻送达”,而是“不会丢件”。

Background Sync API 正是浏览器提供的“待发簿”。开发者在页面里通过 navigator.serviceWorker.ready.then(reg => reg.sync.register('tag')) 注册一个带标签的同步任务;当浏览器检测到网络恢复,就给 Service Worker 派发 sync 事件。开发者在这个事件里把离线期间积压的请求重新发出——表单、评论、埋点日志这些“欠着的账”终于可以结了。

有一个必须接受的现实:sync 事件的触发时机由浏览器启发式调度,取决于电量、网络质量、用户活跃度等因素,不是“断网→恢复→立刻”的精确时序。因此它应当被当成“保证最终送达”的机制,而不是定时器。请求量大时,建议维护队列,成功一条出队一条,失败则留在队里等待下一次同步——但重试有次数上限,超限后请求会被丢弃。

有了这本“待发簿”,驿站在市场关闭时依然敢接单,顾客的信任也就留住了。对 AI 应用而言,离线问答记录、模型使用日志、埋点上报,都靠它实现“迟到的送达”,一份数据都不丢。

register(tag)→网络恢复sync→∑ifetch(qi)\text{register(tag)} \xrightarrow{\text{网络恢复}} \text{sync} \to \sum_{i} \text{fetch}(q_i)
把“必须送达”的请求交给浏览器调度,而不是自己硬闯网络。
  1. 登记待发在页面里注册带标签的同步任务。
    reg.sync.register(’send-log’)\text{reg.sync.register}(\text{'send-log'})
  2. 自动触发网络恢复时,浏览器向 SW 派发 sync 事件。
    addEventListener(′sync′)⇒flush 队列\text{addEventListener}('sync') \Rightarrow \text{flush 队列}
  3. 逐条补发成功一条出队一条,失败等待下次同步。
    q=q∖{ 已成功送达的请求 }q = q \setminus \{\ \text{已成功送达的请求}\ \}

应用清单:驿站的装修图纸

想让驿站被顾客当成“正经门店”而非流动摊贩,就得装修门面、挂招牌、印名片。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 融为一体。这些细节看似不起眼,却正是“网址”与“应用”之间体验差距的来源。

装修图纸本身不产生离线能力,却是用户把驿站“装进口袋”的钥匙——没有它,再可靠的站点也只是一个“网址”;有了它,网页才有了名字、图标和全屏入口,像原生应用一样住在用户的主屏上。

manifest: {short_name, icons192, icons512, start_url, display}\text{manifest}:\ \{ \text{short\_name},\ \text{icons}_{192},\ \text{icons}_{512},\ \text{start\_url},\ \text{display} \}
一份 JSON,决定了用户把它当网址还是当应用。
  1. 门店名片名称与双尺寸图标,决定桌面图标的身份感。
    short_name,icons: […192px, 512px, … ]\text{short\_name},\quad \text{icons}:\ [\dots 192\text{px},\ 512\text{px},\ \dots]
  2. 启动地址指定点击图标后冷启动的页面路径。
    start_url=’/index.html’\text{start\_url} = \text{'/index.html'}
  3. 展示形态在产品与浏览器之间的选择,决定沉浸感。
    display∈{ fullscreen, standalone, minimal-ui, browser }\text{display} \in \{\ \text{fullscreen},\ \text{standalone},\ \text{minimal-ui},\ \text{browser}\ \}

推送通知:驿站的主动派单

驿站不满足于“等人上门”,它还会主动给顾客发“您的包裹已到”的手机提醒。这需要两套设备:一本“订阅登记簿”(Push 订阅)记录该往哪儿发,一个“到货广播”(Notification)让前台把消息亮出来。两者配合,服务就从被动等待变成了主动触达。

Push 与 Notification 是 PWA 主动触达用户的两段式组合:首先,在用户授权后,浏览器向推送服务生成一个订阅对象,页面把其中的 endpoint 上报给你的服务器;随后,服务器向 endpoint 发送消息,浏览器唤醒沉睡的 Service Worker,触发 push 事件;最后,SW 调用 self.registration.showNotification(title, options) 展示通知,并监听 notificationclick 事件处理点击跳转——通常用 clients.openWindow(url) 打开落地页面。

权限体验是推送的关键:浏览器会抑制可疑的自动弹窗,所以授权请求必须放在用户点击“开启提醒”等明确交互之后,而不是一进页面就弹。另外,背景同步与推送天然互补:离线提交成功后再推送一条“同步完成”,整套“断网可用、联网必达、主动通知”的体验闭环就此打通。

推送让驿站的服务从被动等待变成主动触达。至此,仓库(缓存)、管理员(SW)、待发簿(背景同步)、装修(Manifest)和广播(推送)全部就位——一个接近原生体验的渐进式 Web 应用,正式交付。

subscribe→endpoint服务器→pushSW→showNotification用户\text{subscribe} \xrightarrow{\text{endpoint}} \text{服务器} \xrightarrow{\text{push}} \text{SW} \xrightarrow{\text{showNotification}} \text{用户}
推送与权限是一体两面:尊重用户,用户才愿意被触达。
  1. 订阅登记获取推送订阅对象,并把 endpoint 上报服务器。
    endpoint=await pushManager.subscribe(… )\text{endpoint} = \text{await}\ \text{pushManager.subscribe}(\dots)
  2. 服务器推送服务器向 endpoint 发送消息体,浏览器唤醒 SW。
    POST endpoint,payload→SW 的 push 事件\text{POST}\ \text{endpoint},\quad \text{payload} \to \text{SW 的 push 事件}
  3. 前台广播SW 展示系统通知,并处理点击跳转。
    showNotification(title)→notificationclick→openWindow(url)\text{showNotification}(\text{title}) \to \text{notificationclick} \to \text{openWindow}(\text{url})

AI 应用场景

AI 离线知识助手的弱网守护

阶段:应用部署与体验优化

解决的问题:传统 Web 端 AI 助手强依赖网络,弱网环境下知识检索卡顿、问答直接中断。通过 PWA 的缓存优先策略,把常用知识卡片、模型配置等静态资源预缓存,离线时用户仍能查阅历史问答、浏览常用提示词;离线产生的反馈和日志则交给背景同步,在网络恢复后自动补发。

具体案例:某制造业内部部署的 AI 知识助手采用 PWA 架构,车间现场网络极不稳定。借助 Service Worker 预缓存设备手册与故障排查流,工人在信号盲区也能离线查阅资料;提问记录通过 Background Sync 在联网后批量回传,用于后续模型微调的数据积累。

端侧 AI 推理的延迟数据回传

阶段:数据采集与模型迭代

解决的问题:边缘端 AI 设备(如巡检机器人)在信号盲区完成识别任务,结果需要回到云端才有效。传统方案用定时器疯狂重试,既耗电又容易丢数据;改用 PWA 的背景同步后,识别结果先“寄存”在本地队列,由浏览器智能调度、在网络恢复时统一批量上传,实现零人工干预的可靠回传。

具体案例:某园区巡检机器人内置 Web 端推理界面,在隧道与地下车库等盲区完成表计读数识别后,通过 Background Sync 自动上报识别结果与图像缩略图;同步完成后由推送通知告知运维人员“今日数据已全部入库”,形成完整的离线闭环。

Python 演示

python109 行
"""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), "条")
目的:模拟四种缓存策略在断网场景下的成功率差异,并演示背景同步队列在网络恢复后的统一补发流程。
  1. class CacheBox:对应 Cache API 的命名货架,管理请求响应对
  2. def match(self, key):查缓存,命中与未命中分别计数
  3. def put(self, key, value):把响应写回仓库,供后续离线使用
  4. def cache_first(cache, key, online):先翻仓库,没货再回源
  5. def network_first(cache, key, online):先问网络,离线降级读缓存
  6. online = step % 5 != 0用步数制造 20% 的离线时段
  7. def register(self, task):断网时把请求登记进待发簿
  8. def flush(self, online):网络恢复后统一补发队列中的请求

常见误区

误区我在页面里调用了 `serviceWorker.register()`,断网后刷新应该就能用了,但页面还是一片空白。

正解注册只让管理员“入职”,仓库(缓存)还是空的。真正的离线能力来自你手动把资源 caches.put 或 cache.addAll 进缓存。好比驿站开张了,但货架空空如也,顾客来了照样两手空空。要在 install 或 fetch 事件里完成缓存的写入与读取,离线才会生效。

为何常见:注册代码是官方文档的第一步,容易被误当成“一键离线开关”,而忽略了后续的缓存填充动作。

误区既然要追求离线体验,那所有请求都走缓存优先不就行了?又快又省流量。

正解缓存优先像“只卖存货”,用户可能看到过期的价格、陈旧的资讯,对实时性要求高的业务(余额、库存、汇率)是灾难。正确做法是按资源性质组合策略:静态资源用缓存优先或陈旧更新,实时数据用网络优先,写入请求仅网络加背景同步兜底。

为何常见:缓存优先代码最简单,离线演示效果肉眼可见,容易被误当成“一招鲜”。

误区我调用了 `sync.register`,按我的理解,网络一恢复它就该马上执行,可我测试时发现它半天不动。

正解同步事件的时机由浏览器启发式调度,涉及电量、网络质量、用户活跃度等因素,不是“断网→恢复→立刻”的精确响应。工程师应把它当作“保证最终送达”的机制,而非“准时触发”的定时器。尤其要避免在 sync 事件里做耗时过长的任务。

为何常见:教程示例省略了时序细节,让人误以为同步是即时行为。

误区PWA 能离线、能推送、能全屏,添加到主屏幕之后应该和原生 App 一样,什么系统能力都能调用。

正解PWA 能用的系统能力远超普通网页,但仍受 Web 平台 API 的能力边界限制——后台精确定位、原生级多线程、部分蓝牙传感器等仍不可用或受限。PWA 的目标是“接近原生体验”,不是“完全等于原生系统能力”。

为何常见:厂商宣传喜欢强调“类原生”体验,容易让人把体验上的接近误当成能力上的等同。

误区想提高订阅率,所以页面一加载就立刻弹授权框,结果用户拒绝率特别高。

正解浏览器会抑制可疑的自动弹窗;在用户没有任何交互时请求权限,往往被拒甚至被浏览器拦截。最佳实践是在用户点击“开启提醒”等明确操作后请求授权——就像服务员等顾客坐定再递菜单,而不是进门就塞广告。

为何常见:把“尽早触达”误解为“越早弹窗越好”,忽略了浏览器的权限卫生机制与用户心理。

面试题

Q1. PWA 的三大核心组件通常指什么?

单选题 · easy

  • 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 生命周期状态的正确顺序?

单选题 · easy

  • A.安装中、已安装、激活中、已激活、废弃← 正确答案
  • B.安装中、激活中、已激活、已安装、废弃
  • C.激活中、已激活、废弃、安装中、已安装
  • D.待命、安装、激活、终结、启用

答案:A。生命周期的固定顺序是 installing(安装中)→ installed(已安装)→ activating(激活中)→ activated(已激活)→ redundant(废弃)。就像驿站管理员先培训、领工牌、到岗、接钥匙,最后被取代。选项 B 把“已安装”和“已激活”顺序颠倒,C、D 完全打乱,均不正确。

Q3. 缓存优先(cache-first)策略最适合下列哪类资源?

单选题 · mid

  • A.用户账户余额的实时查询
  • B.静态图片与脚本资源加载← 正确答案
  • C.在线聊天消息的实时收发
  • D.支付表单的即时提交

答案:B。缓存优先以过期风险换速度和离线能力,适合版本号固定、几乎不变的静态资源。余额查询、聊天消息、支付提交都要求数据新鲜或必须实时到达,一旦命中旧缓存会带来业务风险,应改用网络优先或仅网络。

Q4. 关于陈旧更新(stale-while-revalidate)策略,下列描述正确的是?

单选题 · mid

  • A.只读缓存,永不更新数据
  • B.先请求网络再读取缓存
  • C.先回旧缓存并后台刷新缓存← 正确答案
  • D.仅走网络,失败即报错

答案:C。陈旧更新先立刻返回旧缓存让用户秒开,再在后台发起网络请求,拿到新响应后更新缓存。A 是无更新的纯缓存,B 是网络优先,D 是仅网络,只有 C 准确描述了“先旧后新、后台补新”的同步行为。

Q5. 注册 Background Sync 任务的正确入口是哪个?

单选题 · hard

  • 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?

多选题 · mid

  • A.离线提交的表单数据补发← 正确答案
  • B.埋点日志的批量上报← 正确答案
  • C.实时视频流的在线传输
  • D.弱网环境下的评论发布← 正确答案

答案:ABD。背景同步的价值是“网络恢复后自动补发”,适合表单、日志、评论这类可以延迟的请求。实时视频流必须连续在线传输,无法通过延迟补发解决,因此 C 不适合。

Q7. Cache API 中用于“打开或创建命名缓存”的方法是?

单选题 · mid

  • 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“可安装”能力的必要条件组合?

单选题 · hard

  • 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 的配合流程,下列说法正确的是?

多选题 · hard

  • A.推送后浏览器触发 SW push 事件← 正确答案
  • B.SW 用 showNotification 展示通知← 正确答案
  • C.点击通知后需 openWindow 落地← 正确答案
  • D.用户无需授权即可收到推送

答案:ABC。完整链路是:服务器向 endpoint 发消息 → 浏览器唤醒 SW 触发 push 事件 → SW 调 showNotification 展示 → 用户点击触发 notificationclick → openWindow 打开落地页。Web 推送必须经过用户显式授权,D 说反了。

Q10. 网络优先策略在网络请求失败后,正确的降级行为是?

单选题 · mid

  • A.请求直接报错并丢弃
  • B.回退读取缓存的旧响应← 正确答案
  • C.清空全部缓存再重试
  • D.等待用户手动点击刷新

答案:B。网络优先的核心是“先网络、后缓存”:在线时用最新数据并写缓存,离线或请求失败时回退读取缓存的旧响应,牺牲一点新鲜度换取可用性。直接报错、清缓存或等用户刷新都没有利用已建立的缓存资产。