渐进式 Web 应用程序初学者指南

已发表: 2022-03-10
快速总结 ↬ PWA 利用最新技术来结合最好的 Web 和移动应用程序。 本文探讨了浏览器的最新进展以及我们作为开发人员必须构建新一代 Web 应用程序的机会。

渐进式网络应用程序可能是移动网络的下一件大事。 最初由谷歌在 2015 年提出,由于相对容易开发和应用程序的用户体验几乎立竿见影,它们已经引起了很多关注。

关于 SmashingMag 的进一步阅读

  • 渐进式 Web 应用程序的构建块
  • 对话式设计要点:构建聊天机器人的技巧
  • 构建利用您的网站的一流应用程序
  • 在 Foundation For Apps 中创建一个完整的 Web 应用程序

渐进式 Web 应用程序利用最新技术将 Web 和移动应用程序的优点结合起来。 把它想象成一个使用网络技术构建的网站,但它的行为和感觉就像一个应用程序。 浏览器和服务工作者可用性以及缓存和推送 API 的最新进展使 Web 开发人员能够允许用户将 Web 应用程序安装到他们的主屏幕、接收推送通知甚至离线工作。

与各自应用商店中的本地应用程序相比,渐进式网络应用程序利用了更大的网络生态系统、插件和社区,以及部署和维护网站的相对容易性。 对于那些同时在移动设备和 Web 上进行开发的人来说,您会意识到可以在更短的时间内构建网站,并且 API 不需要保持向后兼容(所有用户都将运行相同版本的网站)代码,不像原生应用程序的版本碎片),并且应用程序通常更容易部署和维护

跳跃后更多! 继续往下看↓

为什么选择渐进式 Web 应用程序?

一项研究表明,从用户第一次接触应用到用户开始使用应用,每走一步,应用就会失去 20% 的用户。 用户必须首先在应用商店中找到该应用,下载并安装它,然后最后打开它。 当用户找到您的渐进式 Web 应用程序时,他们将能够立即开始使用它,从而消除不必要的下载和安装阶段。 并且当用户返回应用程序时,会提示他们安装应用程序并升级到全屏体验。

但是,原生应用程序绝对不是坏事。 带有推送通知的移动应用程序的留存率是没有推送的移动应用程序的三倍,用户重新打开移动应用程序的可能性是网站的三倍。 此外,设计良好的移动应用程序消耗的数据更少,速度更快,因为一些资源驻留在设备上。

渐进式 Web 应用程序利用了移动应用程序的特性,从而提高了用户保留率和性能,而无需维护移动应用程序所涉及的复杂性。

用例

什么时候应该构建渐进式 Web 应用程序? 对于您希望用户经常返回的应用程序,通常建议使用 Native,渐进式 Web 应用程序也不例外。 Flipkart 为其流行的电子商务平台 Flipkart Lite 使用渐进式网络应用程序,而 SBB 在其在线签到过程中使用渐进式网络应用程序,允许用户在没有互联网连接的情况下访问他们的门票。

在评估您的下一个应用程序应该是渐进式 Web 应用程序、网站还是原生移动应用程序时,首先确定您的用户和最重要的用户操作。 作为“渐进式”,渐进式 Web 应用程序适用于所有浏览器,并且每当用户的浏览器使用新的和改进的功能和 API 进行更新时,体验都会得到增强。

因此,与传统网站相比,渐进式 Web 应用程序的用户体验没有任何妥协; 但是,您可能必须决定离线支持哪些功能,并且您必须方便导航(请记住,在独立模式下,用户无法访问后退按钮)。 如果您的网站已经有类似应用程序的界面,那么应用渐进式 Web 应用程序的概念只会让它变得更好

如果关键用户操作需要某些功能,但由于缺乏跨浏览器支持而尚不可用,则本机移动应用程序可能是更好的选择,可确保所有用户获得相同的体验。

渐进式 Web 应用程序的特征

在我们进入代码之前,重要的是要了解渐进式 Web 应用程序具有以下特征:

  • 渐进式。 根据定义,渐进式 Web 应用程序必须在任何设备上运行并逐步增强,利用用户设备和浏览器上可用的任何功能。
  • 可发现的。 因为渐进式网络应用程序是一个网站,所以它应该可以在搜索引擎中发现。 这是与原生应用程序相比的主要优势,原生应用程序在可搜索性方面仍然落后于网站。
  • 可链接。 作为从网站继承的另一个特征,一个设计良好的网站应该使用 URI 来指示应用程序的当前状态。 这将使 Web 应用程序能够在用户添加书签或共享应用程序的 URL 时保留或重新加载其状态。
  • 反应灵敏。 渐进式 Web 应用程序的 UI 必须适合设备的外形尺寸和屏幕尺寸。
  • 类似应用程序的. 渐进式 Web 应用程序应该看起来像原生应用程序,并且构建在应用程序外壳模型上,并且页面刷新次数最少。
  • 与连接无关。 它应该在连接性低或离线(我们最喜欢的特性)的区域工作。
  • 可重新接合。 移动应用程序用户更有可能重用他们的应用程序,而渐进式 Web 应用程序旨在通过推送通知等功能实现相同的目标。
  • 可安装。 可以在设备的主屏幕上安装渐进式 Web 应用程序,使其随时可用。
  • 新鲜。 当新内容发布并且用户连接到 Internet 时,该内容应该在应用程序中可用。
  • 安全。 因为渐进式 Web 应用程序具有更亲密的用户体验,并且所有网络请求都可以通过服务工作人员拦截,所以必须通过 HTTPS 托管应用程序以防止中间人攻击。

让我们编码吧!

我们的第一个渐进式网络应用 Sky High 将模拟机场的到达时间表。 当用户第一次访问我们的 Web 应用程序时,我们希望向他们显示从 API 检索到的即将到来的航班列表。 如果用户没有 Internet 连接并且他们重​​新加载了 Web 应用程序,我们希望向他们显示他们上次通过连接下载它时的航班时刻表。

天高截图
Sky High,我们虚构的渐进式网络应用程序(大预览)

基础

渐进式 Web 应用程序的第一个特征是它必须在所有设备上运行,并且必须在允许它的设备和浏览器上进行增强。 因此,我们使用传统的 HTML5 和模拟从模拟 API 检索数据的 JavaScript 构建了我们的网站。 在整个应用程序中,我们使用少量的 Knockout 来处理我们的 Model-View-ViewModel (MVVM) 绑定——一个轻量级的 JavaScript 框架,允许我们将 JavaScript 模型绑定到我们的 HTML 视图。 我们选择使用 Knockout 是因为它比较容易理解并且不会使代码混乱; 但是,您可以将其替换为任何其他框架,例如 React 或 AngularJS。

我们的网站遵循 Google 的材料设计指南,这是一组指导设计和交互的原则。 材料设计不仅作为跨应用程序和设备的统一标准,而且赋予设计意义。 我们为 Sky High 的到达视图使用材料设计,为我们的渐进式 Web 应用程序提供原生应用程序的外观和感觉。

最后,我们测试了我们的应用程序以确保它没有卡顿并且滚动流畅。 无 Jank 渲染已被证明可以提高用户参与度。 目标是每秒渲染 60 帧。

对于这个演示,我们将检索一个静态 JSON 文件,而不是一个真正的 API。 这只是为了让事情变得简单。 在现实世界中,您将查询 API 或使用 WebSockets。

索引.html

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sky-High Airport Arrivals</title> <link async rel="stylesheet" href="./css/style.css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,600,300italic,600italic" rel="stylesheet" type="text/css"> </head> <body> <header> <div class="content"> <h3>Arrivals</h3> </div> </header> <div class="container"> <div class="content"> <ul class="arrivals-list" data-bind="foreach: arrivals"> <li class="item"> <span class="title" data-bind="html: title"></span> <span class="status" data-bind="html: status"></span> <span class="time" data-bind="html: time"></span> </li> </ul> </div> </div> <script src="./js/build/vendor.min.js"></script> <script src="./js/build/script.min.js"></script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sky-High Airport Arrivals</title> <link async rel="stylesheet" href="./css/style.css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,600,300italic,600italic" rel="stylesheet" type="text/css"> </head> <body> <header> <div class="content"> <h3>Arrivals</h3> </div> </header> <div class="container"> <div class="content"> <ul class="arrivals-list" data-bind="foreach: arrivals"> <li class="item"> <span class="title" data-bind="html: title"></span> <span class="status" data-bind="html: status"></span> <span class="time" data-bind="html: time"></span> </li> </ul> </div> </div> <script src="./js/build/vendor.min.js"></script> <script src="./js/build/script.min.js"></script> </body> </html>

index.html文件是比较标准的。 我们创建了一个 HTML 列表,并使用 Knockout 通过属性data-bind=“foreach: arrivals”我们的视图模型属性arrivals绑定到它。 视图模型arrivals在下面的page.js文件中声明,并在Page模块中公开。 在我们的 HTML 页面上,对于arrivals数组中的每个项目,我们已将titlestatustime属性绑定到 HTML 视图。

page.js

 (var Page = (function() { // declare the view model used within the page function ViewModel() { var self = this; self.arrivals = ko.observableArray([]); } // expose the view model through the Page module return { vm: new ViewModel(), hideOfflineWarning: function() { // enable the live data document.querySelector(".arrivals-list").classList.remove('loading') // remove the offline message document.getElementById("offline").remove(); // load the live data }, showOfflineWarning: function() { // disable the live data document.querySelector(".arrivals-list").classList.add('loading') // load html template informing the user they are offline var request = new XMLHttpRequest(); request.open('GET', './offline.html', true); request.onload = function() { if (request.status === 200) { // success // create offline element with HTML loaded from offline.html template var offlineMessageElement = document.createElement("div"); offlineMessageElement.setAttribute("id", "offline"); offlineMessageElement.innerHTML = request.responseText; document.getElementById("main").appendChild(offlineMessageElement); } else { // error retrieving file console.warn('Error retrieving offline.html'); } }; request.onerror = function() { // network errors console.error('Connection error'); }; request.send(); } } })();

这个page.js文件公开了Page模块,其中包含我们的 ViewModel vm和两个函数hideOfflineWarningshowOfflineWarning 。 View Model ViewModel是一个简单的 JavaScript 文字,将在整个应用程序中使用。 arrivals上的属性是 Knockout 的observableArray ,它自动将我们的 HTML 绑定到 JavaScript 数组,允许我们在 JavaScript 中将项目推送和弹出到我们的数组中,并自动更新页面的 HTML。

函数hideOfflineWarningshowOfflineWarning使我们的应用程序的其余部分能够调用这些函数来更新显示我们是否在线连接的页面 UI。 showOfflineWarning向我们的arrivals arrivals-list HTML 元素添加了一个loading类以淡化列表,然后它通过XHR 检索HTML 文件offline.html 。 假设文件已成功检索( response.status === 200 ),我们将其附加到我们的 HTML 中。 当然,如果我们不使用 service worker 并且用户没有连接到互联网,那么就无法检索到offline.html ,因此用户会看到浏览器的离线页面。

我们从 API 检索数据并将其绑定到我们的视图模型和视图的业务逻辑可以在arrivals.js中找到,并且是使用 Knockout 的标准 MVVM 功能。 在arrivals.js文件中,我们简单地初始化我们将在整个应用程序中使用的服务和视图模型,并公开一个函数Arrivals.loadData() ——检索数据并将其绑定到视图模型。

网络应用清单

让我们的网络应用程序更像应用程序。 Web 应用清单文件是遵循 W3C 规范的简单 JSON 文件。 有了它,可以将 Web 应用程序作为独立应用程序以全屏模式运行,分配在设备上安装应用程序时将显示的图标,并为应用程序分配主题和背景颜色。 此外,Android 版 Chrome 将通过网络应用安装横幅主动建议用户安装网络应用。 要显示安装提示,您的 Web 应用程序需要:

  • 有一个有效的网络应用清单文件,
  • 通过 HTTPS 提供服务,
  • 注册了有效的服务人员,
  • 已访问过两次,每次访问间隔至少五分钟。

Web 应用程序安装横幅
Web 应用安装横幅(查看大图)

清单.json

 { "short_name": "Arrivals", "name": "Arrivals at Sky High", "description": "Progressive web application demonstration", "icons": [ { "src": "launcher-icon.png", "sizes": "48x48", "type": "image/png" }, { "src": "launcher-icon-96.png", "sizes": "96x96", "type": "image/png" }, { "src": "launcher-icon-144.png", "sizes": "144x144", "type": "image/png" }, { "src": "launcher-icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "launcher-icon-256.png", "sizes": "256x256", "type": "image/png" } ], "start_url": "./?utm_source=web_app_manifest", "display": "standalone", "orientation": "portrait", "theme_color": "#29BDBB", "background_color": "#29BDBB" }

让我们分解这个清单文件:

  • short_name是应用程序的人类可读名称。 在 Android 版 Chrome 中,这也是伴随主屏幕图标的名称。
  • name也是应用程序的可读名称,并定义应用程序的列出方式。
  • description提供 Web 应用程序的一般描述。
  • icons定义了一系列不同大小的图像,这些图像将用作应用程序的图标集。 在 Android 版 Chrome 中,该图标将用于初始屏幕、主屏幕和任务切换器。
  • start_url是应用程序的起始 URL。
  • display定义了 web 应用程序的默认显示模式: fullscreenstandaloneminimal-uibrowser
  • orientation定义 Web 应用程序的默认方向: portraitlandscape
  • theme_color是应用程序的默认主题颜色。 在 Android 上,这也用于为状态栏着色。
  • background_color定义 Web 应用程序的背景颜色。 在 Chrome 中,它还定义了启动画面的背景颜色。
  • related_applications在我们的示例中未实现,但用于指定各种应用商店中的本地应用程序替代方案。

manifest.json引用添加到index.html文件的head标签:

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

用户将 Web 应用程序添加到他们的主屏幕后,他们将能够立即从他们的设备重新使用您的应用程序,而无需直接打开浏览器。 您可以看到这不仅仅是一个网络书签。

从 Vimeo 上的 Smashing Magazine 添加到 Android 版 Chrome 上的主屏幕。

在 Android 版 Chrome 中添加到主屏幕

服务人员

渐进式网络应用程序更令人兴奋的方面之一是它们可以离线工作。 使用服务工作者,可以显示在应用程序的先前会话中检索到的数据(使用 IndexedDB),或者,显示应用程序外壳并通知用户他们没有连接到互联网(我们已经在这个演示中拍摄)。 一旦用户重新连接,我们就可以从服务器检索最新数据。

所有这一切都可以通过服务工作者实现,服务工作者是事件驱动的脚本(用 JavaScript 编写),可以访问域范围的事件,包括网络获取。 有了它们,我们可以缓存所有静态资源,这可以大大减少网络请求并显着提高性能。

应用程序外壳

应用程序外壳是驱动用户界面所需的最低 HTML、CSS 和 JavaScript。 原生移动应用程序包括应用程序外壳作为其可分发的一部分,而网站通常通过网络请求它。 渐进式 Web 应用程序通过将应用程序外壳的资源和资产放在浏览器的缓存中来弥补这一差距。 在我们的 Sky High 应用程序中,我们可以看到我们的应用程序外壳由顶部标题栏、字体和优雅呈现这些所需的任何 CSS 组成。

要开始使用 Service Worker,我们首先需要创建我们的 Service Worker 的 JavaScript 文件sw.js ,放在根目录中。

sw.js

 // Use a cacheName for cache versioning var cacheName = 'v1:static'; // During the installation phase, you'll usually want to cache static assets. self.addEventListener('install', function(e) { // Once the service worker is installed, go ahead and fetch the resources to make this work offline. e.waitUntil( caches.open(cacheName).then(function(cache) { return cache.addAll([ './', './css/style.css', './js/build/script.min.js', './js/build/vendor.min.js', './css/fonts/roboto.woff', './offline.html' ]).then(function() { self.skipWaiting(); }); }) ); }); // when the browser fetches a URL… self.addEventListener('fetch', function(event) { // … either respond with the cached object or go ahead and fetch the actual URL event.respondWith( caches.match(event.request).then(function(response) { if (response) { // retrieve from cache return response; } // fetch as normal return fetch(event.request); }) ); });

让我们更仔细地看看我们的服务工作者。 首先,我们设置一个cacheName变量。 这用于确定是否对我们的缓存资产进行了任何更改。 对于本例,我们将使用静态名称,这意味着我们的资产不会更改或需要更新。

 self.addEventListener('install', function(e) { // declare which assets to cache }

install事件在 Service Worker 的安装阶段触发,如果 Service Worker 已经安装,则只会触发一次。 因此,刷新页面不会再次触发安装阶段。 在安装阶段,我们可以声明将缓存哪些资产。 在上面的示例中,我们缓存了一个 CSS 文件、两个 JavaScript 文件、我们的字体文件、我们的离线 HTML 模板,当然还有应用程序根目录。 self.skipWaiting()强制等待的服务工作者变得活跃。

到目前为止,我们已经声明了我们的 service worker,但是在我们看到它生效之前,我们需要在我们的 JavaScript 中引用它。 在我们的应用程序中,我们在main.js中注册它

// Register the service worker if available. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then(function(reg) { console.log('Successfully registered service worker', reg); }).catch(function(err) { console.warn('Error whilst registering service worker', err); }); } window.addEventListener('online', function(e) { // Resync data with server. console.log("You are online"); Page.hideOfflineWarning(); Arrivals.loadData(); }, false); window.addEventListener('offline', function(e) { // Queue up events for server. console.log("You are offline"); Page.showOfflineWarning(); }, false); // Check if the user is connected. if (navigator.onLine) { Arrivals.loadData(); } else { // Show offline message Page.showOfflineWarning(); } // Set Knockout view model bindings. ko.applyBindings(Page.vm);

我们还包括了两个事件监听器来检查会话的状态是否从online变为offline ,反之亦然。 然后,事件处理程序调用不同的函数以通过Arrivals.loadData()检索数据,并分别通过Page.showOfflineWarningPage.hideOfflineWarning启用或禁用离线消息。 我们的应用程序还使用 navigator.onLine 检查用户当前是否在线,并相应地检索数据或显示离线警告。 在main.js的最后一行,我们将 Knockout 绑定应用到 View Model Page.vm

如果我们第一次加载我们的应用程序(使用 Chrome 开发者工具),我们将看不到任何新东西。 但是,在重新加载时,我们会看到已经从 service worker 中检索了一些网络资源。 这是我们的应用程序外壳。

应用程序外壳
应用外壳网络资源,在 Chrome 开发者工具中(查看大图)

离线测试

在没有 Internet 连接的情况下运行应用程序的用户(假设他们已经在页面上)只会导致应用程序外壳和离线警告被显示——这是对 Chrome 的潜行 t-rex 的改进。 一旦用户建立了网络连接,我们就会禁用警告并检索最新数据。

优雅地失败
呈现自定义 HTML 页面而不是 Chrome 的默认页面(查看大图)

当离线用户访问其网站时,卫报采取了一种特别有趣的方法,提供了一个填字游戏:

卫报的离线填字游戏
卫报的离线填字游戏(查看大图)

推送通知

推送通知允许用户选择及时更新他们信任的应用程序,帮助他们重新使用这些应用程序。 即使浏览器关闭,网络上的推送通知也能让您与观众互动。

推送通知
在 Emojoy 上推送通知(查看大图)

Push API 在 Chrome、Opera 和三星的浏览器中受支持,并且正在 Firefox 和 Microsoft Edge 中开发。 不幸的是,没有迹象表明该功能将在 Safari 中实现。

表现

服务工作者最简单的胜利之一是我们可以毫不费力地提高性能。 在实施服务工作者之前,在我们在页面加载时检索超过 200 KB 之前,将我们的网站与自身进行比较; 现在减少到 13 KB。 在常规 3G 网络上,页面加载需要 3.5 秒; 现在需要 500 毫秒。

这些性能改进是巨大的,因为应用程序本身非常小并且功能有限。 然而,通过正确使用缓存,可以显着提高性能和感知性能,尤其是对于连接性较低的地方的用户。

灯塔

谷歌的 Chrome 团队已经整合了一个用于测试渐进式网络应用程序的工具。 Lighthouse 在 Node.js 中运行或作为 Chrome 插件运行,也可以在 GitHub 上找到。

要运行 Lighthouse 测试,您的网站需要在线可用,这意味着您无法在localhost上进行测试。

首先,下载 npm 包:

 npm install -g GoogleChrome/lighthouse

安装后,运行 Chrome(52 及以上版本):

 npm explore -g lighthouse -- npm run chrome lighthouse https://incredibleweb.github.io/pwa-tutorial/

Lighthouse 运行的输出将在命令行中可见,并将根据您已实现的渐进式 Web 应用程序功能和属性对您的网站进行评分 - 例如,您是否使用manifest.json文件或您的页面是否可以离线使用.

结论

本文只是渐进式 Web 应用程序的开胃菜。 我们可以做更多的事情来创建用户正在寻找的类似应用程序的体验,无论是通过使用 Push API 支持推送通知,使应用程序可重新参与,还是使用 IndexedDB 和后台同步来改善离线体验。

跨浏览器支持

对于渐进式 Web 应用程序而言,这些仍处于早期阶段,跨浏览器支持仍然有限,尤其是在 Safari 和 Edge 中。 但是,Microsoft 公开支持渐进式 Web 应用程序,并且应该在今年年底前实现更多功能。

  • 服务工作者和缓存 API 。 在 Chrome、Firefox、Opera 和三星的浏览器中支持。 正在 Microsoft Edge 中开发,预计将于 2016 年底推出。正在考虑用于 Safari。
  • 添加到主屏幕。 支持 Chrome、Firefox、Opera、Android 浏览器和三星的浏览器。 微软似乎表示渐进式网络应用程序将作为商店列表提供。 目前还没有 Safari 的计划。
  • 推送 API 。 主要支持 Chrome、Firefox、Opera 和三星的浏览器。 在 Microsoft Edge 中进行开发。 目前还没有 Safari 的计划。

如果更多开发人员利用渐进式 Web 应用程序提供的功能(这些功能相对容易实现并提供即时奖励),那么用户将更喜欢在受支持的浏览器中使用这些 Web 应用程序,希望能说服其他浏览器供应商适应。

源代码

本教程的完整源代码可在 Github 存储库中获得,演示可在 GitHub Pages 上获得。