跳到主要內容
黯羽輕揚每天積累一點點

Progressive Web Apps

免費2017-12-15#Front-End#PWA Engaging#渐进式WebApp#PWA downasaur#PWA入门指南#PWA tutorial

為什麼總能聽到 PWA?

一. What?

A new way to deliver amazing user experiences on the web.

一種提升 Web 用戶體驗的方式。除了 Web 天生的(便捷)體驗外,還有 3 個特點:Reliable, Fast, Engaging

  • 可靠:在不確定的網路環境下,也能立即載入,而不會(因為斷網而)瞬間回到遠古時代

可靠指的是離線快取,斷網狀態走快取,保證離線場景仍然可用,service worker 配合 cache API 建立快取-代理機制

  • 快速:迅速以絲滑的動畫作為交互反饋,而不存在掉幀卡頓的捲動

快速,只是強調交互反饋「感覺快」,與推崇的 Material Design 有關,並沒有真正的速度優勢(至少首屏沒有)

另外,得益於快取-代理機制,再次訪問時走本地快取會相當快

  • 類 native:像設備原生 App 一樣,具有沈浸式的用戶體驗(即全屏)

除了全屏外,還有主屏圖示(讓 Web App 在主屏幕有一席之地)和系統通知(「拉活」的能力),透過 Web App Manifest 配置來實現,依賴用戶環境支持

P.S. Engaging 這個抽象形容詞真不好翻譯,這裡暫且取其實際意義,類 native

所以,表面上看,PWA 的亮點分 2 部分:

  • (離線)快取-代理機制

  • 全屏,主屏圖示和系統通知等類 native 特性

快取機制在 Web App/SPA 裡一點不新鮮,抽離出數據層之後,快取順手就做了。但側重點不同,PWA 的快取機制偏向於靜態資源快取,而 Web App/SPA 的快取層多用來做動態內容快取(上次的內容沒過期的話,不再重新獲取動態部分,而是直接做客戶端渲染)

至於全屏,主屏圖示以及系統通知等類 native 特性,算是漸進增強中的增強,在支持的用戶環境是可用的(一些瀏覽器提供了支持,但更廣泛的 WebView 環境在不久的將來可能還是不行)。這表明 Web 正在以漸進增強的方式走出 PC 時代,向著行動化發展

二. 試玩

依賴環境

  • HTTPS

要求服務源必須是安全的,所以需要 HTTPS 環境。除了出於 Web 資訊安全的考慮,想要推進 HTTPS 普及也是一個重要原因,HTTPS 作為 Web 技術發展的必要基礎設施,對於拍照,錄音,push API 等新特性,都需要獲得用戶許可,而 HTTPS 是權限工作流的關鍵部分,必不可少

P.S. 在 permission.site 能夠體驗到 HTTPS 與 HTTP 環境在獲取用戶授權方面的差異

類 native 增強

透過引入 Web App Manifest 配置文件來實現類 native 增強,在支持 PWA 的瀏覽器生效(在不支持的環境最壞結果也就是多請求一個 JSON 文件):

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

注意,有個比較相似的東西,叫 Application Cache(HTML5 特性,已過時),其 manifest 引入方式不同:

<html manifest="example.appcache">
  ...
</html>

因為二者引入方式不同,所以 Web App Manifest 與 Application Cache 是不相干的,沒有歷史包袱的後顧之憂

P.S. Application Cache 對 SPA 支持較好,對多頁應用則不適用,但 存在很多問題,這裡不多做介紹

主屏圖示

Web App Manifest 內容示例如下:

{
  "short_name": "主屏显示的应用名称",
  "name": "安装banner显示的应用名称",
  "icons": [
    {
      "src": "launcher-icon-1x.png",
      "type": "image/png",
      "sizes": "48x48",
      "density": "1.0"
    },
    {
      "src": "launcher-icon-2x.png",
      "type": "image/png",
      "sizes": "96x96",
      "density": "1.0"
    },
    {
      "src": "launcher-icon-4x.png",
      "type": "image/png",
      "sizes": "192x192",
      "density": "1.0"
    }
  ],
  "start_url": "index.html?launcher=true"
}

P.S. 安裝 banner 是指一個類似於獲取權限的彈出面板,用戶可以選擇添加至主屏幕或取消,滿足一定條件的話,Chrome 會自動彈出安裝 banner,具體見 Web App Install Banners

這樣理想情況下我們就擁有了主屏圖示,支持 Web App Manifest 的環境會選用最合適的(最接近 48dp 的)圖示

注意:index.html 裡的內容應該是首屏渲染需要的最小化內容,為了達到首屏立即載入的效果,可以把帶 loading 和默認佔位圖的頁面框架作為 App Shell 展示出來。另外,為了達到秒開可用的首屏性能,Web App 首屏性能優化其他常規手段在 PWA 也是推薦使用的,比如 數據直出。如開篇所說,PWA 並沒有天生的(首屏)性能優勢,Web App 適用的常規優化手段仍然是必要的

閃屏(Splash)

從主屏圖示進入,可定制的啓動過程顯示內容包括:標題,背景色和圖像。新配置項如下:

// 背景色
"background_color": "#2196F3",
// 主题色,包括工具栏
"theme_color": "#2196F3",

圖像從 icons 中選取最接近 128dp 的圖像作為閃屏,支持動圖

另外,還可以指定顯示模式和頁面方向:

// 全屏(隐藏浏览器的UI)
"display": "standalone",
// 显示浏览器外壳,像打开书签一样
"display": "browser",
// 横屏
"orientation": "landscape"

P.S. 關於閃屏的示例及更多資訊請查看 Adding a Splash Screen for Installed Web Apps in Chrome 47

特別注意:如果 manifest.json 文件有更新,這些改動不會自動生效,除非用戶重新添加應用到主屏

系統通知

與 Web App Manifest 無關,依賴 Push API。簡單示例如下:

// service-worker.js
self.addEventListener("push", function (event) {
  event.waitUntil(
    self.self.registration.showNotification("发布新文章啦", {
      body: "有新文章发布啦,点击查看。"
    })
  );
});

這裡不多做介紹(目前(2017/12/15)幾乎可以認為這個特性不存在),因為規範定義了 API,但沒規定統一個 push 協定,所以各家瀏覽器的 push 機制不同,比如 Chrome 的 GCM 在我們這片天空下就不可用

關於 Push API 的更多資訊,請查看 【Service Worker】消息推送功能“全军覆没”

快取-代理

快取分為幾部分:

  • 首屏靜態資源快取(預快取)

  • 已訪問資源快取(執行時快取)

  • 動態內容快取(執行時快取)

快取是純數據操作(包括持久化),而 service worker 能夠在後台運行,尤其適合處理這種與頁面及交互無關的事情,所以 service worker 與 Cache API,Push API 成了搭檔。但 service worker 自身也應該看做「增強」項,在不支持 service worker 的環境應該跳過快取機制保證基本的頁面體驗,簡單的特徵檢測方案如下:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
           .register('./service-worker.js')
           .then(function() { console.log('Service Worker Registered'); });
}

service worker 在 install 事件處理器完成包括 App Shell 在內的首屏靜態資源快取:

// service-worker.js
var cacheName = 'weatherPWA-step-6-1';
var filesToCache = [
  // 入口URL
  '/',
  '/index.html',
  '/scripts/app.js',
  '/styles/inline.css',
  // App Shell需要的资源
  '/images/ic_add_white_24px.svg',
  '/images/ic_refresh_white_24px.svg',
  // 内容展示可能用到的资源
  '/images/clear.png',
  '/images/cloudy-scattered-showers.png',
  '/images/cloudy.png',
  '/images/fog.png',
  '/images/partly-cloudy.png',
  '/images/rain.png',
  '/images/scattered-showers.png',
  '/images/sleet.png',
  '/images/snow.png',
  '/images/thunderstorm.png',
  '/images/wind.png'
];

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[ServiceWorker] Caching app shell');
      //! 只要有一个失败就不接着做下一个了
      return cache.addAll(filesToCache);
    })
  );
});

當然,還需要對快取做基本的版本控制:

// service-worker.js
self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        // 以为cacheName为cache key,如果存在旧的缓存,删除掉
        if (key !== cacheName) {
          console.log('[ServiceWorker] Removing old cache', key);
          return caches.delete(key);
        }
      }));
    })
  );
  // 要求立即激活service worker,避免边界case
  return self.clients.claim();
});

P.S. 邊界 case 指的是某些情況下 service worker 無法立刻恢復激活態,導致不走快取。為了屏蔽這些邊界 case,推薦使用 GoogleChromeLabs/sw-precache 幫助處理快取控制問題(包括過期,更新策略等等)

快取有了,接下來實現代理部分,攔截請求,並把快取內容作為回應:

// service-worker.js
// 拦截请求
self.addEventListener('fetch', function(event) {
  console.log('[ServiceWorker] Fetch', e.request.url);
  // 自定义响应内容
  e.respondWith(
    // 查找缓存,没有才请求
    caches.match(e.request).then(function(response) {
      return response || fetch(e.request);
    })
  );
});

到這裡基本的快取-代理機制就準備好了,我們做了這些事情:

  1. 按資源列表預先快取靜態資源

  2. 攔截請求

  3. 把快取內容作為回應給過去

有 3 個注意事項

  • 瀏覽器快取可能會影響快取更新,所以 install 事件處理器中的請求不會走快取,而是直接進入網路

  • 註銷 service worker 不會清掉快取,cache key 不變的話,之後還會拿到舊的快取內容

  • 默認新註冊的 service worker 在頁面重新載入之後才會生效,除非做 特殊處理

另外,我們的簡版實現還存在一些問題,例如:

  • 快取版本控制依賴一個靜態的 cache key,每次更新 service-worker.js 都要修改這個 key

  • 一旦 cache key 有變化,會抹掉所有快取,重新請求一遍,對於靜態資源有些浪費

  • 缺少執行時快取,資源列表不夠靈活,期望更強大的邊訪問邊快取

第 1 個問題沒什麼太好的辦法,第 2 個問題可以透過細分資源類型來緩解,例如:

// Shorthand identifier mapped to specific versioned cache.
var CURRENT_CACHES = {
  font: 'font-cache-v' + FONT_CACHE_VERSION,
  css: 'css-cache-v' + CSS_CACHE_VERSION,
  img: 'img-cache-v' + IMG_CACHE_VERSION
};

透過更細粒度的版本控制,能在一定程度上降低強制更新快取的成本,當然,快取層下面還有 HTTP Cache 兜底,快取更新成本不是非常關鍵

至於執行時快取,實際上只需要再做最後一小步就好了:

  1. 沒命中快取的話,請求資源*並快取*

具體如下:

// 查找缓存,没有才请求
caches.match(e.request).then(function(response) {
  return response || fetch(e.request).then(function(res) {
    return caches.open(dataCacheName).then(function(cache) {
      // 并缓存起来
      cache.put(e.request.url, res.clone());
      return res;
    )
  })
})

另外,還可以根據資源類型及場景要求,針對性的選用合適的快取策略,例如:

// service-worker.js
self.addEventListener('fetch', function(e) {
  console.log('[Service Worker] Fetch', e.request.url);
  var dataUrl = 'https://cache.domain.com/fresh/';
  // 策略1:有实时性要求的资源,请求优先,fetch then cache
  if (e.request.url.indexOf(dataUrl) > -1) {
    e.respondWith(
      caches.open(dataCacheName).then(function(cache) {
        return fetch(e.request).then(function(response){
          cache.put(e.request.url, response.clone());
          return response;
        });
      })
    );
  } else {
    // 策略2:一般资源,缓存优先,cache falling back to fetch
  }
});

P.S. 更多快取策略,見參考資料部分

三. Demo

官方 Demo:Weather PWA,可能無法正常訪問

搬運 Demo(把官方 Demo 挪到 github pages):https://ayqy.github.io/pwa/demo/weather-pwa/index.html

P.S. github pages 非常適合用作試驗田,穩定可靠的 HTTPS,發布內容沒有任何限制可以隨便折騰,以後的部落格 Demo 都會逐步遷移過去(之前一直放在自己的 FTP,那可真蠢..)

[caption id="attachment_1610" align="alignnone" width="169"]weather-pwa weather-pwa[/caption]

不太樂觀的消息:事實上,故意精心準備了用戶環境(官方正版 Chrome + 官方 Demo),在小米 4 上沒有自動彈出安裝 banner(可能是操作姿勢等條件不滿足,見上文),手動點擊「添加至主屏幕」,toast 添加成功,但主屏幕上啥也沒有……這就是提不起興趣手寫 Demo 試玩的原因(當然,主要原因是懶 ;))

四. 案例

注意,隱身模式可能會導致阿里巴巴國際站的 service worker 拋如下錯誤:

Uncaught (in promise) DOMException: Quota exceeded.

正常環境可正常體驗

P.S. 更多案例,請查看 Case Studies | Web | Google | Developers

五. 應用場景

簡言之,PWA 算是 Web App 的升級版,主要亮點是類 native 支持。以漸進增強的方式,不需要太高成本就能完成 Web App 到 PWA 的「升級」,讓部分用戶(支持 PWA 的環境)獲得更快(快取)更便捷(主屏圖示)的類 native 體驗(全屏)

那麼具體應用場景分以下幾種:

  • 快取能帶來明顯收益的 Web App

  • 期望具有離線能力,或類 native 體驗,或者單純只是想要個主屏圖示的 Web 應用

  • 期望蹭個技術熱點/協助推動其發展的 Web 應用或瀏覽器供應商

不管應用場景,話說回來,正如 zxx 某篇關於快取(還是 worker?)的文章所說,這麼點兒成本就能讓頁面獲得離線能力,真切看到快取帶來的收益,何樂而不為呢?

另外,Angular,React,Vue 等主流框架都提供了 PWA 腳手架,具體請查看 The Ultimate Guide to Progressive Web Applications

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論