Hegwin.Me

南朝四百八十寺,多少楼台烟雨中。

谷歌开发者大会2017心得:PWA 渐进式网页应用

Takeaways from Google Developer Days 2017: PWA Progressive Web App

这是Google Developer Days 2017 Take Ways系列的第二篇。在今年的GDG上,渐进式网页应用(PWA,Progressive Web App)是一个反复被提起的技术,我至少参与两次主题是 PWA 的演讲。

PWA所要解决的主要问题是能让 Web App 有着接近 Native App 的效率。另外,在人们不太乐意下载新App的情况下,PWA 给开发者的应用提供一个入口,用户可以通过搜索引擎进入PWA。此外,PWA是可以像 Native App一样被用户保存到桌面,这个过程很轻松就像在手机浏览器里保存书签一样。

关于 PWA 的基本概念

Manifest.json

Manifest-json.jpg

简而言之,Web Application Manifest 就是Web App的一张名片。它是一个JSON文件,开发者可以在这个JSON文件中声明他的Web App的一些元信息,比如:

  • name
  • description
  • theme_color
  • start_url

下面是一个 manifest.json 的例子:

{
  "background_color": "white",
  "description": "Your description",
  "display": "standalone",
  "name": "Your title",
  "short_name": "Your short name",
  "start_url": "/",
  "scope": "/",
  "lang": "en",
  "theme_color": "white",
  "icons": [
    {
      "src": "icon/lowres.png",
      "sizes": "64x64"
    }, {
      "src": "icon/hd_hi",
      "sizes": "128x128"
    }
  ]
}

Service Workers

service-worker-lifecyle.jpg

Web worker的发展已经有一段时间了,但是 service worker 对我来说还是一个比较新的概念。这两者都是为了在JS主进程之外,运行一些复杂的逻辑,从而解放主进程,提高App的运行效率。而service worker除了独立运行之外,它可以有离线缓存的能力。这些是 service worker 特性:

  • 生命周期: service worker的生命周期比较复杂,有 registration, installation, activation等等不同的阶段。

  • 注册:service worker必须要注册(Registration)才能使用

  • 通信:和 web worker 一样,service worker也可以使用 postMessage() 和主进程通信;此外,service worker还有更丰富的API可以调用,如Fetch、 Cache 和 Push,因此它可以用于拦截请求,缓存文件

  • 持久化:service worker 被 install 之后就永远存在,除非手动卸载

  • 兼容性:现在并非所有的浏览器都兼容 service worker,目前兼容列表可以看 这里,相信之后各大浏览器厂商会提供会有更好的支持。

service-worker-registration.jpg

Push 和 Notification

作为 App, 推送和通知也是不可缺少的功能。其中,推送(Push)和通知(Notification)的概念并不一样。

  • 推送:服务器将新信息推给 App,在 PWA里主要就是指推送给 service worker
  • 通知:Service Worker 将更新的信息弹出,给用户以提示

PWA with Ruby on Rails

纸上得来终觉浅;PWA这件事还是要自己尝试一下才好。

我比较常用的 web 框架是 Ruby on Rails,这里我打算基于 Rails 做一点 PWA 的示例。在示例中,我打算一切从简(主要原因是 Rails 有一套自己的 assets 打包机制,这套机制在不同版本 Rails 变化比较大,所以我打算使用最直白的方式)。如果你正在使用 webpacker的话,可以尝试下这个gem serviceworker-rails

从 Manifest 开始

我们在 public 目录下写入 manisfest.json 这个文件,icons部分我就简单写一下:

{
  "name":"Hegwin.me",
  "description":"Hegwin's Blog",
  "icons":[
    {"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
    {"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}
  ],
  "theme_color":"#ffffff",
  "background_color":"#ffffff",
  "display":"standalone",
  "start_url":"/"
}

写一个service worker

在 public/service-worker.js 中:

function onInstall(event) {
  console.log('[ServiceWorker]', "Installing!", event);
}

function onActivate(event) {
  console.log('[ServiceWorker]', "Activating!", event);
}

function onFetch(event) {
  console.log('[ServiceWorker]', "Fetching!", event);
}
self.addEventListener('install', onInstall);
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);

你可能疑惑这里面的 self 是什么,它是Service Worker中的一个全局变量。我们一般会用到这些全局变量:

  • self: 表示 Service Worker 作用域, 也是全局变量
  • caches: 表示缓存
  • skipWaiting: 表示强制当前处在 waiting 状态的脚本进入 activate 状态
  • clients: 表示 Service Worker 接管的页面

初始化 service worker

在 app/views/layouts/application.html.erb 中:

<!DOCTYPE html>
<html>
  <head>
    <link rel="manifest" href="/manifest.json">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <%= javascript_include_tag "application", defer: true %>
    <%= csrf_meta_tags %>

在 app/assets/javascripts/application.js 中:

//= require worker-init

现在我们来写一下 app/assets/javascripts/worker-init.js 这个文件:

if (navigator.serviceWorker) {
  navigator.serviceWorker.register("/service-worker.js", { scope: "/" })
    .then(() => navigator.serviceWorker.ready)
    .then((registration) => {
      if ("SyncManager" in window) {
        registration.sync.register("sync-forms");
      } else {
        window.alert("This browser does not support background sync.")
      }
    }).then(() => console.log("[WorkerInit]", "Service worker registered!"));
}

看看效果吧

service-worker-init.png

好像还不错,logs都成功打印出来了。但是,让我们用Lighthouse再检查一下。

pwa-lighthouse.png

虽然检查认为已经支持了PWA,但我们看的出还是有些瑕疵的:

pwa-check-1.png

主要问题是:

  1. Does not set a theme color for the address bar.Failures: No <meta name="theme-color"> tag found.

  2. Manifest doesn't have a maskable icon

优化

在 app/views/layouts/application.html.erb 中加入:

<!DOCTYPE html>
<html>
  <head>
    <link rel="manifest" href="/manifest.json">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <meta name="theme-color" content="#ffffff">

在 manifest.json 的icons中加入:

{
  "icons": [
    {"src":"/apple-touch-icon.png","sizes": "180x180","type":"image/png","purpose": "any maskable"},

再来看看。

pwa-check-2.png

这下好多了!

如果你一步步跟下来,你会发现之前说的“service worker 被 install 之后就永远存在,除非手动卸载”并非谎言,你可以一直在DevTool的Console里看到打印的logs。怎么消除它呢,方法是这样:

navigator.serviceWorker.getRegistrations().then(function(registrations) {
    for(let registration of registrations) {
        registration.unregister();
    } 
});

Let's call it a day!

< Back