技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

面试官:请你实现一个 PWA,我:😭

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/135-面试官:请你实现一个PWA 我:😭
本文目录23 个章节
  1. 前言
  2. 问题 1:什么是 PWA?
  3. 问题 2:PWA 有哪些优点?
  4. 问题 3:PWA 有哪些缺点?
  5. 问题 4:如何让一个 Web 应用成为 PWA?
  6. 问题 5:为什么 PWA 需要 Web App Manifest?
  7. 问题 6:PWA 有哪些原生 App 不具备的特性?
  8. 问题 7:Service Worker 是什么?
  9. 问题 8:PWA 与 Hybrid 有什么不同?
  10. 问题 9:什么是 fetch 事件?
  11. 问题 10:对网站进行 PWA 安装有哪些要求?
  12. 问题 11:什么是 IndexedDB,如何在 PWA 中使用?
  13. 问题 12:什么是 CacheStorage?
  14. 问题 13:说说 Service Worker 生命周期
  15. 问题 14:如何更新 Service Worker?
  16. 问题 15:Service Worker 常见缓存策略
  17. 问题 16:什么是 App Shell?
  18. 问题 17:App Shell 应具有什么特性?
  19. 问题 18:苹果设备对 PWA 的支持怎样?
  20. 问题 19:Service Worker 能否同时存在多个?
  21. 问题 20:Service Worker 能做而 Web Worker 不能做什么?
  22. 问题 21:使用 Service Worker 的 App Shell 架构有哪些优势?
  23. 问题 22:PWA 能拥有真正的持久存储吗?

面试官:请你实现一个 PWA,我:😭

Category(分类): JavaScript Status: 未知

配图来自原文页面,已下载并转换为本地 WebP;原始授权未核实,发布前请人工确认。

前言

渐进式 Web 应用(Progressive Web App,简称 PWA)使用 Web 平台技术构建,但可以在支持的浏览器和操作系统中提供接近独立应用的体验,例如安装到主屏幕、离线或弱网工作、推送通知和系统集成。

PWA 不是一个单独的框架,也不是一份在所有浏览器中都完全相同的功能清单。它强调渐进增强:不能安装或不支持 Service Worker 的浏览器,仍然应该能把应用当作普通网站使用;可安装性、通知、后台同步、硬件访问和展示模式都取决于浏览器、操作系统、权限和用户设置。

原文配图 135-01

原文配图 135-02

原文配图 135-03

原文以 2020–2021 年的面试场景为背景,下面保留 22 个问题,并把已经变化的结论标注出来。

问题 1:什么是 PWA?

难度:⭐

PWA 是使用 HTML、CSS、JavaScript 和各种 Web API 构建的 Web 应用。它可以通过 URL 被发现、分享和搜索,也可以在支持的平台上安装,以独立窗口或浏览器标签页运行。

一个成熟的 PWA 往往具备以下特征:

  • 可发现:内容仍然是 Web 内容,可以被链接和搜索。
  • 可安装:通过 Web App Manifest 和浏览器/平台支持,显示为设备上的应用入口。
  • 可离线或弱网工作:通常借助 Service Worker、Cache API 和 IndexedDB。
  • 可重新参与:在获得权限且平台支持时使用 Web Push、Notifications、Badging 等能力。
  • 响应式和可访问:适配不同屏幕、输入方式和辅助技术。
  • 渐进增强:缺少某项 API 时仍能提供基本 Web 功能。

“像原生 App”是用户体验目标,不代表 PWA 自动拥有原生应用的全部系统权限。硬件、后台任务、推送和文件访问都要逐项检查兼容性和权限。

问题 2:PWA 有哪些优点?

难度:⭐⭐

原文提到的优点仍有参考价值,但不应把它们写成无条件保证:

  1. 一个代码库覆盖多个平台:减少重复开发,但仍需要针对浏览器、屏幕、输入方式和系统能力做适配。
  2. 可链接、可搜索、可分享:这是 Web 的优势;SEO 取决于内容可抓取、渲染方式、结构化数据和性能,并不是安装 PWA 自动带来排名提升。
  3. 安装摩擦较低:用户可以直接使用 URL;支持的平台还可以把它安装为应用。但安装提示由浏览器决定,不是每次访问都会出现。
  4. 更新灵活:网页资源可以从服务器更新,不需要等待应用商店审核;Service Worker 缓存策略设计不当时也可能导致旧资源,所以必须设计版本化和更新提示。
  5. 离线和弱网能力:Service Worker 可以拦截受控页面的请求,Cache API 可以保存静态资源,IndexedDB 可以保存结构化数据。
  6. 响应式体验:同一 Web 应用可以适配手机、平板、桌面等设备,但需要真正的响应式布局和可访问性实践。
  7. 安全模型清晰:Service Worker、Push 等能力通常要求 HTTPS 或 localhost 安全上下文;HTTPS 是必要条件之一,不等于应用代码天然安全。
  8. 可以使用 Web 平台能力:通知、分享、文件、蓝牙、USB、地理位置等 API 的可用范围和权限各不相同,不能统一概括为“访问设备硬件”。
  9. 可选择应用商店分发:PWA 可以只通过 Web 分发,也可以使用 PWABuilder、Trusted Web Activity 等方式打包到部分应用商店;商店审核并没有被 PWA 永久绕过。

“更小更快”“零安装”“不占磁盘”都是可能的体验,不是 PWA 的必然结果。首次加载仍需要下载资源,安装后也会占用存储空间。

问题 3:PWA 有哪些缺点?

难度:⭐⭐

  1. 平台能力不完全一致:浏览器和操作系统对安装、推送、后台同步、文件系统、蓝牙、分享和窗口控制的支持不同。
  2. 后台执行受限制:Service Worker 可以被浏览器终止并在事件发生时重新启动,不能当成一个一直运行的后台进程。
  3. 存储不是永久保险箱:Cache Storage、IndexedDB 等数据受到配额、用户清理和存储压力回收影响;需要持久业务数据时应同步到服务器,并按需申请持久化存储。
  4. 调试和更新复杂:Service Worker 有独立生命周期,旧版本可能处于 waiting,缓存版本管理错误会造成白屏或旧资源混用。
  5. 系统集成有限且需要权限:通知、推送、后台同步和硬件 API 可能需要用户授权,在部分浏览器或平台不可用。
  6. 应用商店分发仍有成本:如果要进入应用商店,仍需遵守对应商店的打包、审核和隐私政策。
  7. 性能并非天然优于原生:复杂图形、持续后台任务、深度系统集成和大规模本地计算可能更适合原生或其他专用方案。

iOS/iPadOS 的 PWA 能力已经比早期文章发布时丰富:Service Worker、Cache API 和“添加到主屏幕”均可用,iOS/iPadOS 16.4 起主屏幕 Web App 还支持 Web Push。但具体能力仍应以目标版本和 WebKit 文档为准。

问题 4:如何让一个 Web 应用成为 PWA?

难度:⭐⭐

可以从以下方面逐项建设:

  • 可发现:页面有真实 URL、清晰内容和合理的 SEO/可访问性结构。
  • 可安装:提供 Web App Manifest,并满足目标浏览器的平台条件。
  • 安全:通过 HTTPS 提供;本地开发通常使用 localhost127.0.0.1
  • 可离线或弱网工作:注册 Service Worker,设计合适的缓存和回退策略。
  • 渐进增强:不支持某项 API 时仍能正常浏览和完成核心任务。
  • 可重新参与:在权限、浏览器和平台都支持时加入 Push、Notifications、Badging 等能力。
  • 响应式和可访问:适配设备、输入方式、屏幕阅读器和键盘操作。
  • 可维护:缓存有版本号,Service Worker 更新有回滚和用户提示方案。

PWA 没有一张能覆盖所有浏览器的“勾选表”。基于 Chromium 的安装提示有自己的 manifest 和 Service Worker 条件,iOS、Safari、Firefox 和桌面平台的安装流程也不同。

问题 5:为什么 PWA 需要 Web App Manifest?

难度:⭐⭐

Web App Manifest 是一个 JSON 文件,用来描述应用的名称、图标、启动地址、显示模式、主题色、作用域等信息。它是浏览器了解“如何把这个 Web 应用作为应用展示”的重要入口,但不是权限声明,也不会自动授予通知或硬件权限。

在 HTML 中引用:

<link rel="manifest" href="/manifest.webmanifest">

一个现代的最小示例:

{
  "id": "/",
  "name": "我的 PWA",
  "short_name": "我的应用",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

不同浏览器的必需字段和图标要求可能不同。id 用于稳定识别应用,start_urlscope、图标 URL 和 manifest 本身应满足同源/CORS 等加载条件;manifest 不要求必须放在网站根目录。

问题 6:PWA 有哪些原生 App 不具备的特性?

难度:⭐⭐⭐

与传统原生应用相比,PWA 的 Web 特性更突出:

  • URL 可直接分享,应用内的页面可以成为独立入口。
  • 搜索引擎可以抓取公开内容,具体效果取决于渲染和 SEO 实现。
  • 用户可以先访问再决定是否安装,不一定需要先进入应用商店。
  • Web 应用可以快速发布和迭代,不必为每次内容更新提交商店审核。
  • 同一套 Web 代码可以覆盖多个操作系统和设备。
  • Service Worker 可以在合适的时机提供离线缓存。

这些优势也伴随限制:Web 权限模型更严格,浏览器可能拒绝安装提示、限制后台执行或回收本地存储。不能把“绕过应用商店”理解成绕过安全、隐私和分发规则。

问题 7:Service Worker 是什么?

难度:⭐⭐⭐

Service Worker 是运行在独立 Worker 上下文中的事件驱动脚本,通常用于代理受控页面的网络请求、缓存资源、处理 Push/Notification 相关事件以及部分后台任务。它:

  • 没有 DOM 访问权限;
  • 通常要求 HTTPS,localhost 等本地安全来源可用于开发;
  • 受 scope 限制,只能控制作用域内的页面和请求;
  • 可能被浏览器终止,下一次事件到来时再启动;
  • 通过 event.waitUntil() 延长安装、激活等生命周期事件;
  • 不是一个一直运行的后台线程,也不能直接操纵页面 DOM。

注册 Service Worker:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then(registration => {
      console.log('注册成功', registration.scope)
    })
    .catch(error => {
      console.error('注册失败', error)
    })
}

Service Worker 的能力并不等于所有 PWA 能力。Push、Background Sync、Periodic Background Sync 等 API 需要分别检查浏览器支持和权限。

问题 8:PWA 与 Hybrid 有什么不同?

难度:⭐⭐⭐

Hybrid 通常把 Web 技术运行在原生 WebView 中,并通过 JavaScript Bridge 调用原生能力,再以原生应用包的形式通过应用商店分发。Cordova、Capacitor、Ionic 等方案都可以构建这类应用,具体架构因工具而异。

PWA 主要运行在浏览器或浏览器提供的独立窗口中,依靠 Web API 和 Service Worker,不必先安装原生应用包。PWA 也可以借助 Trusted Web Activity、PWABuilder 等工具进入部分应用商店,因此二者并不是“只能商店发布”和“绝不能商店发布”的简单二分。

简单对比:

方面PWAHybrid
运行容器浏览器或独立 Web App 窗口原生 WebView/应用容器
分发方式URL、安装提示、可选应用商店通常是应用商店安装包
原生能力受浏览器权限和 API 支持限制可通过 Bridge 使用更多原生 API
更新方式Web 资源可直接更新,但要管理缓存代码和原生能力受应用包版本影响
适合场景可链接、跨平台、离线 Web 应用需要深度系统集成或商店能力的应用

问题 9:什么是 fetch 事件?

难度:⭐⭐⭐

当一个页面已经被 Service Worker 控制后,作用域内符合条件的请求可以触发 Service Worker 全局的 fetch 事件。第一次注册 Service Worker 的页面通常需要重新加载,或者由 Service Worker 调用 clients.claim() 后才会被控制。

一个更安全的 cache-first 示例:

self.addEventListener('fetch', event => {
  const request = event.request

  // 通常只缓存 GET;POST 等请求不要直接放进 Cache API。
  if (request.method !== 'GET') return

  event.respondWith((async () => {
    const cached = await caches.match(request)
    if (cached) return cached

    try {
      const response = await fetch(request)
      const url = new URL(request.url)

      // 示例只缓存同源且成功的响应,实际项目应按资源类型设计策略。
      if (response.ok && url.origin === self.location.origin) {
        const cache = await caches.open('runtime-v1')
        await cache.put(request, response.clone())
      }

      return response
    } catch (error) {
      const fallback = await caches.match('/offline.html')
      if (fallback) return fallback
      throw error
    }
  })())
})

缓存策略必须考虑请求方法、响应状态、同源/CORS、缓存版本和隐私数据。不能把所有请求都缓存,也不能把缓存当成服务器数据的唯一来源。

问题 10:对网站进行 PWA 安装有哪些要求?

难度:⭐⭐⭐

没有一份适用于所有浏览器的统一安装清单。常见的 Chromium Web 安装条件包括:

  • 通过 HTTPS 提供,或使用 localhost 等安全开发来源;
  • HTML 正确引用 Web App Manifest;
  • manifest 包含目标浏览器要求的名称、图标、start_urldisplay 等字段;
  • 注册 Service Worker,并提供能支持基本离线体验的 fetch 事件处理;
  • 应用满足浏览器的用户体验、内容和平台要求。

在 iOS/iPadOS 上,用户可以通过分享菜单将网站添加到主屏幕,流程和 Chromium 的安装提示不同。安装提示也可能因用户已经安装、浏览器策略、设备平台或应用状态而不出现。

因此工程上应使用浏览器开发者工具、Lighthouse 和目标设备实测,不能只检查一个 manifest 文件就断言“已经是可安装 PWA”。

问题 11:什么是 IndexedDB,如何在 PWA 中使用?

难度:⭐⭐⭐

IndexedDB 是浏览器提供的异步、事务性、面向对象的客户端数据库,适合存储较大量的结构化数据、索引和 Blob。它与 Cache API 的职责不同:

  • 静态资源和 Request/Response:通常使用 Cache API;
  • 业务数据、离线队列、结构化对象:通常使用 IndexedDB;
  • 需要跨设备或不可丢失的数据:仍应同步到服务器。

IndexedDB API 较底层,实际项目可以使用 Dexie 等库简化事务和版本迁移,但要评估依赖和兼容性。

const request = indexedDB.open('pwa-demo', 1)

request.onupgradeneeded = () => {
  const database = request.result
  if (!database.objectStoreNames.contains('notes')) {
    database.createObjectStore('notes', { keyPath: 'id' })
  }
}

request.onsuccess = () => {
  const database = request.result
  const transaction = database.transaction('notes', 'readwrite')
  transaction.objectStore('notes').put({ id: 1, text: '离线笔记' })
}

request.onerror = () => {
  console.error('IndexedDB 打开失败', request.error)
}

问题 12:什么是 CacheStorage?

难度:⭐⭐⭐

CacheStorage 是管理多个命名 Cache 对象的接口,通常通过全局 caches 访问。CacheRequest 为键、Response 为值,常用于 Service Worker 的资源缓存。

CacheStorage 不等同于浏览器 HTTP 缓存,也不是只能在 Service Worker 中使用;Window 和 Worker 上下文在支持时都可以访问。但它不会自动根据 HTTP Cache-Control 进行所有版本管理,缓存何时更新和删除要由应用负责。

常用方法:

  • caches.open(name):打开或创建命名缓存;
  • cache.match(request) / caches.match(request):查找响应;
  • cache.put(request, response):写入响应;
  • cache.addAll(urls):请求并缓存一批资源;
  • caches.keys()caches.delete(name):清理旧版本。

响应体通常是流,网络响应在返回页面后如果还要写入缓存,需要使用 response.clone()

async function cacheNetworkResponse(request) {
  const response = await fetch(request)
  const cache = await caches.open('runtime-v1')
  await cache.put(request, response.clone())
  return response
}

问题 13:说说 Service Worker 生命周期

难度:⭐⭐⭐⭐

Service Worker 生命周期独立于页面,常见阶段如下:

  1. 注册:页面调用 navigator.serviceWorker.register(),浏览器检查脚本 URL、源和 scope。
  2. 安装 installing:触发 install,通常在这里预缓存静态资源;通过 event.waitUntil() 告知浏览器安装任务何时完成。
  3. 等待 waiting:安装成功后,如果旧版本仍控制页面,新版本通常等待旧版本退出。
  4. 激活 activating/activated:触发 activate,适合删除旧缓存、迁移数据;skipWaiting() 可以请求跳过等待,但要防止新旧资源不兼容。
  5. 控制页面:新激活的 worker 通常控制之后打开或重新加载的页面;clients.claim() 可以请求立即接管已打开页面,也应谨慎使用。
  6. 终止和重新启动:浏览器可以为了节省资源终止 worker,下一次事件到来时再启动它。

原文配图 135-04

一个简化的版本化生命周期示例:

const CACHE_NAME = 'app-shell-v2'
const ASSETS = ['/', '/index.html', '/styles.css', '/app.js', '/offline.html']

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
  )
  // 不建议在不了解新旧页面兼容性的情况下盲目 skipWaiting。
})

self.addEventListener('activate', event => {
  event.waitUntil((async () => {
    const keys = await caches.keys()
    await Promise.all(
      keys
        .filter(key => key !== CACHE_NAME)
        .map(key => caches.delete(key))
    )
  })())
})

如果 install 中的 Promise 失败,安装会失败;如果新旧版本共享资源名但内容不兼容,贸然 skipWaiting 可能使旧页面加载新资源而出错。

问题 14:如何更新 Service Worker?

难度:⭐⭐⭐⭐

常见更新流程如下:

  1. 发布新的 Service Worker 脚本或使其内容发生字节变化。
  2. 浏览器在导航或更新检查时获取新脚本并安装新版本。
  3. 新版本完成 install 后,如果旧页面仍受旧 worker 控制,新版本进入 waiting
  4. 旧页面关闭或新版本通过明确的更新流程接管后,触发 activate
  5. activate 中删除不再使用的缓存,并通过页面消息提示用户刷新。

缓存名称或预缓存清单应版本化:

const CURRENT_CACHE = 'app-shell-v3'

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys
        .filter(key => key.startsWith('app-shell-') && key !== CURRENT_CACHE)
        .map(key => caches.delete(key))
    ))
  )
})

生产环境还要注意 HTTP 缓存对 Service Worker 脚本的影响、回滚策略、资源原子发布,以及 skipWaiting()/clients.claim() 引起的新旧页面不一致。

问题 15:Service Worker 常见缓存策略

难度:⭐⭐⭐⭐

缓存策略要按资源类型、实时性和失败回退设计,常见策略包括:

1. Cache Only(仅缓存)

只从缓存读取,适合明确在安装阶段预缓存的离线资源;缓存没有命中时会失败。

Cache Only

2. Network Only(仅网络)

始终访问网络,不读取 Cache API,适合必须实时获取且不应使用旧数据的请求,例如部分管理操作和写入请求。

Network Only

3. Stale-While-Revalidate(缓存优先并后台更新)

先快速返回缓存,同时在后台请求网络并更新缓存,适合头像、样式、图片等允许短暂旧数据的资源。要处理缓存未命中、请求失败和并发更新。

Stale While Revalidate

4. Cache First(缓存优先)

缓存命中就返回,未命中才请求网络并写入缓存,适合带 hash 的静态资源;对于 HTML 和实时业务数据要谨慎,否则容易显示旧页面。

Cache First

5. Network First(网络优先)

优先访问网络,网络失败时回退缓存,适合需要较新内容但又要兼容离线的导航或 API 请求。可以设置超时,但超时不是所有环境都自动提供,需要自己实现或使用成熟工具。

Network First

Angular、Workbox 等框架和工具会提供类似的策略名称,但具体实现、匹配规则和缓存淘汰策略仍需要查看文档。不要把 Cache API 与 HTTP 缓存、CDN 缓存混为一谈。

问题 16:什么是 App Shell?

难度:⭐⭐⭐⭐

App Shell 是应用启动所需的最小 HTML、CSS、JavaScript 和基础资源。把它预缓存后,应用可以更快显示基本界面,再从网络或本地数据库加载动态内容。

它适合导航相对稳定、内容持续变化的应用,例如后台系统、邮件、社交信息流。App Shell 不是必须采用的架构;SSR、MPA、岛屿架构或普通静态网站也可以通过合理的缓存获得良好体验。不要把完整的动态数据和用户隐私数据盲目预缓存。

问题 17:App Shell 应具有什么特性?

难度:⭐⭐⭐⭐

一个合适的 App Shell 通常应该:

  • 快速加载并尽快展示可用界面;
  • 使用尽可能少的数据;
  • 只缓存确实需要的静态资源;
  • 把导航、布局和动态内容分离;
  • 在离线时提供明确的状态和回退页面;
  • 通过网络、Cache API 或 IndexedDB 获取页面数据;
  • 有明确的版本、更新和缓存清理策略;
  • 兼顾无障碍、性能、错误处理和安全。

问题 18:苹果设备对 PWA 的支持怎样?

难度:⭐⭐⭐⭐⭐

原文基于 Safari 11.1/iOS 11.3 的历史状态,不能直接作为当前结论。现代 iOS/iPadOS 已支持 Service Worker、Cache API 和将网站添加到主屏幕;iOS/iPadOS 16.4 起,添加到主屏幕的 Web App 支持 Web Push 和通知能力。

仍需注意:

  • 能力受 iOS/iPadOS 版本、浏览器和“是否添加到主屏幕”等条件影响;
  • Web Push 的权限、用户手势和服务端推送配置仍然必须满足要求;
  • 后台同步、周期性同步、文件系统、窗口控制等能力不能按 Chromium 行为推断;
  • iOS 的安装入口通常是分享菜单,不能依赖 Chromium 的 beforeinstallprompt
  • Safari 的存储配额、回收策略和 WebKit 行为应在目标设备上实测。

结论应写成“按目标版本检查 WebKit 和 MDN 兼容性”,而不是简单地说 iOS 支持或不支持 PWA。

问题 19:Service Worker 能否同时存在多个?

难度:⭐⭐⭐⭐⭐

同一个源可以注册多个 Service Worker,但每个注册都有自己的 scope;同一请求通常由匹配范围最长的注册控制。一个 scope 在同一时间只有一个 active worker 版本,更新时会出现 waiting/activating 等状态。

需要注意:

  • Service Worker 的脚本 URL 必须满足同源和 scope 规则;
  • 不同 scope 的 worker 可能分别控制不同路径;
  • Cache Storage 和 IndexedDB 默认按源共享,不会因为 scope 不同自动隔离;
  • 多个 worker 使用相同缓存名称时可能互相覆盖,因此应使用清晰的命名空间和版本策略;
  • worker 可能被终止,不能假设每个 tab 都拥有一个长期存活的独立实例。

问题 20:Service Worker 能做而 Web Worker 不能做什么?

难度:⭐⭐⭐⭐⭐

Web Worker 主要用于把计算移到独立线程,减轻页面主线程压力;它通常由页面创建,使用 postMessage 通信,没有 DOM 访问权限。

Service Worker 是面向源和 scope 的事件驱动代理,可以在页面未打开时由浏览器按事件启动。它可以处理受控请求、缓存资源、接收 Push,并在支持的浏览器中参与后台同步等。但 Service Worker 也不能直接操作 DOM,也不是持续运行的通用后台线程。

方面Web WorkerService Worker
主要用途并行计算、后台处理请求代理、离线缓存、推送和事件驱动后台任务
生命周期通常由页面/Worker 的拥有者管理由浏览器按注册、事件和资源策略管理,可被终止后重启
作用范围通常服务创建它的页面按 origin 和 scope 控制多个客户端
网络拦截不能接管页面所有请求可以处理受控客户端的 fetch 事件
DOM无法直接访问无法直接访问
通信postMessage、MessageChannel 等Client.postMessage、MessageChannel 等

SharedWorker 又是另一种模型,不能把它与普通 Web Worker 或 Service Worker 混为一谈。

问题 21:使用 Service Worker 的 App Shell 架构有哪些优势?

难度:⭐⭐⭐⭐⭐

  • 重复访问可以更快:已经缓存的 Shell 不必每次重新下载。
  • 离线有基本界面:缓存命中时可以显示导航和回退内容。
  • 动态内容与静态资源分离:数据可以按需请求,静态资源可以长期缓存并通过 hash 更新。
  • 数据使用更可控:只缓存必要资源,避免把所有大图片和无关页面预缓存。
  • 体验接近独立应用:在支持安装和 standalone 显示模式的平台上,可以有独立窗口和启动图标。

代价是需要处理缓存失效、版本升级、存储配额、离线数据冲突和错误回退。App Shell 不是把所有资源“永久缓存”,更不是替代服务端数据一致性的方案。

问题 22:PWA 能拥有真正的持久存储吗?

难度:⭐⭐⭐⭐⭐

原文直接回答“不能”,过于绝对。更准确的说法是:Web 存储受到浏览器配额和用户代理管理,普通 Cache API/IndexedDB 数据可能在存储压力、隐私模式、用户清理或卸载应用时被删除;因此不能把它当作绝对永久存储。

可以查询配额并请求持久化存储:

async function requestPersistentStorage() {
  if (!navigator.storage?.persist) return false

  const alreadyPersistent = await navigator.storage.persisted()
  if (alreadyPersistent) return true

  return navigator.storage.persist()
}

requestPersistentStorage().then(persistent => {
  console.log(persistent ? '已请求持久化存储' : '仍可能被回收')
})

persist() 是否获准由浏览器策略、用户参与度、安装状态和平台决定;即使获准,用户显式清理数据、卸载应用或系统管理存储时仍不能保证永不丢失。

可靠的离线优先应用通常采用组合方案:

  • Cache API 保存可重新下载的静态资源;
  • IndexedDB 保存离线队列和结构化业务数据;
  • navigator.storage.persist() 在适合的场景请求持久化;
  • 关键数据同步到服务器,并处理冲突、重试和身份认证;
  • 退出登录时明确清理包含敏感信息的本地数据。

现代实践补充

  1. 先保证普通 Web 体验,再逐步加入 manifest、Service Worker 和离线能力。
  2. 使用 Workbox 等工具时,仍需理解 precache、runtime caching、版本清理和更新提示,不要盲目套模板。
  3. 不缓存带有用户隐私或认证信息的响应;对 API、HTML、图片和带 hash 的静态资源使用不同策略。
  4. 通过 Lighthouse、浏览器 Application 面板、真实设备和弱网/离线测试验证安装、更新、推送和数据恢复。
  5. 监控 Service Worker 错误、缓存命中率、离线失败和更新失败;出现问题时应能让用户清除或回滚缓存。

参考资料

  1. MDN:渐进式 Web 应用
  2. MDN:让 PWA 易于安装
  3. MDN:使用 Service Worker
  4. MDN:CacheStorage
  5. MDN:IndexedDB API
  6. MDN:StorageManager.persist()
  7. web.dev:Learn PWA
  8. web.dev:Service worker lifecycle
  9. WebKit:Web Push for Web Apps on iOS and iPadOS

作者:Earl_Lam

链接:https://juejin.cn/post/6844904052166230030

来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS