
上次更新日期:2019 年 4 月 30 日
Web 应用为何是渐进式 Web 应用?
渐进式 Web 应用可在网页和移动应用中提供通过网页直接构建和分发的、与应用类似的可安装体验。它们是快速、可靠的 Web 应用。最重要的是,它们是可在任何浏览器中运行的 Web 应用。如果您正在构建一款 Web 应用,那么您已经在朝着构建渐进式 Web 应用的目标前进。
速度快,安全可靠
每个网络体验都必须快速,这在渐进式网页应用中尤其如此。快速是指在屏幕上显示有意义的内容并提供互动式体验所需的时间。
而且,它必须快速可靠。很难充分强调应用性能的高低。可以这样理解:原生应用的首次加载会令人不快。该费用受应用商店和庞大的下载内容的限制,但当您安装应用后,预付费用会在所有应用启动时分摊,并且所有启动都不会产生延迟。每个应用的启动速度都与上一个应用一样快,没有变化。渐进式 Web 应用必须能够提供用户期望从任何已安装的体验中获得的可靠性能。
可安装
渐进式 Web 应用可以在浏览器标签页中运行,但也可以安装。为网站添加书签只会添加快捷方式,但已安装的渐进式 Web 应用 (PWA) 的外观和行为与其他所有已安装应用相同。它会从其他应用的同一位置启动。您可以控制启动体验,包括自定义启动画面和图标等。该应用在没有地址栏或其他浏览器界面的应用窗口中作为应用运行。与所有其他已安装的应用一样,它也是任务切换器中的顶级应用。
请谨记,可安装的 PWA 必须快速可靠。安装 PWA 的用户希望他们的应用能正常运行,无论他们使用的是哪种网络连接。这是一个基准预期,每个已安装的应用都必须满足。
移动设备和桌面设备
借助自适应设计技术,PWA 可在移动设备和桌面设备上运作,并在一个平台之间使用单一代码库。如果您正在考虑编写原生应用,不妨了解一下 PWA 的优势。
构建内容
在此 Codelab 中,您将使用 PWA 技术构建天气 Web 应用。您的应用将:
- 采用自适应设计,适合在桌面设备或移动设备上使用。
- 速度快,使用 Service Worker 预缓存运行所需的应用资源(HTML、CSS、JavaScript、图片)并在运行时缓存天气数据,以提高性能。
- 可安装,使用 Web 应用清单和
beforeinstallprompt事件来通知用户其可安装。

学习内容
- 如何创建和添加 Web 应用清单
- 如何提供简单的离线体验
- 如何提供完整的离线体验
- 如何使您的应用可供安装
本 Codelab 主要介绍渐进式 Web 应用。对于不相关的概念,我们仅会略作介绍,但是会提供不相关的代码块供您复制和粘贴。
所需条件
- 最新版 Chrome(74 或更高版本)。PWA 可在所有浏览器中运行,但我们要使用 Chrome DevTools 的一些功能来更好地了解浏览器级别的状况,并用其测试安装体验。
- 了解 HTML、CSS、JavaScript 和 Chrome 开发者工具。
获取 Dark Sky API 的密钥
我们的天气数据来自 Dark Sky API。要使用它,您需要请求 API 密钥。该工具使用简单,对非商业项目免费。
验证您的 API 密钥是否正常工作
要测试您的 API 密钥能否正常工作,请向 DarkSky API 发出 HTTP 请求。更新以下网址,将 DARKSKY_API_KEY 替换为您的 API 密钥。如果一切正常,您应该会看到纽约市的最新天气预报。
https://api.darksky.net/forecast/DARKSKY_API_KEY/40.7720232,-73.9732319
获取代码
我们已将此项目所需的所有内容放入 Git 代码库中。首先,您需要获取代码并在您喜欢的开发环境中打开。对于此 Codelab,我们建议使用 Glitch。
强烈建议:使用 Glitch 导入代码库
建议使用 Glitch 完成此 Codelab。
- 打开新的浏览器标签页,然后转到 https://glitch.com。
- 如果您还没有帐号,则需要注册一个。
- 点击新建项目,然后点击从 Git 代码库克隆 (Clone from Git Repo)。
- 克隆 https://github.com/googlecodelabs/your-first-pwapp.git,然后点击“OK”。
- 代码库加载完成后,修改
.env文件,并使用 DarkSky API 密钥进行更新。 - 点击显示按钮,然后选择在新窗口中,查看 PWA 的实际应用。
替代方案:下载代码并在本地工作
如果要下载代码并在本地工作,您需要具备最新版本的 Node.js 和代码编辑器设置,并且已准备就绪。
- 解压缩下载的 ZIP 文件。
- 运行
npm install以安装运行服务器所需的依赖项。 - 修改
server.js并设置您的 DarkSky API 密钥。 - 运行
node server.js,以在端口 8000 上启动服务器。 - 打开一个浏览器标签页以访问 http://localhost:8000
我们从何处入手?
我们的起点是专为此 Codelab 设计的基本天气应用。代码已简化为展示此 Codelab 中的概念,并且几乎没有错误处理。如果您选择在正式版应用中重复使用任何代码,请确保处理所有错误并全面测试所有代码。
请尝试以下操作...
- 使用右下角的蓝色 + 按钮添加新城市。
- 使用右上角的刷新按钮刷新数据。
- 使用每张城市卡片右上角的 x 删除城市。
- 使用 Chrome DevTools 中的切换设备工具栏,了解其在桌面设备和移动设备上的运行情况。
- 使用 Chrome DevTools 的 Network 面板,了解离线时会发生什么。
- 使用 Chrome DevTools 中的 Network 面板,了解在网络采用慢速 3G 网络后会发生什么情况。
- 通过更改
server.js中FORECAST_DELAY的值,向预测服务器添加延迟
使用 Lighthouse 进行审核
Lighthouse 是一款简单易用的工具,可帮助提高网站和网页的质量。Lighthouse 会对性能、无障碍功能、渐进式 Web 应用等项目进行审核。每个审核都包含一个参考文档,解释为什么审核很重要以及如何解决问题。

我们将使用 Lighthouse 审核我们的天气应用,并验证我们所做的更改。
运行 Lighthouse
- 在新标签页中打开项目。
- 打开 Chrome 开发者工具并切换到审核面板。请将所有审核类型保持启用状态。
- 点击运行审核。一段时间后,Lighthouse 会在页面上为您提供报告。
渐进式 Web 应用审核
我们将重点关注渐进式 Web 应用审核的结果。

您需要关注很多红色内容:
- ❗失败:离线时,当前页面没有响应,并返回 200。
- ❗ 失败:在离线状态下,
start_url不会返回 200。 - ❗FAILED:未注册用于控制页面和
start_url.的 Service Worker - ❗失败:Web 应用清单不符合可安装性要求。
- ❗失败:未针对自定义启动画面进行配置。
- ❗FAILED:不设置地址栏主题颜色。
现在就开始解决其中一些问题吧!
在本部分结束时,我们的天气应用将通过以下审核:
- Web 应用清单不符合可安装性要求。
- 未针对自定义启动画面进行配置。
- 不设置地址栏主题颜色。
创建 Web 应用清单
Web 应用清单是一个简单的 JSON 文件,您(开发者)可利用它控制应用向用户显示的方式。
通过使用 Web 应用清单,您的 Web 应用可以:
- 告知您希望应用在独立窗口 (
display) 中打开的浏览器。 - 定义首次启动应用时会打开哪个页面 (
start_url)。 - 定义应用在基座或应用启动器(
short_name、icons)上的外观。 - 创建启动画面(
name、icons、colors)。 - 指示浏览器在横屏模式或竖屏模式 (
orientation) 下打开窗口。 - 其他事项。
在项目中创建一个名为 public/manifest.json 的文件。复制并粘贴以下内容:
public/manifest.json
{
"name": "Weather",
"short_name": "Weather",
"icons": [{
"src": "/images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "/images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "/images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "/images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "/images/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}, {
"src": "/images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#3E4EB8",
"theme_color": "#2F3BA2"
}
清单支持一系列适用于不同屏幕尺寸的图标。在此 Codelab 中,我们还针对 iOS 集成介绍了一些其他功能。
添加指向 Web 应用清单的链接
接下来,我们需要向应用中的每个页面添加 <link rel="manifest"...,以将清单告知浏览器。将以下行添加到 index.html 文件的 <head> 元素中。
public/index.html
<!-- CODELAB: Add link rel manifest -->
<link rel="manifest" href="/manifest.json">
DevTools 绕道
DevTools 可让您快速轻松地查看 manifest.json 文件。在 Application 面板上打开 Manifest 窗格。如果您正确添加了清单信息,您就可以在此窗格中看到它以直观易懂的格式解析和显示。

添加 iOS 元标记和图标
iOS 上的 Safari 尚不支持 Web 应用清单(尚),因此您需要将传统 meta 标记添加到 index.html 文件的 <head> 中:
public/index.html
<!-- CODELAB: Add iOS meta tags and icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Weather PWA">
<link rel="apple-touch-icon" href="/images/icons/icon-152x152.png">
奖励:轻松修复 Lighthouse
我们的 Lighthouse 评估指出了一些很容易解决的其他问题,请耐心等候。
设置元描述
根据 SEO 审核,Lighthouse 注意到我们的“文档没有元描述”。说明会显示在 Google 搜索结果中。优质的独特广告内容描述能够使搜索结果与搜索用户更加相关,并且可以提高您的搜索流量。
如需添加说明,请将以下 meta 标记添加到文档的 <head> 部分:
public/index.html
<!-- CODELAB: Add description here -->
<meta name="description" content="A sample weather app">
设置地址栏主题颜色
在 PWA 审核中,Lighthouse 注意到我们的应用“不设置地址栏主题颜色”这一问题。浏览器地址栏与品牌颜色相配,带来更加身临其境的用户体验。
如需在移动设备上设置主题颜色,请将以下 meta 标记添加到文档的 <head> 部分:
public/index.html
<!-- CODELAB: Add meta theme-color -->
<meta name="theme-color" content="#2F3BA2" />
使用 Lighthouse 验证更改
再次运行 Lighthouse(点击 Audits 窗格左上角的 + 号),并验证您所做的更改。
SEO 审核
- ✅ 通过:文档包含元描述。
渐进式 Web 应用审核
- ❗失败:离线时,当前页面没有响应,并返回 200。
- ❗ 失败:在离线状态下,
start_url不会返回 200。 - ❗FAILED:未注册用于控制页面和
start_url.的 Service Worker - ✅ 通过:Web 应用清单符合可安装性要求。
- ✅ 通过:针对自定义启动画面进行配置。
- ✅ 通过:设置地址栏主题颜色。
用户希望,如果已安装的应用始终处于离线状态,他们就会获得基准体验。因此,可安装的 Web 应用永远不会显示 Chrome 的离线恐龙游戏,这一点非常重要。离线体验可以是简单的离线网页,也可以是以前缓存的数据的只读体验,也可以是在网络连接恢复后自动同步的全功能离线体验。
在本部分中,我们将向天气应用添加一个简单的离线页面。如果用户在离线时尝试加载该应用,该应用会显示我们的自定义页面,而不是浏览器显示的典型离线页面。在本部分结束时,我们的天气应用将通过以下审核:
- 离线时,当前页面无响应,并返回 200。
start_url在离线时不会响应 200。- 不会注册控制页面和
start_url.的 Service Worker
在下一部分中,我们将用完整的离线体验取代我们的自定义离线网页。这将改善离线体验,但更重要的是,它将显著提升我们的性能,因为我们的大多数资源(HTML、CSS 和 JavaScript)都将在本地存储和提供,从而消除了网络可能成为瓶颈的问题。
Service Worker 帮助您
如果您不熟悉 Service Worker,可以阅读 Service Worker 简介,大致了解它们的功能、生命周期及其工作原理。
通过 Service Worker 提供的功能应被视为渐进式增强功能,仅在得到浏览器支持时添加。例如,使用 Service Worker,您可以缓存应用的 App Shell 和数据,使其在网络不可用时依然可用。不支持 Service Worker 时,不会调用离线代码,用户只能获得基础体验。使用功能检测来提供渐进式增强功能的开销很小,在不支持该功能的旧浏览器上它也不会中断运行。
注册 Service Worker
第一步是注册 Service Worker。将以下代码添加到您的 index.html 文件中:
public/index.html
// CODELAB: Register service worker.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then((reg) => {
console.log('Service worker registered.', reg);
});
});
}
此代码用于检查 Service Worker API 是否可用。如果可用,则页面加载后就会注册 /service-worker.js 中的 Service Worker。
请注意,Service Worker 从根目录(而非 /scripts/ 目录)提供。这是设置 Service Worker 的 scope 的最简单方法。Service Worker 的 scope 决定了 Service Worker 控制从哪些路径拦截请求的文件。默认的 scope 是 Service Worker 文件的位置,并扩展到下面的所有目录。因此,如果 service-worker.js 位于根目录下,则 Service Worker 将控制来自此网域上所有网页的请求。
预缓存离线网页
首先,我们需要告知 Service Worker 缓存的内容。我们已经创建了一个简单的离线页面 (public/offline.html),只要没有网络连接,我们就会显示该网页。
在 service-worker.js 中,将 '/offline.html', 添加到 FILES_TO_CACHE 数组,最终结果应如下所示:
public/service-worker.js
// CODELAB: Add list of files to cache here.
const FILES_TO_CACHE = [
'/offline.html',
];
接下来,我们需要将以下代码添加到 install 事件,以告知 Service Worker 预缓存离线网页:
public/service-worker.js
// CODELAB: Precache static resources here.
evt.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[ServiceWorker] Pre-caching offline page');
return cache.addAll(FILES_TO_CACHE);
})
);
现在,我们的 install 事件会使用 caches.open() 打开缓存并提供缓存名称。提供缓存名称可让我们对文件进行版本控制,或将数据与缓存的资源分开,以便我们能轻松地更新某个数据而不会影响另一个资源。
缓存打开后,我们便可调用 cache.addAll(),它会获取网址列表,从服务器提取网址并将响应添加到缓存中。请注意,如果任何单个请求失败,cache.addAll() 就会失败。这意味着,您可以保证,如果安装步骤成功,缓存将保持一致的状态。但是,如果它因某种原因而失败,它将在 Service Worker 下次启动时自动重试。
DevTools 绕道
让我们来看看如何使用 DevTools 来了解和调试 Service Worker。在重新加载页面之前,请打开 DevTools,然后转到 Application 面板的 Service Workers 窗格。该属性应如下所示:

如果您看到诸如此类的空白页面,则表示当前打开的页面没有任何已注册的 Service Worker。
现在,请重新加载您的页面。Service Worker 窗格现在应如下所示:

如果您看到此类信息,则表明页面正在运行 Service Worker。
在“状态”标签旁边有一个数字(在本例中为 34251)。与 Service Worker 合作时,请留意该数字。这是判断 Service Worker 是否已更新的一种简单方式。
清理旧的离线网页
我们将使用 activate 事件来清理缓存中的所有旧数据。此代码可确保每当任何 App Shell 文件发生更改时,您的 Service Worker 会更新其缓存。为使此功能正常运行,您需要递增 Service Worker 文件顶部的 CACHE_NAME 变量。
将以下代码添加到您的 activate 事件中:
public/service-worker.js
// CODELAB: Remove previous cached data from disk.
evt.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key !== CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
DevTools 绕道
打开“Service Workers”窗格后,刷新页面。您将看到已安装新 Service Worker,且状态编号已递增。

更新的 Service Worker 会立即获得控制权,因为我们的 install 事件以 self.skipWaiting() 结束,activate 事件以 self.clients.claim() 结束。否则,只要有打开的标签页,旧的 Service Worker 就会继续控制页面。
处理失败的网络请求
最后,我们需要处理 fetch 事件。我们将使用网络回退到缓存策略。Service Worker 会先尝试从网络获取资源。如果失败,Service Worker 会从缓存中返回离线页面。

public/service-worker.js
// CODELAB: Add fetch event handler here.
if (evt.request.mode !== 'navigate') {
// Not a page navigation, bail.
return;
}
evt.respondWith(
fetch(evt.request)
.catch(() => {
return caches.open(CACHE_NAME)
.then((cache) => {
return cache.match('offline.html');
});
})
);
fetch 处理程序只需要处理页面导航,因此其他请求可以从处理程序中转储出来,并由浏览器正常处理。但是,如果 .mode 请求为 navigate,请使用 fetch 尝试从网络中获取该项。如果失败,catch 处理程序会使用 caches.open(CACHE_NAME) 打开缓存,并使用 cache.match('offline.html') 获取预缓存的离线网页。然后,系统会使用 evt.respondWith() 将结果传回浏览器。
DevTools 绕道
让我们检查以确保一切按预期运行。打开 Service Workers 窗格后,刷新页面。您将看到已安装新 Service Worker,且状态编号已递增。
我们还可以查看缓存的内容。转到开发者工具的 Application 面板上的 Cache Storage 窗格。右键点击缓存存储,选择刷新缓存,然后展开该部分。您应该会看到左侧列出的静态缓存的名称。点击缓存名称可查看所有缓存的文件。

现在,让我们来测试离线模式。返回到开发者工具的 Application 面板中的 Service Workers 窗格,然后选中 Offline 复选框。检查完成后,网络面板标签页旁边应该会显示一个黄色警告图标。此图标表示您处于离线状态。

重新加载页面,这样就可以了!我们获得了离线 Panda,而不是 Chrome 的离线恐龙!
关于测试 Service Worker 的提示
调试 Service Worker 并非易事。当涉及缓存时,如果缓存未按预期更新,那么事情会变得更加糟糕。在典型的 Service Worker 生命周期与代码中的 bug 之间,您可能会很快感到沮丧。但不要。
使用 DevTools
在 Application 面板的 Service Workers 窗格中,有几个复选框可让您的生活更轻松。

- 离线 - 选中此选项后,可模拟离线体验,并防止任何请求发送到网络。
- 重新加载时更新 - 选中后,将获取最新的 Service Worker,进行安装并立即激活。
- 网络绕过 - 此选项处于选中状态时,请求会绕过 Service Worker,直接发送至网络。
重新开始
在某些情况下,您可能会发现自己正在加载缓存的数据,或者内容未按预期更新。如需清除所有保存的数据(localStorage、indexedDB 数据、缓存文件)以及移除所有 Service Worker,请使用 Application 面板中的 Clear storage 窗格。您也可以在无痕式窗口中工作。

其他提示:
- 取消注册 Service Worker 后,它可能仍存在于列表中,直到包含它的浏览器窗口关闭为止。
- 如果您的应用有多个窗口处于打开状态,则新的 Service Worker 只有在所有窗口重新加载到最新 Service Worker 后才会生效。
- 取消注册 Service Worker 不会清除缓存!
- 如果 Service Worker 已存在且注册了新的 Service Worker,则除非您立即控制,否则新 Service Worker 将不具备控制权,直到页面重新加载。
使用 Lighthouse 验证更改
再次运行 Lighthouse 并验证您的更改。验证您的更改前,请务必取消选中“离线”复选框!
SEO 审核
- ✅ 通过:文档包含元描述。
渐进式 Web 应用审核
- ✅ 通过:离线时,当前页面返回 200。
- ✅ 通过:离线时,
start_url会返回 200。 - ✅ 通过:注册可控制页面和
start_url.的 Service Worker - ✅ 通过:Web 应用清单符合可安装性要求。
- ✅ 通过:针对自定义启动画面进行配置。
- ✅ 通过:设置地址栏主题颜色。
花点时间将你的手机设为飞行模式,然后尝试运行你喜爱的一些应用。在几乎所有情况下,它们都可以提供相当强大的离线体验。用户期望从应用中获得可靠的体验。而网络也是如此。渐进式 Web 应用应以离线为核心场景进行设计。
Service Worker 生命周期
Service Worker 的生命周期是最复杂的部分。如果您不了解它要做什么以及它有哪些优势,那么您会感觉它让您败下阵来。但一旦您知道它的工作原理,您就可以向用户提供无缝、不会产生干扰的更新,并融合网络和模式的优点。
install 事件
Service Worker 获取的第一个事件是 install。它会在工作器执行后立即触发,并且每个 Service Worker 仅调用一次。如果您更改 Service Worker 脚本,浏览器会将其视为不同的 Service Worker,因此它将获得自己的 install 事件。

通常,install 事件用于缓存应用运行所需的所有内容。
activate 事件
Service Worker 在每次启动时都会收到 activate 事件。activate 事件的主要用途是配置 Service Worker 的行为、清理先前运行中遗留的所有资源(例如旧缓存),并使 Service Worker 已准备好处理网络请求(例如,下述 fetch 事件)。
fetch 事件
获取事件允许 Service Worker 拦截任何网络请求并处理请求。它可以发送到网络以获取资源,可以从自己的缓存中提取资源,生成自定义响应或任意数量的不同选项。请参阅离线实战宝典,了解您可以使用的不同策略。
更新 Service Worker
浏览器会在每次网页加载时检查是否有新版 Service Worker。如果找到新版本,系统会在后台下载并安装该新版本,但不会将其激活。新版 Service Worker 将处于等待状态,直到打开任何使用旧 Service Worker 的页面为止。所有使用旧 Service Worker 的窗口关闭后,新 Service Worker 就会激活并可执行控制。如需了解详情,请参阅 Service Worker 生命周期文档的更新 Service Worker 部分。
选择合适的缓存策略
选择合适的缓存策略,取决于您尝试缓存的资源类型以及日后可能需要访问的方式。对于天气应用,我们将需要缓存的资源分为两类:预缓存的资源和在运行时缓存的数据。
缓存静态资源
预缓存资源这一概念与在用户安装桌面版或移动版应用时发生的情况类似。系统会将运行应用所需的关键资源安装到设备上或缓存在设备上,以便日后是否有网络连接时进行加载。
对于我们的应用,我们会在安装 Service Worker 时预缓存所有静态资源,以便将运行应用所需的所有内容存储在用户设备上。为了确保我们的应用能以闪电般的速度加载,我们会使用缓存优先策略,而不是从网络获取资源,而是从本地缓存中提取资源;只有在不可用的情况下,我们才会尝试从网络获取资源。

从本地缓存中提取数据可消除任何网络变化。无论用户使用的是哪种网络(Wi-Fi、5G、3G,甚至是 2G),我们几乎不需要马上就能运行所需的关键资源。
缓存应用数据
stale-when-revalidate 策略非常适合特定类型的数据,非常适合我们的应用。它会尽快将数据显示在屏幕上,然后在网络返回最新数据后更新。“过时而重新验证”意味着我们需要发起两个异步请求,一个发向缓存,一个发向网络。

在正常情况下,缓存的数据将会立即返回,为应用提供其可以使用的最新数据。然后,在网络请求返回时,将使用来自网络的最新数据更新应用。
对于我们的应用,这可提供比网络更好的体验,回退到缓存策略,因为用户不必等到网络请求超时才能看到屏幕上的内容。它们可能一开始会看到较旧的数据,但在网络请求返回后,应用会更新最新数据。
更新应用逻辑
如前所述,应用需要发起两个异步请求,一个发向缓存,一个发向网络。应用使用 window 中提供的 caches 对象来访问缓存和检索最新数据。这是一个出色的渐进式增强示例,因为并非所有对象都提供 caches 对象,即使没有该对象,网络请求仍应正常工作。
更新 getForecastFromCache() 函数,以检查 caches 对象在全局 window 对象中是否可用,如果有,则从缓存中请求数据。
public/scripts/app.js
// CODELAB: Add code to get weather forecast from the caches object.
if (!('caches' in window)) {
return null;
}
const url = `${window.location.origin}/forecast/${coords}`;
return caches.match(url)
.then((response) => {
if (response) {
return response.json();
}
return null;
})
.catch((err) => {
console.error('Error getting data from cache', err);
return null;
});
然后,我们需要修改 updateData(),使其进行两次调用,一次调用 getForecastFromNetwork() 以从网络获取预测结果,另一次调用 getForecastFromCache() 以获取最新的缓存预测:
public/scripts/app.js
// CODELAB: Add code to call getForecastFromCache.
getForecastFromCache(location.geo)
.then((forecast) => {
renderForecast(card, forecast);
});
我们的天气应用现在会发出两个异步数据请求,一个来自缓存,另一个通过 fetch 发出。如果缓存中存在数据,系统便会非常快速地返回并呈现这些数据(几十毫秒)。然后,当 fetch 响应时,卡片会直接从天气 API 中更新最新的数据。
请注意,缓存请求和 fetch 请求都是以更新预报卡片的调用结尾。应用如何知道它是否显示了最新数据?这是通过 renderForecast() 中的以下代码处理的:
public/scripts/app.js
// If the data on the element is newer, skip the update.
if (lastUpdated >= data.currently.time) {
return;
}
每次更新卡片时,应用都会将时间戳存储在卡片上的隐藏属性中。如果卡片上已存在的时间戳比传递给该函数的数据新,则应用仅释放。
预缓存应用资源
让我们在 Service Worker 中添加一个 DATA_CACHE_NAME,以便将应用的数据与 App Shell 分开。更新 App Shell 并清除较旧缓存时,我们的数据将保持不变,可随时用于实现超快速加载。请注意,如果将来您的数据格式发生变化,您需要一种方法来处理这种情况,并确保 App Shell 和内容保持同步。
public/service-worker.js
// CODELAB: Update cache names any time any of the cached files change.
const CACHE_NAME = 'static-cache-v2';
const DATA_CACHE_NAME = 'data-cache-v1';
别忘了还要更新 CACHE_NAME;我们也会更改所有静态资源。
为了让我们的应用能够离线工作,我们需要预缓存其需要的所有资源。这也会对我们的表现起到帮助作用。应用无需从网络获取所有资源,就能够从本地缓存加载所有资源,从而避免网络不稳定问题。
使用文件列表更新 FILES_TO_CACHE 数组:
public/service-worker.js
// CODELAB: Add list of files to cache here.
const FILES_TO_CACHE = [
'/',
'/index.html',
'/scripts/app.js',
'/scripts/install.js',
'/scripts/luxon-1.11.4.js',
'/styles/inline.css',
'/images/add.svg',
'/images/clear-day.svg',
'/images/clear-night.svg',
'/images/cloudy.svg',
'/images/fog.svg',
'/images/hail.svg',
'/images/install.svg',
'/images/partly-cloudy-day.svg',
'/images/partly-cloudy-night.svg',
'/images/rain.svg',
'/images/refresh.svg',
'/images/sleet.svg',
'/images/snow.svg',
'/images/thunderstorm.svg',
'/images/tornado.svg',
'/images/wind.svg',
];
由于我们需要手动生成要缓存的文件列表,因此每次更新文件时,我们都必须更新 CACHE_NAME。我们现在可以从缓存文件列表中移除 offline.html,因为我们的应用现在具备离线工作所需的所有必要资源,而且不会再显示离线网页。
更新激活事件处理脚本
为确保我们的 activate 事件不会意外删除数据,请在 service-worker.js 的 activate 事件中,将 if (key !== CACHE_NAME) { 替换为:
public/service-worker.js
if (key !== CACHE_NAME && key !== DATA_CACHE_NAME) {
更新提取事件处理脚本
我们需要修改 Service Worker,以拦截发给天气 API 的请求,并将其响应存储在缓存中,以便日后轻松访问。在过时的重新验证策略中,我们期望网络响应是“可信来源”,始终向我们提供最新信息。如果网络无法检索,也没有关系,因为我们已检索到应用中最新的缓存数据。
更新 fetch 事件处理脚本,以便单独处理对 data API 的请求,以及其他请求。
public/service-worker.js
// CODELAB: Add fetch event handler here.
if (evt.request.url.includes('/forecast/')) {
console.log('[Service Worker] Fetch (data)', evt.request.url);
evt.respondWith(
caches.open(DATA_CACHE_NAME).then((cache) => {
return fetch(evt.request)
.then((response) => {
// If the response was good, clone it and store it in the cache.
if (response.status === 200) {
cache.put(evt.request.url, response.clone());
}
return response;
}).catch((err) => {
// Network request failed, try to get it from the cache.
return cache.match(evt.request);
});
}));
return;
}
evt.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(evt.request)
.then((response) => {
return response || fetch(evt.request);
});
})
);
该代码会拦截请求,并检查它是否适用于天气预报。如果是,请使用 fetch 发出请求。返回响应后,打开缓存,克隆响应,将其存储在缓存中,然后将响应返回给原始请求者。
我们需要移除 evt.request.mode !== 'navigate' 检查,因为我们希望 Service Worker 处理所有请求(包括图片、脚本、CSS 文件等),而不仅仅是处理导航。如果我们将该签入保留,将仅从 Service Worker 缓存提供 HTML。其他所有请求均由广告联盟请求。
试试看
现在,该应用应该能够完全离线运行。请刷新页面,确保已安装最新的 Service Worker。然后保存几座城市,按下应用程序上的刷新按钮即可获取最新的天气数据。
接下来,转到开发者工具的 Application 面板上的 Cache Storage 窗格。展开此部分,您应该会看到左侧列出的静态缓存和数据缓存的名称。打开数据缓存应显示每个城市存储的数据。

切换到 Service Workers 窗格,然后选中 Offline 复选框。请尝试重新加载页面,然后离线并重新加载页面。
如果您的网络速度较快,并且想查看在天气较慢时如何更新天气预报数据,请将 server.js 中的 FORECAST_DELAY 属性设置为 5000。向预测 API 发出的所有请求都会延迟 5000 毫秒。
使用 Lighthouse 验证更改
再次运行 Lighthouse 也是一个好办法。
SEO 审核
- ✅ 通过:文档包含元描述。
渐进式 Web 应用审核
- ✅ 通过:离线时,当前页面返回 200。
- ✅ 通过:离线时,
start_url会返回 200。 - ✅ 通过:注册可控制页面和
start_url.的 Service Worker - ✅ 通过:Web 应用清单符合可安装性要求。
- ✅ 通过:针对自定义启动画面进行配置。
- ✅ 通过:设置地址栏主题颜色。
渐进式 Web 应用安装后,其外观和行为与所有其他已安装的应用类似。它会从其他应用启动的位置启动。它在没有地址栏或其他浏览器界面的应用中运行。与所有其他已安装的应用一样,它也是任务切换器中的顶级应用。

在 Chrome 中,您可以通过三点状上下文菜单安装渐进式 Web 应用,也可以向用户提供按钮或其他界面组件,以提示用户安装您的应用。
使用 Lighthouse 进行审核
为了让用户能够安装您的渐进式 Web 应用,该应用需要符合特定条件。最简单的检查方法是使用 Lighthouse 并确保它符合安装标准。

如果您已完成此 Codelab,您的 PWA 应该已经符合这些条件。
将 index.js 添加到 index.html
首先,我们来将 install.js 添加到 index.html 文件中。
public/index.html
<!-- CODELAB: Add the install script here -->
<script src="/scripts/install.js"></script>
监听 beforeinstallprompt 事件
如果满足“添加到主屏幕”条件,Chrome 就会触发 beforeinstallprompt 事件,指明您的应用可以安装,然后提示用户进行安装。添加以下代码以监听 beforeinstallprompt 事件:
public/scripts/install.js
// CODELAB: Add event listener for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', saveBeforeInstallPromptEvent);
保存事件并显示安装按钮
在 saveBeforeInstallPromptEvent 函数中,我们将保存对 beforeinstallprompt 事件的引用,以便稍后对其调用 prompt() 并更新界面以显示安装按钮。
public/scripts/install.js
// CODELAB: Add code to save event & show the install button.
deferredInstallPrompt = evt;
installButton.removeAttribute('hidden');
显示提示并隐藏该按钮
当用户点击安装按钮时,我们需要对已保存的 beforeinstallprompt 事件调用 .prompt()。我们还需要隐藏安装按钮,因为在每个已保存的事件上只能调用一次 .prompt()。
public/scripts/install.js
// CODELAB: Add code show install prompt & hide the install button.
deferredInstallPrompt.prompt();
// Hide the install button, it can't be called twice.
evt.srcElement.setAttribute('hidden', true);
调用 .prompt() 时,系统将向用户显示一个模态对话框,要求用户将应用添加到主屏幕。
记录结果
您可以通过监听已保存 beforeinstallprompt 事件的 userChoice 属性返回的 promise,了解用户如何响应安装对话框。在提示显示且用户响应后,promise 会返回具有 outcome 属性的对象。
public/scripts/install.js
// CODELAB: Log user response to prompt.
deferredInstallPrompt.userChoice
.then((choice) => {
if (choice.outcome === 'accepted') {
console.log('User accepted the A2HS prompt', choice);
} else {
console.log('User dismissed the A2HS prompt', choice);
}
deferredInstallPrompt = null;
});
关于 userChoice 的一条注释:规范将其定义为属性,而不是您预期的函数。
记录所有安装事件
除了您添加的任何用于安装应用的界面外,用户还可以通过其他方法安装您的 PWA,例如 Chrome 的三点状菜单。如需跟踪这些事件,请监听 appinstalled 事件。
public/scripts/install.js
// CODELAB: Add event listener for appinstalled event
window.addEventListener('appinstalled', logAppInstalled);
然后,我们需要更新 logAppInstalled 函数。在此 Codelab 中,我们只使用 console.log,但在生产应用中,您可能需要使用分析软件将此事件记录为事件。
public/scripts/install.js
// CODELAB: Add code to log the event
console.log('Weather App was installed.', evt);
更新 Service Worker
不要忘记更新 service-worker.js 文件中的 CACHE_NAME,因为您已经对已缓存的文件进行了更改。在开发者工具的 Application 面板的 Service Workers 窗格中,启用 Bypass for network 复选框可用于开发,但在现实环境中并不会有任何帮助。
试试看
我们来看看安装步骤如何顺利进行。为安全起见,请使用 DevTools 的 Application 面板中的 Clear site data 按钮清除所有内容并确保我们从头开始。如果您之前已安装该应用,请务必将其卸载,否则安装图标不会再次显示。
验证是否显示安装按钮
首先,请验证我们的安装图标能否正常显示。请务必在桌面设备和移动设备上尝试此操作。
- 在新的 Chrome 标签页中打开网址。
- 打开 Chrome 的三点状菜单(在地址栏旁边)。
▢ 验证您是否在菜单中看到“Install 天气...”。 - 使用右上角的刷新按钮刷新天气数据,确保符合用户互动启发法。
▢ 验证应用图标中会显示安装图标。
验证安装按钮是否正常工作
接下来,让我们确保所有内容均已正确安装,并且我们的事件已正确触发。您可以在桌面设备或移动设备上执行此操作。如果您想在移动设备上对此进行测试,请确保您使用的是远程调试,以便查看控制台中记录的内容。
- 打开 Chrome,然后在新的浏览器标签页中导航至 天气 PWA。
- 打开 DevTools 并切换到 Console 面板。
- 点击右上角的“安装”按钮。
▢ 验证“安装”按钮消失
▢ 验证是否显示安装模式对话框。 - 点击“取消”。
▢ 在控制台输出结果中显示用户已关闭 A2HS 提示。
▢ 验证“安装”按钮是否会重新显示。 - 再次点击安装按钮,然后点击模态对话框中的安装按钮。
▢ 验证用户已接受 A2HS 提示”显示在控制台输出中。
▢ 验证“天气应用已安装”显示在控制台输出中。
▢ 验证天气应用是否已添加到您通常查找应用的位置。 - 启动 天气 PWA。
▢ 验证此应用是否会作为独立的应用打开(在桌面设备上的应用窗口中打开,或在移动设备上全屏打开)。
而被移除。
验证 iOS 安装是否正常运行
下面我们来看 iOS 上的行为。如果您有 iOS 设备,则可以使用该设备;如果您使用的是 Mac 设备,可以试用 Xcode 中提供的 iOS 模拟器。
- 打开 Safari,然后在新的浏览器标签页中导航至您的 天气 PWA。
- 点击共享
按钮。 - 向右滚动,然后点击 Add to Home Screen 按钮。
▢ 验证标题、网址和图标是否正确。 - 点击添加
。“验证”应用图标已添加到主屏幕。 - 从主屏幕启动 天气 PWA。
▢ 验证该应用是否以全屏模式启动。
奖励:检测您的应用是否从主屏幕启动
通过 display-mode 媒体查询,可以根据应用的启动方式应用样式,或使用 JavaScript 确定应用的启动方式。
@media all and (display-mode: standalone) {
body {
background-color: yellow;
}
}
您还可以在 JavaScript 中查看 display-mode 媒体查询,看看您是否独立运行。
奖励:卸载 PWA
请注意,如果应用已安装,beforeinstallevent 不会触发,因此在开发过程中,您可能需要多次安装和卸载应用,以确保一切正常。
Android
在 Android 上,PWA 的卸载方式与其他卸载应用相同。
- 打开应用抽屉。
- 向下滚动,找到“天气”图标。
- 将应用图标拖动到屏幕顶部。
- 选择卸载。
Chrome 操作系统
在 Chrome 操作系统上,您可以轻松地从启动器搜索框中卸载 PWA。
- 打开启动器。
- 在搜索框中输入“天气”,结果中就会显示您的 天气 PWA。
- 右键点击(点击鼠标左键)天气 PWA。
- 点击从 Chrome 中移除...
macOS 和 Windows
在 Mac 和 Windows 上,可以通过 Chrome 卸载 PWA:
- 在新的浏览器标签页中,打开 chrome://apps。
- 右键点击(点击鼠标左键)天气 PWA。
- 点击从 Chrome 中移除...
您也可以打开已安装的 PWA,点击右上角的三点状上下文菜单,然后选择“Uninstall 天气 PWA...”
恭喜您,您已成功构建您的第一个 Progressive Web App!
您添加了一个 Web 应用清单,使其能够安装;此外,您还添加了一个 Service Worker,以确保您的 PWA 始终快速可靠。您已了解如何使用 DevTools 审核应用,以及它如何帮助您改善用户体验。
现在,您已了解将任何 Web 应用转换成渐进式 Web 应用所需的关键步骤。
后续操作
查看下列 Codelab…