如何优化渐进式 Web 应用程序:超越基础
已发表: 2022-03-10渐进式 Web 应用程序 (PWA) 在 2020 年仍然越来越受欢迎。考虑到更高的转化率、客户参与度、页面加载速度降低以及开发和管理成本降低等好处,这并不令人意外。
我们可以看到受人尊敬的公司也通过他们的 PWA 获得成功,例如 Twitter、Uber、Tinder、Pinterest 和 Forbes。 他们都吹嘘实施渐进式应用程序带来的巨大好处。
好消息是,开发 PWA 并不是只有大预算公司才能负担得起的。 这些应用程序平等地服务于小型和普通企业,并且创建起来并不复杂。
您可以在 Smashing Magazine 上找到全面的渐进式 Web 应用初学者指南,该指南侧重于构建 PWA 的核心。
但是,让我们更进一步,学习如何将现代品质部署到 PWA 中,例如离线功能、基于网络的优化、跨设备用户体验、SEO 功能以及非侵入式通知和请求。 您还将找到示例代码或对更具体指南的引用,以便您可以在 PWA 中实施这些技巧。
渐进式 Web 应用程序 (PWA) 快速概览
让我们不要跳过基础知识,快速了解 PWA 的核心。
什么是 PWA?
“Progressive Web Apps (PWA) 使用现代 API 构建和增强,以提供增强的功能、可靠性和可安装性,同时通过单一代码库在任何设备上覆盖任何人、任何地方。”
——谷歌的开发者
换句话说,PWA 是用户可以用作独立应用程序的网站。 它们与原生应用的不同主要是因为 PWA 不需要安装并且可以与各种设备一起使用——原生应用主要是为移动设备构建的。
PWA 是如何工作的?
PWA 的核心由三个组件组成:Web 应用程序清单、服务工作者和应用程序外壳。 您可以在上面提到的初学者指南中找到构建这些的详细说明。
以下是这些组件的作用。
网络应用清单
Web 应用程序清单是使网站以全屏模式作为独立应用程序运行的核心。 您可以定义 PWA 的外观,针对不同的设备对其进行优化,并分配一个在应用程序安装后显示的图标。
服务人员
服务工作者通过获取缓存数据或通知用户没有 Internet 连接来启用 PWA 的离线使用。 一旦服务器连接恢复,服务工作者也会检索最新数据。
应用程序外壳架构
应用程序外壳是用户在访问 PWA 时看到的内容。 它是支持用户界面所需的最低 HTML、CSS 和 JavaScript。 在开发 PWA 时,您可以在浏览器中缓存应用程序外壳的资源和资产。
将现代特征部署到您的 PWA
除了核心功能之外,现代 PWA 还包含其他特征,这些特征进一步推动其用户获得更非凡的用户体验。
让我们看看 PWA 的一些特定现代特征,并了解如何将这些添加到您的 PWA 中。 以下品质被 Google 开发人员认为是对基本 PWA 的重要补充。
该应用程序可以像在线一样离线工作
在构建 PWA 时,您还可以开发自定义离线页面作为核心的一部分。 但是,如果您的 PWA 即使在没有 Internet 连接的情况下也能继续运行(直到需要连接的某个点),它对用户来说会更加友好。 否则,用户体验可能会像 Ankita Masand 在她关于 PWA 的痛点的文章中所描述的那样,在订购蛋糕时所遭受的折磨一样令人沮丧。
您可以通过使用缓存内容、后台同步和骨架屏幕来获得更重要的用户体验。 让我们来看看每一个。
使用IndexedDB
缓存内容
IndexedDB
是一个浏览器内的 NoSQL 存储系统,您可以使用它来缓存和检索所需的数据,以使您的 PWA 脱机工作。
但是,并非所有浏览器都支持IndexedDB
,因此您要做的第一件事是检查用户的浏览器是否支持它。
if (!('indexedDB' in window)) { console.log('This browser doesn\'t support IndexedDB'); return; }
在此之后,您可以使用 IndexedDB API 创建缓存内容。 下面是一个来自 Google 开发人员的示例,它打开一个数据库、添加一个对象存储以及向该存储中添加一个项目。
var db; var openRequest = indexedDB.open('test_db', 1); openRequest.onupgradeneeded = function(e) { var db = e.target.result; console.log('running onupgradeneeded'); if (!db.objectStoreNames.contains('store')) { var storeOS = db.createObjectStore('store', {keyPath: 'name'}); } }; openRequest.onsuccess = function(e) { console.log('running onsuccess'); db = e.target.result; addItem(); }; openRequest.onerror = function(e) { console.log('onerror!'); console.dir(e); }; function addItem() { var transaction = db.transaction(['store'], 'readwrite'); var store = transaction.objectStore('store'); var item = { name: 'banana', price: '$2.99', description: 'It is a purple banana!', created: new Date().getTime() }; var request = store.add(item); request.onerror = function(e) { console.log('Error', e.target.error.name); }; request.onsuccess = function(e) { console.log('Woot! Did it'); }; }
后台同步
如果您的 PWA 在后台同步数据,则用户可以在离线时执行操作,然后在 Internet 连接恢复时执行这些操作。 一个简单的例子是一个消息应用程序。 用户可以在离线时发送消息,而无需等待消息发送——后台同步会在连接恢复时自动发送消息。
这是 Jake Archibald 如何开发后台同步功能的示例。
// Register your service worker: navigator.serviceWorker.register('/sw.js'); // Then later, request a one-off sync: navigator.serviceWorker.ready.then(function(swRegistration) { return swRegistration.sync.register('myFirstSync'); });
然后在/sw.js
中监听事件:
self.addEventListener('sync', function(event) { if (event.tag == 'myFirstSync') { event.waitUntil(doSomeStuff()); } });
骨架屏风
使用骨架屏幕的主要好处之一是用户感知应用程序正在工作而不是闲置。 虽然用户没有连接,但骨架屏幕会在没有内容的情况下绘制界面 - 然后在连接恢复后填充。
Code My UI 提供了一些出色的代码片段,可用于为 PWA 创建骨架屏幕。
根据网络使用情况进行优化
PWA 的一个核心好处是它为用户提供了更快的体验。 您可以通过让 PWA 使用缓存优先网络、优先资源以及使用基于网络质量的自适应加载来进一步优化加载速度。
让我们看看如何将这些开发到您的 PWA 中。
先缓存,后网络
首先使用缓存的内容进一步使您的 PWA 能够离线运行,并为用户即使在低网络覆盖区域也可以访问内容铺平了道路。 您可以通过创建一个服务工作者来缓存内容然后获取它来做到这一点。
这是 Jeff Posnick 关于使用服务工作者缓存静态 HTML 的示例。
self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { // See /web/fundamentals/getting-started/primers/async-functions // for an async/await primer. event.respondWith(async function() { // Optional: Normalize the incoming URL by removing query parameters. // Instead of https://example.com/page?key=value, // use https://example.com/page when reading and writing to the cache. // For static HTML documents, it's unlikely your query parameters will // affect the HTML returned. But if you do use query parameters that // uniquely determine your HTML, modify this code to retain them. const normalizedUrl = new URL(event.request.url); normalizedUrl.search = ''; // Create promises for both the network response, // and a copy of the response that can be used in the cache. const fetchResponseP = fetch(normalizedUrl); const fetchResponseCloneP = fetchResponseP.then(r => r.clone()); // event.waitUntil() ensures that the service worker is kept alive // long enough to complete the cache update. event.waitUntil(async function() { const cache = await caches.open('my-cache-name'); await cache.put(normalizedUrl, await fetchResponseCloneP); }()); // Prefer the cached response, falling back to the fetch response. return (await caches.match(normalizedUrl)) || fetchResponseP; }()); } });
优先考虑资源
默认情况下,由于 PWA 的精简特性,其性能优于类似的原生应用程序。 此外,由于 PWA 使用浏览器的缓存,因此可以指示哪些资源具有优先级,甚至在使用之前就需要渲染。 这主要适用于静态元素,因为动态内容需要在获取之前更新。
您可以使用 HTML 中的<link>
字符串指定元素的优先级。 您还可以使用rel=”preconnect”
和rel=”dns-prefetch.”
Maximiliano Firtman 通过在浏览器引擎中优先考虑 Web 字体给出了一个简单的例子:
<link rel=”preload” as=”font” href=”font.woff” crossorigin>
实现自适应加载
WiFi 和 4G 的网速并非无处不在,用户仍然可以通过 2G 和 3G 连接上网。 由于您希望尽可能多的人可以访问您的 PWA,因此您可能希望优化它以在较低的 Internet 速度下执行。
您可以通过实现自适应加载来实现这一点,它根据用户拥有的连接类型加载 PWA 元素。
最直接的方法是使用 Google 的 Workbox 工具,其中包括许多现成的缓存策略插件。
假设您要定义自定义缓存策略。 以下是 Demian Renzulli 和 Jeff Posnick 的示例:
const adaptiveLoadingPlugin = { requestWillFetch: async ({request}) => { const urlParts = request.url.split('/'); let imageQuality; switch ( navigator && navigator.connection ? navigator.connection.effectiveType : '' ) { //... case '3g': imageQuality = 'q_30'; break; //... } const newUrl = urlParts .splice(urlParts.length - 1, 0, imageQuality) .join('/') .replace('.jpg', '.png'); const newRequest = new Request(newUrl.href, {headers: request.headers}); return newRequest; }, };
接下来,将插件传递给包含正则表达式的cacheFirst
策略以匹配图像 URL(例如/img/
):
workbox.routing.registerRoute( new RegExp('/img/'), workbox.strategies.cacheFirst({ cacheName: 'images', plugins: [ adaptiveLoadingPlugin, workbox.expiration.Plugin({ maxEntries: 50, purgeOnQuotaError: true, }), ], }), );
所有平台上的出色用户体验
出色的 PWA 可以在浏览器、移动设备和平板电脑上无缝运行。 虽然使用 Android 设备是访问 Internet 的最流行方式(占 38.9% 的市场份额),但针对所有平台优化您的应用程序是开发 PWA 核心功能的一部分。
您可以采取进一步措施来提高可用性并提供出色的用户体验,例如减少 PWA 加载时的跳跃,并确保您的 PWA 可与任何输入法一起使用。
以下是您如何处理这些方面的每一个方面。
减少“跳跃”的内容加载
即使使用高速互联网,网站的内容在加载时也会发生变化,因为网站的元素是按顺序加载的。 如果连接速度较慢,这种效果会更糟,严重损害用户体验。
加载时导致内容移动的最常见元素是图像,因为这些元素通常较大并且在加载内容时不是优先级。 您可以通过使用较小的占位符图像来解决“延迟加载”这个问题,您可以在呈现 HTML 结构后优先处理这些占位符图像。
下面是 Mozilla 开发人员的一个示例,说明如何在 JavaScript 中添加在实际图像之前首先加载的轻量级图像:
<img src='data/img/placeholder.png' data-src='data/img/SLUG.jpg' alt='NAME'>
app.js
文件处理 data-src 属性,如下所示:
let imagesToLoad = document.querySelectorAll('img[data-src]'); const loadImages = (image) => { image.setAttribute('src', image.getAttribute('data-src')); image.onload = () => { image.removeAttribute('data-src'); }; };
然后创建一个循环:
imagesToLoad.forEach((img) => { loadImages(img); });
您还可以查看 Smashing Magazine 上关于减少与其他元素的内容跳转的详尽指南。
PWA 适用于任何输入法
我们已经介绍了 PWA 应该如何与各种不同的设备一起工作。 为了更进一步,您还需要考虑用户可以在这些设备上使用的其他输入方法,例如触摸、鼠标和触控笔。
将指针事件 API 添加到您的 PWA 主要解决了这个问题。 以下是根据 Google 开发人员介绍的方法。
首先,检查浏览器是否支持指针事件:
if (window.PointerEvent) { // Yay, we can use pointer events! } else { // Back to mouse and touch events, I guess. }
接下来,您可以定义各种输入法可以执行的操作:
switch(ev.pointerType) { case 'mouse': // Do nothing. break; case 'touch': // Allow drag gesture. break; case 'pen': // Also allow drag gesture. break; default: // Getting an empty string means the browser doesn't know // what device type it is. Let's assume mouse and do nothing. break; }
由于大多数浏览器已经具备触控功能,因此您无需添加任何其他内容。
可通过搜索发现
PWA 与原生应用程序相比的主要优势之一是 PWA 本质上是一个网站,搜索引擎可以索引它们。 这使您可以部署 SEO 策略,使您的 PWA 内容更容易被发现。
您可以首先确保 PWA 中的每个 URL 都具有唯一的描述性标题和元描述,这是任何 SEO 优化活动的基准。
让我们看看您可以采取哪些其他步骤来使您的 PWA 可搜索。
分析 PWA 的可查找性
Google 在其 Search Console 中有一个出色的工具,可以分析您的网站 (PWA) 并报告结果。 您可以使用它对您的网站进行基本扫描,并发现任何可以开始修复的弱点。
或者,您可以在 Chrome 浏览器中使用 Lighthouse 来运行 SEO 审核。
首先,导航到目标 URL。 然后按Control+Shift+J
(或 Mac 上的Command+Option+J
)打开开发人员的工具菜单。 选择 Lighthouse 选项卡,勾选 SEO 类别框,然后生成报告。
使用结构化数据
Google 的搜索引擎使用结构化数据来了解您网页上内容的用途。
结构化数据是一种标准化格式,用于提供有关页面的信息并对页面内容进行分类; 例如,在食谱页面上,成分是什么,烹饪时间和温度,卡路里等等。
- 谷歌
在您开始编码之前,Google 还整理了一份有用的常见结构化数据错误列表以及修复这些错误的相关指南。 学习本材料应该为您提供一个很好的基线,以了解应避免的情况。
Frederick O'Brien 在 Smashing Magazine 上写了一篇出色的指南,将结构化数据烘焙到设计过程中,其中描述了如何从一开始就构建结构化数据。
用户友好的通知和权限请求
最后但并非最不重要的一点是,您可以通过优化通知和权限请求来增加用户体验,以便它们为您的用户服务——而不仅仅是令人困惑和烦人。
虽然您通常可以依靠常识,但您也可以实施一些实用技巧,例如创建非侵入式推送通知并为用户提供取消订阅消息的选项。
微妙的通信许可请求
有两种现代方式可以自动化网站和用户之间的通信——聊天机器人和通知。
在 PWAs 上下文中,聊天机器人的主要优点是它不需要用户的许可来与用户交互。 但是,根据您使用的聊天机器人应用程序,用户可能会错过微妙的消息。 另一方面,通知需要用户的许可,但更明显。
由于您可以将聊天机器人添加为单独的第三方应用程序,因此让我们专注于创建用户友好的推送通知。 如果您首先需要有关如何创建推送通知的指南,这里是 Indrek Lasn 的一个很棒的指南。
创建非侵入式权限请求的最直接方法是使用双重请求。 这意味着您在用户操作系统的默认交互之上包含与站点的自定义交互。
Matt Gaunt 通过以下图片为这种效果提供了完美的插图。
这是不提供上下文的默认通知权限请求:
这是在上述默认通知权限之前添加的自定义交互:
通过在操作系统的默认警报之前添加自定义警报,您可以更清楚地向用户描述通知的目的。 这增加了用户选择接收您网站通知的机会。
允许用户选择退出通知
对于用户来说,无论他们使用什么设备,禁用站点的推送通知都是非常烦人的。 因此,就用户体验而言,为用户提供选择退出消息的选项大有帮助。
下面是一个来自 Matt Gaunt 的例子,关于如何在你的代码和 UI 中实现取消订阅功能:
首先,定义pushButton
的点击监听器:
pushButton.addEventListener('click', function() { pushButton.disabled = true; if (isSubscribed) { unsubscribeUser(); } else { subscribeUser(); } });
然后,添加一个新函数:
function unsubscribeUser() { swRegistration.pushManager.getSubscription() .then(function(subscription) { if (subscription) { // TODO: Tell application server to delete subscription return subscription.unsubscribe(); } }) .catch(function(error) { console.log('Error unsubscribing', error); }) .then(function() { updateSubscriptionOnServer(null); console.log('User is unsubscribed.'); isSubscribed = false; updateBtn(); }); }
这是成功实现订阅按钮以在用户选择时禁用通知后它在控制台中的外观。
结论
PWA 在增加网站流量和扩展用户体验方面非常强大。 要进一步优化您的 PWA,您可以使用本文中描述的这些现代解决方案来增强性能和功能。
您也不需要一次实现所有内容。 随意挑选最相关的选项,因为这些建议中的每一个都可以帮助您进一步推进 PWA。
更多资源
- Google 的渐进式 Web 应用程序培训
- web.dev 的渐进式 Web 应用程序
- Mozilla 的渐进式 Web 应用程序 (PWA)