[Google I/O2021]优化PWA体验的四个建议

976 阅读9分钟

本文内容来自视频直播,如果想要了解更多内容可以点击每一段落的标题打开对应的博文进行了解
想要了解PWA以及相应的优化技巧,推荐阅读一下 PWA应用实战 ,虽然里面有部分内容已经过气+过期了,不过里面的思想还是可以活用于现在的

为了让网页可以提供原生应用般的体验,PWA自提出以来越来越受欢迎。单就自己的使用体验而言,越来越多的谷歌网页都开始适配PWA支持,使得以前要在标签页里输入打开的网页成为一个可以直接在开始菜单启动的应用。除了通常的适配策略之外,在这里分享五个用于提供更好用户体验的PWA优化技巧。

提供离线时的fallback页面

谷歌相册会在网络不好的时候跳转到断网页面 serviceWorker可以对于网络请求进行缓存,这样不但可以提高加载的速度,还可以在网络请求失败的时候(例如用户网络环境不好)进行兜底,用缓存数据替代从网络获取失败的页面(并不推荐使用跳转的方式,因为这样意味着用户可能会丢失想要访问的页面URL)。因此,对于很多需要大量网络交互的应用可以在用户进入页面的时候通过一个友好的页面告知用户应用在网路状态不好的情况下不能使用。对于一些纯前端的小应用就没必要增加离线告知界面了,只需要缓存应用代码让用户离线使用即可。

思路首次加载的时候注册SW,缓存OFFLINE.html->下次访问的页面如果既没有匹配到SW的缓存又没有成功通过网络fetch到,就返回之前混存了的OFFLINE.html告知用户这次打开的时候网络有问题->在离线界面不断地循环fetch操作,如果能够成功从网络拉取到任何信息则说明网络恢复,自动跳转到应用界面

代码片段

首先,准备一个告知用户当前离线的页面。为了便于缓存以及节省用户的存储空间,样式和脚本最好内嵌在同一个文件中。
其中,脚本方面,需要对于两部分进行监听:首先,当然是浏览器的网络访问状况,因此,监听Navigator.online事件,网络恢复就重新加载

  window.addEventListener('online', () => {
        window.location.reload();
      });  

除此之外,还有可能服务器出现问题导致不能访问,因此对于这种用户在线服务器当机的情况,设定定时循环操作定期检验服务器状态,直到服务器有响应再重新加载

 async function checkNetworkAndReload() {
        try {
          const response = await fetch('.');
          // 检验是否能从服务器获得应答
          if (response.status >= 200 && response.status < 500) {
            window.location.reload();
            return;
          }
        } catch {
          //用户在线,服务器不能正常工作,那也不用重新加载页面了
        }
        //作为定期任务
        window.setTimeout(checkNetworkAndReload, 2500);
      }

      checkNetworkAndReload();

页面准备好了就要配置SW让其可以在断网情况下加载出来,请参考注释内容

/*
Copyright 2015, 2019, 2020, 2021 Google LLC. All Rights Reserved.
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// Add a comment for your linter if you want:
// eslint-disable-next-line no-unused-vars
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_NAME);//如果SW成功安装,就在对应的cache版本中保存离线用页面
      // Setting {cache: 'reload'} in the new request will ensure that the
      // response isn't fulfilled from the HTTP cache; i.e., it will be from
      // the network.
      await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
    })()
  );
  // Force the waiting service worker to become the active service worker.
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      // Enable navigation preload if it's supported.
      // See https://developers.google.com/web/updates/2017/02/navigation-preload
      if ("navigationPreload" in self.registration) {
        await self.registration.navigationPreload.enable();
      }
    })()
  );

  // Tell the active service worker to take control of the page immediately.
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  // We only want to call event.respondWith() if this is a navigation request
  // for an HTML page.
  if (event.request.mode === "navigate") {//只有跳转请求才使用一整个离线页面接管
    event.respondWith(
      (async () => {
        try {
          // 首先看看浏览器的preloadResponse能不能返回
          const preloadResponse = await event.preloadResponse;
          if (preloadResponse) {
            return preloadResponse;
          }

          // 然后使用网络请求
          const networkResponse = await fetch(event.request);
          return networkResponse;
        } catch (error) {
        //如果出现错误就用之前缓存的离线页面进行回应
          // catch is only triggered if an exception is thrown, which is likely
          // due to a network error.
          // If fetch() returns a valid HTTP response with a response code in
          // the 4xx or 5xx range, the catch() will NOT be called.
          console.log("Fetch failed; returning offline page instead.", error);

          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(OFFLINE_URL);
          return cachedResponse;
        }
      })()
    );
  }

  // If our if() condition is false, then this fetch handler won't intercept the
  // request. If there are any other fetch handlers registered, they will get a
  // chance to call event.respondWith(). If no fetch handlers call
  // event.respondWith(), the request will be handled by the browser as if there
  // were no service worker involvement.
});

按照谷歌相册的使用流程,当网络与服务器的连接不稳定的时候,会显示“相册只能在网络下使用”的提示,中间有一个“重试”的按钮让用户可以主动重新加载,即便浏览器处于online状态,但是不能访问谷歌的时候也不会进行跳转,当网络可以访问的时候,便自动刷新页面。
可以在开发者工具-Application-SW设置里开启offline模式,查看谷歌家的PWA应用是如何处理网络问题的。
不过有的应用则是选择优先加载应用骨架,然后通过组件的提示来告知断网情况,各有利弊吧

image.png

给应用增加快捷指令

image.png
许多应用都会有些常用功能,例如推特经常需要查看个人首页,或者发布消息,对于笔记应用,对于收藏夹增加个快捷方式也是很不错的。因此,我们可以在PWA的声明文件中声明一些常用链接/功能的快捷方式,这样,对于移动端用户,可以通过在桌面长按安装了的PWA的图标选择快捷方式,桌面端用户也可以右键单击图标的方式进行使用。

代码示例

要让一个PWA可以被安装,同时让用户和浏览器更加理解应用提供的功能,manifest必不可少,因此,如果需要添加快捷方式,也只需要在shortcuts项中声明即可。

{
  "name": "Player FM",
  "start_url": "https://player.fm?utm_source=homescreen","shortcuts": [
    {
      "name": "Open Play Later",
      "short_name": "Play Later",
      "description": "View the list of podcasts you saved for later",
      "url": "/play-later?utm_source=homescreen",
      "icons": [{ "src": "/icons/play-later.png", "sizes": "192x192" }]
    },
    {
      "name": "View Subscriptions",
      "short_name": "Subscriptions",
      "description": "View the list of podcasts you listen to",
      "url": "/subscriptions?utm_source=homescreen",
      "icons": [{ "src": "/icons/subscriptions.png", "sizes": "192x192" }]
    }
  ]
}

其中,只有 name,url是必要的。所有被声明了的快捷方式可以在开发者工具中查看,使用的时候需要注意:把最常用的放在shortcuts数组的最前面,名字需要简洁直接 image.png

做好应用声明的国际化

虽然感觉这个不属于前端的范畴了,不过还是值得讲一下。之前提到了PWA通过声明文件告知浏览器和用户自己的各种信息和功能,因此,对于不同语言的用户,提供不同语言的声明文件十分有必要,这样同样是添加一个应用在桌面,中文用户显示的是推特,英文用户显示的就是twitter了。其实现有两种方式。 许多人会把应用托管在GHPages上,因此需要把更换manifest的逻辑放在前端,因此可以参照这篇文章进行操作,具体思路就是判断navigator.language然后根据获得的用户语言动态生成对应语言的manifest节点。

<script>
    var userLan = navigator.language || 'en-US'
link = document.createElement('link');
    link.href = './manifest/manifest.' + userLan + '.json';
    link.rel = 'manifest';
document.getElementsByTagName('head')[0].appendChild(link);
</script>
<!-- <link rel="manifest" href="/manifest/manifest.zh-CN.json" > -->

不过这个有一个缺点,因为应用安装过后不能更改manifest的路径/文件名,否则就不能对其进行更新。因此如果用户中间更改了需要的语言,就需要卸载重新安装了。
因此,可以对服务器进行操作,推荐使用请求头来判断用户语言,然后给用户的同一请求返回对应语言的文件的方式,这样manifest的路径不变,PWA就可以以更新的方式切换应用声明的语言了。可以通过获取请求头的accept-language来决定语言,如果应用提供设置界面给用户切换应用语言的话(推特,又是你),还可以先读取cookie携带的信息,通过用户设置的语言->浏览器语言的方式决定返回什么语言的声明。

监测用户使用PWA功能的效果

同样是网页应用,其又可以直接在浏览器直接浏览,也可以通过添加到桌面的方式像一个独立的应用进行使用。那肯定是希望用户能够安装一下啦(,所以就搞个推荐窗口引导用户安装,并且进行统计吧。

引导用户安装

如果浏览器判断用户在一个页面中使用的活跃度比较高的话,就会自动弹出如下的窗口建议用户安装PWA应用到桌面以更好地使用。不过很多用户不明白这是什么,所以就让我们自己来接管吧(使命感) image.png

let defferedPromt;//这个存储被接管了的浏览器PWA安装推广事件
window.addEventListener('beforeinstallpromt',e=>{
e.preventDefault();//接管
defferedPromt =e;//用于用户点击了我们提供的按钮之后的调用
showCustomPWAPromtInWeb();
ga(xxxx);
})

showCustomPWAPromtInWeb函数中,我们需要在自己的页面中弹出推广窗口,推荐用户将应用进行安装,威逼利诱下啥的,例如“我这里有好康的,让我安装!”或者"我们发现您在我们页面中使用了很多次,是否有兴趣将其安装到桌面,这样就可以以独立程序的形式进行使用啦"这样的,用户点击了yes之后,就调用 defferedPromt.promt() 这时候浏览器才会弹出之前的那个安装确认气泡,这样用户的安装概率就大大提升了。

做好用户使用的统计

小应用什么的随便啦,不过相信对于适配了PWA的开发者而言,还是对于自己的PWA效果挺好奇的吧,用户喜不喜欢,适配PWA能不能提升用户留存啥的。因此可以对于以下的流程进行埋点统计:
首先,当弹出了PWA安装提示的时候,以及用户对于浏览器安装PWA提示的选择 await defferedPromt.userChoice 可以得到用户对于PWA的接受程度,同时还可以监听appInstalled事件,判断用户是自行点击浏览器地址栏的应用安装的还是5通过我们弹出的小广告来安装的。同时,还可以匹配当前应用是在standalone(独立应用窗口)还是tab的形式进行使用,来生成对应的用户画像。

闲话

我很喜欢PWA!因为可以让一个简单写出来的网页有应用般的效果!
现在浏览器越来越厉害了,让web应用有原生应用般的体验,无论是网络优化,还是系统支持,都在越来越完善,我也发现很多我平时使用的平台开始对于PWA进行支持了。不过国内的网络走向了另一个方向,首先是阉割web功能,强制下载app制造围城,齐次国内的浏览器也阉割了对于web标准的支持,不过别人怎么样是别人的事,如果自己在写web小工具的时候可以进行优化,相信别人在使用的时候也能会心一笑吧xd