PWA 一隅

文章創做於 2019-08-30,2019-12-20 遷移至此

PWA 簡介

PWA,全稱是 Progressive Web Application,它不特指某一項具體技術,能夠看作是一些新技術的集合。PWA 本質上是 Web App,藉助新技術,具有了 Native App 的一些特性。css

亮點

MDN 上列舉了 PWA 的優勢,這些優勢主要是對目前 Web App 的痛點進行改進。下面列舉比較能體現 PWA 特點的幾點。html

漸進式(Progressive)

各項技術相互之間沒有依賴,能夠獨立實施。若是某項技術在客戶端上不支持,那就對其無效,僅此而已。實施新特性無需破壞應用的向後兼容性。前端

採用漸進式的考慮主要體如今:vue

  • 能夠下降站點改造的代價
  • 新技術標準的支持度還不徹底,標準還未最終肯定

鏈接獨立性(Connectivity independent)

藉助 Service Worker,能夠在離線或低速網絡狀態下工做。react

可安裝(Installable)

容許用戶將應用添加到桌面。webpack

再次訪問的吸引力(Re-engageable)

經過 Web Push API 實現消息推送, Notifications API 實現桌面通知,可以吸引用戶從瀏覽器外再次訪問。git

PWA Checklist

PWA Checklist 給出了 PWA 應用能夠參照的標準,除了閱讀這個標準外,也能夠經過 Lighthouse tool 對 Web 應用進行分析,獲得 PWA 的改進建議。程序員

相關核心技術

  • Web App Manifest
  • Service Worker
  • Notifications API
  • Push API

例子 - 豆瓣 PWA

若是你已經閱讀了上文的 PWA Checklist,你可能會發現,現有的不少網站已經具有了部分的 PWA 能力(如 HTTPS,響應式)。但做爲一個稍微有點追求的程序員,對我來講只有使用上 Service Worker、App Manifest 等核心技術的 Web App 在我心中才有資格稱得上是真正的 PWA。github

以這個標準來衡量一個 Web App 是不是 PWA 的話,目前國內廠商中使用 PWA 技術的主要有 豆瓣移動版微博移動版餓了麼-H5阿里巴巴(國際)-移動版。細心的你可能會發現,這些應用都是移動端的。若是咱們考慮 PWA 的出現是爲了使 Web App 擁有某些 Native App 的能力(如離線使用、消息推送),而這些能力在移動端能發揮出更大的價值的話,也許就不難理解廠商爲何首先在移動端使用 PWA 技術了。(固然也有像 Vue 官網谷歌郵箱 這樣同時在 PC 端提供 PWA 技術的,由於這些技術對於 PC 端一樣有幫助,只是在移動端更加明顯而已)。web

OutwebAppscope 是兩個收錄 PWA 應用的網站,能夠在上面查找 PWA 應用。

下面以 豆瓣移動版 爲例,大體地感覺一下 PWA 與傳統 Web App 之間的區別。

首先讓咱們用上文提到的 Lighthouse tool 跑個分。

36600763

嗯,PWA 單項得分 91 分,好像還不錯的樣子,不過不夠直觀,因此咱們仍是看看程序吧。

74040098

62878046

如圖 (a) - (f) 是使用 Android 的 Chrome 瀏覽器瀏覽豆瓣移動版時的截圖。這裏主要涉及 Manifest(a-e)和 Service Worker(f)兩項技術。

第一次進入頁面的時候,瀏覽器會提示將應用添加到主屏幕(a),點擊添加(b)後,手機桌面上將生成豆瓣手機版的圖標(c),點擊桌面圖標再次進入頁面時,能夠看到應用的歡迎界面,瀏覽器的地址欄也會消失不見(d)。此時,若是查看後臺應用,能夠發現系統進程中當前頁面以 「豆瓣(手機版)」 而不是 「Chrome 瀏覽器」 的名義顯示(e)。關閉移動數據和無線網絡,刷新頁面後仍能夠正常瀏覽以前瀏覽過的內容(f)。

61229909

咱們經過 PC 端的 Chrome 瀏覽器控制檯能夠看到,首頁的推薦信息流以 JSON 的形式被保存在 Cache 中,若是瀏覽了相關文章,Cache 中也會有相關的 JSON 文件被保存下來。當咱們離線使用時,若是命中了相關的 Cache,其中的內容將被取出用於渲染頁面,這也是咱們爲何能在離線狀態下看到(f)的緣由。

Service Worker

前身

Service Worker 與另外兩項技術有所關聯: Web WorkerApplication Cache

瀏覽器的 JavaScript 都是運行在一個主線程上,隨着業務不斷複雜,性能問題不斷凸顯。W3C 提出了 Web Worker API,將一些耗時、耗資源的任務交給這個 API,完成後經過 post Message 方法告訴主線程,主線程經過 onMessage 方法獲得反饋結果。但 Web Worker 是臨時的,每次進行的操做不能被持久化保存下來,不能解決重複訪問時的耗時問題。在此基礎上,Service Worker 被提出,在 Web Worker 的基礎上增長了持久的離線緩存能力。

Application Cache 則是在 HTML5 早些時候提出的一種應用程序緩存機制。這個標準也試圖讓應用在離線狀態下可用。可是因爲開發者沒法對緩存進行有效控制,以及其它一些更新邏輯的缺陷,目前已從 Web 標準中移除。

Service Worker 能作什麼?

  • 攔截網絡請求
  • 緩存可用時返回緩存內容
  • 對緩存內容進行管理
  • 向客戶端推送信息
  • 後臺數據同步
  • 資源預取

特色

  • 必須在 HTTPS 環境下才能工做
  • 獨立的線程,有本身的 worker context
  • 使用時被自動喚醒,不用時自動休眠
  • 不能直接操做 DOM
  • 異步實現,內部大都是經過 Promise 實現

相關依賴

  • HTTPS
  • Promise
  • Fetch API(獲取資源)
  • Cache API(緩存)
  • Push API(消息推送)

兼容性

截至目前(2018-08-30),大約 83.89% 的瀏覽器支持 Service Worker。

72417529

生命週期方法

1658a6b5d94c511b

Service Worker 的生命週期大體能夠分爲四個階段。

  • Parse 解析
  • Install 安裝
  • Activate 激活
  • Redundant 廢棄

Parse

Service Worker 是掛載到 navigator 下的對象,使用前須要檢查其可用性,若是可用則進行 Service Worker 的註冊。始終要記住的一點是 Service Worker 須要工做在 HTTPS 下,不然會無條件地註冊失敗。

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

register() 方法有一個可選的參數 scope,能夠指定讓 Service Worker 控制緩存哪一個目錄下的文件(做用域),不填寫時默認爲 Service Worker 文件所在的目錄。

.register('service-worker.js', {scope: '/'})

註冊成功後在 Chrome 控制檯 Application 下的 Service Worker 中能夠看到。

Install

const PRECACHE_URLS = ["../", "../styles/index.css", "../scripts/index.js"]

self.addEventListener("install", event => {
  event.waitUntil(
    caches
      .open('shell')
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  )
})
  1. self.skipWaiting() 用於跳過等待狀態,這個方法主要涉及新的 Service Worker 安裝和老的 Service Worker 廢棄的過程,即 Service Worker 的更新。通常狀況下,新的 Service Worker 安裝完成後將會進入等待狀態,須要在老的 Service Worker 中止工做(通常是關閉瀏覽器)後纔會取代。
  2. 因爲系統會隨時休眠 Service Worker,爲了防止執行中斷,須要使用 event.waitUntil() 進行捕獲,它會監聽異步請求返回的 promise,若是其中有 reject 的狀況,則會致使 Service Worker 開啓失敗。

爲了防止因爲某些大文件或不穩定的文件下載失敗致使 Service Worker 啓動失敗,能夠只讓一部分文件經過 cache.addAll() 返回。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('shell').then(function(cache) {
    // 不穩定文件或大文件加載
      cache.addAll(
        //...
      );
      // 穩定文件或小文件加載
      return cache.addAll(
        //
      );
    })
  );
});

其中,第一個 cache.addAll() 將不會被捕獲。

Activate

Service Worker 處於 activated 狀態下時能夠處理事件,如請求攔截與緩存捕獲。在這以前,咱們能夠監聽 activate 事件,在回調函數中對舊的無用緩存文件進行清理。

self.addEventListener("activate", event => {
  const currentCaches = [SHELL, RUNTIME];
  event.waitUntil(
    caches
      .keys()
      .then(cacheNames => {
        return cacheNames.filter(
          cacheName => !currentCaches.includes(cacheName)
        );
      })
      .then(cachesToDelete => {
        return Promise.all(
          cachesToDelete.map(cacheToDelete => {
            return caches.delete(cacheToDelete);
          })
        );
      })
      .then(() => self.clients.claim())
  );
});

self.clients.claim() 作的是在不從新加載的前提下取得頁面控制權。

fetch

下面介紹一下請求攔截與緩存捕獲這一步。PWA 最吸引人的地方之一離線能力就是這一部分操做實現的。

這個部分涉及上文提到的兩個 API:

  • Fetch API
  • Cache API

下面是一個簡單的例子。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});

首先咱們須要監聽瀏覽器自己的 fetch 事件,respondWith 用來響應頁面的請求。這裏使用了 Catch API 的 match 方法來查找 Cache 中是否存在與 request 請求匹配的緩存,若是不存在則再經過 Fetch API 進行遠程請求。

若是咱們在 install 時把頁面和相關的資源緩存下來,在這一步已經可以實現頁面的離線訪問了。

這段代碼能夠進行優化,當沒有命中 cache 進行遠程請求後,能夠將 fetch 的內容加入緩存中,這樣這些資源在下一次訪問的時候就能夠直接使用了。

self.addEventListener("fetch", event => {
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.open(RUNTIME).then(function(cache) {
        return cache.match(event.request).then(function(response) {
          var fetchPromise = fetch(event.request).then(function(networkResponse) {
            cache.put(event.request, networkResponse.clone());
            return networkResponse;
          })
          return response || fetchPromise;
        })
      })
    );
  }
});

值得注意的是這裏的 response 須要給瀏覽器進行渲染,並同時保存的緩存中。因爲 caches.put 使用的是文件的響應流,一旦使用就會形成 response 沒法訪問(能夠理解爲破壞性讀出),因此須要事先使用 clone 方法複製一份。

咱們再捋一下上面代碼的邏輯。

  1. 監聽瀏覽器 fetch 事件,攔截本來的請求,
  2. 檢查 cache 中是否存在將要請求的資源,有則返回緩存,無則進入下一步,
  3. 遠程請求資源,將資源緩存後返回。

這是典型的 「緩存優先」 策略。事實上,經過 Fetch API 和 Cache API 順序的排列組合,在攔截瀏覽器 fetch 事件後能夠實現多種策略。

  • 緩存優先
  • 網絡優先
  • 僅使用緩存
  • 僅使用網絡
  • 速度優先

若是對策略的具體實現有所疑惑,能夠參考 Service Worker最佳實踐 - 騰訊瀏覽服務,此處再也不贅述。

Redundant

新的 Service Worker 進入 activated 狀態後,老的 Service Worker 將被廢棄,即 redundant 狀態。

Service Worker 更新

更新 Service Worker 只需直接改動對應的 JavaScript 文件便可。瀏覽器會自動檢測差別性進行獲取。

當新的 Service Worker 被下載並 install 後,將進入 waiting 狀態。此時兩個 Service Worker 同時存在,仍由老的 Worker 控制頁面。只有當老的 Service Worker 中止工做時,新的 Service Worker 纔會進入 activated 狀態並掌管頁面。

Web App Manifest

注意這裏的 ManifestApp Cache 中的 Manifest 徹底不一樣,後者已從 Web 標準中移除,並由 Service Worker 替代。

JSON ? meta?

Web App Manifest(Web 應用程序清單)歸納地說是一個以 JSON 形式集中書寫頁面相關信息和配置的文件。這種清單的形式早在 瀏覽器插件開發中就已經出現(也許 PWA 中的 Manifest 是借鑑了其中的形式,只是屬性有所區別)。

W3C 上提到了 Manifest 採用 JSON 形式的外置文件的一些考慮。總結一下主要有如下幾點:

  1. 解耦。無需在各個頁面重複聲明 meta 標籤,利於維護。
  2. 可緩存。HTML 可能常常變更,意味着用戶代理一般須要下載整個 HTML 文件。使用外置的 Manifest 文件可以更好地利用緩存。
  3. 書寫更靈活。這點的考慮其實能夠借鑑 XML 與 JSON 的比較。標籤化的結構適合 UI,而像 JSON 這樣的結構更適合數據。Manifest 就是應用程序的一些數據,從這個角度看 JSON 比 meta 標籤更合適。

用法

Manifest 的使用方法很是簡單。首先須要在 head 中引用。

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

在 Manifest 文件中,用 JSON 的形式書寫應用的相關信息。

{
  "name": "App name",
  "short_name": "App short name",
  "description": "Here is the description",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#fff",
  "icons": [
      {
        "src": "images/homescreen.png",
        "sizes": "48x48 72x72 96x96 128x128 256x256",
        "type": "image/png"
      },
      {
        "src": "icon/logo.ico",
        "sizes": "96x96"
      }
  ]
}

相關字段說明

下面介紹幾個經常使用的字段。

name

Web App 的全名,做爲 App 圖標的文字標籤。

short_name

爲 Web App 提供簡短易讀的名稱,以便在沒有足夠空間顯示應用程序全名時使用。

description

有關 Web App 的描述信息。

start_url

設置用戶從主屏啓動 App 時加載的 URL(用戶在詳情頁將應用添加到首屏,此時若 start_url 爲空,則用戶從首屏打開應用時打開的頁面將是詳情頁)。

scope

定義 Web 應用程序上下文的導航範圍,若是用戶在範圍以外瀏覽應用程序,則返回正常的網頁。

display

定義應用程序的首選顯示模式,擁有四個可選的值,目前使用 PWA 的網站用得比較多的是 standalone 。

  • fullscreen 全屏顯示,不顯示狀態欄
  • standalone 像一個獨立的應用程序,具備不一樣的窗口、圖標,瀏覽器用於控制導航的 UI 將被移除,可能包含其它 UI 元素(如狀態欄)
  • minimal-ui 像一個獨立的應用程序,但包含瀏覽器地址欄
  • browser(默認) 以傳統瀏覽器標籤或新窗口形式打開

background_color

使瀏覽器能夠在 CSS 加載前繪製 Web App 的背景顏色。

icons

指定各類環境下,應用程序圖標的的圖像對象數組。


除此以外,還有其它的屬性,如需查看它們的用法能夠參考 MDN 文檔W3C文檔

Manifest 更新

一旦用戶將應用 icon 添加到桌面,以後 icon 將不能被更新,除非用戶將其刪除後從新添加到桌面。

額外須要考慮的問題

Service Worker 啓動性能

在 W3C 的 Github 上關於 Service Worker的討論中,能夠看到目前 Chrome 中 Service Worker 的啓動耗時大概在 200ms 左右,這意味着若是使用 Service Worker 以後減小的加載時間若是不足 200ms,反而會延長頁面的加載時間。但這個問題這有望在今年 Chrome 後續版本的更新中獲得改善。

沒法優化 「首次加載」 速度

從 PWA 的流程上能夠發現,PWA 不能完全優化 「首屏加載」 的性能問題(如白屏)。當新用戶 「首次加載」 或用戶清除瀏覽器緩存以後進入頁面,到真正的使用上某個文件的緩存,須要通過三次網絡請求。第一次是請求 Service Worker 所在的腳本文件,第二次是請求這個須要緩存的文件自己,到了第三次請求的時候,這個緩存才能真正生效。若是想要讓用戶在「首次加載」的時候一樣擁有流暢的體驗,單靠 PWA 是不夠的。

優化「首次加載」的速度,首先想到的方式多是使用 SSR (Server Side Render,瀏覽器端渲染)代替 CSR (Client Side Render,客戶端渲染)。Vue 甚至有官方的 SSR 指南 介紹使用過程。這裏分享一個騰訊視頻前端團隊的演講 —— 《極致流暢的移動 Web 應用解決方案》,演講中詳細比較了 SSR 和 CSR 的關鍵渲染路徑及相關性能。下圖摘自演講 PPT ,從圖中能夠看出 SSR 相比 CSR 節省的主要時間來自 JS 生成 HTML 以及請求數據填充內容的過程。

65175998

值得一提的是,這個演講中我的感受最有趣的一點是他們使用了 Web Socket 代替 AJAX 請求數據,減小了重複建立鏈接所消耗的時間。

對於 CSR,一樣有相應的優化措施。好比,能夠在 webpack 打包的過程當中,使用 prerender-spa-plugin 在構建時生成頁面首屏,也能夠大大減小 FCP(First Contentful Paint)時間,達到優化首次加載的目的。

國內 PWA 生態環境

在 PWA 技術推廣上,谷歌表現得較爲積極,其 PWA 生態也較爲完整。但因爲衆所周知的緣由,谷歌提供的服務在國內沒法正常使用。好比,在消息推送上,谷歌提供了FCM 服務,但國內沒有相關的能夠替代的基礎設施,致使 PWA 的這一功能在國內實施起來較有難度。

另外一方面, PWA 技術的討論和試驗也比較活躍。也許是由於其漸進式(Progressive)的思想,其中一部分特性如今已有比較普遍的應用。好比 Service Worker,當你打開瀏覽器控制檯後你會發現,像淘寶QQ音樂百度網盤 這樣耳熟能詳的產品網站已經應用了相關的技術,以優化頁面資源下載時間。

瀏覽器之間存在差別

因爲本人精力及設備有限,僅測試了 2018 年 7 月的 艾瑞數據 中排名靠前而且可以在應用市場下載使用的瀏覽器。

說明:

  1. 測試環境: Android 7.0.0
  2. 測試網站: 豆瓣
  3. 測試標準: Service Worker 僅對其離線能力(有或無)進行測試,Manifest 部分因爲各家沒有統一的表現,在這各家中 Chrome 瀏覽器的功能點最多,故以 Chrome 瀏覽器爲基準,從五個方面進行評估。
  4. 「添加到桌面」 一項中,「手動」 指的是在瀏覽器中能夠找到添加到桌面的入口並將網頁手動添加到桌面;「提示」 指的是進入頁面時,瀏覽器提示用戶將應用添加到桌面,雖然以後確認添加這一過程仍需手動點擊,但免去了尋找添加入口的冗餘操做步驟。
  5. 「地址欄自動隱藏」 指的是從桌面點擊進入應用以後的全過程,瀏覽器會根據 Manifest 配置決定是否隱藏地址欄。有些瀏覽器會有其它觸發隱藏地址欄的設定,但這種觸發跟用戶操做相關(如上滑和下滑),而不會在全過程隱藏地址欄。
瀏覽器名稱 離線能力 添加到桌面 桌面圖標及名稱與配置一致 歡迎屏幕 地址欄自動隱藏 後臺駐留以應用形式顯示
Chrome(68.0.3440.70) 支持 提示 + 手動
QQ 瀏覽器(8.8.0.4420) 不支持 手動
UC 瀏覽器(12.1.0.990) 支持 手動
360 瀏覽器(8.2.0.128) 支持 提示 + 手動
百度瀏覽器(7.18.20.0) 支持 提示 + 手動
三星瀏覽器(7.2.10.33) 支持 手動
搜狗瀏覽器(5.15.15) 支持 手動

從上表能夠看出,國內主要瀏覽器廠商對於 Service Worker 的離線能力支持度仍是很高的,也許是由於這些瀏覽器基於 Chromium 內核進行開發。QQ 瀏覽器因爲使用的是 X5 內核,表現上與 Chromium 內核存在差別。至於 Manifest,其功能點可能更多的屬於軟件自己實現而不是內核的問題,各家的表現差別較大。

小結

PWA 雖然不是完美的最終解決方案,而且目前在國內實現其全部特性尚有難度,但其中包含的不少技術,如對緩存的精細控制、消息推送等已經填補了以前 Web 生態的空白,瑕不掩瑜或許是對其最準確的評價。對我來講,至少 「漸進式」 的思想足以讓我相信並期待着 PWA 乃至整個 Web App 的美好將來。Make Web App great again!

相關連接

PWA 網址導航

國內部分 PWA

工具

改造案例

參考

相關文章
相關標籤/搜索