前端應該瞭解的PWA

瀟湘待雨發表於2018-05-09

一、傳統web 應用

當前web應用在移動時代並沒有達到其在桌面裝置上流行的程度,下面有張圖來對比與原生應用之間的差別。
前端應該瞭解的PWA
究其原因,無外乎下面不可避免的幾點:

  • 移動裝置網路限制-不可忽略的載入時間
  • web應用依賴於瀏覽器作為入口
  • 體驗與原生的差距

假如能解決以上的幾點,對web app 來說會有多大的提升可以想象。

二、PWA是什麼

PWA 全稱Progressive Web Apps(漸進式Web應用程式),旨在使用現有的web技術提供使用者更優的使用體驗。 基本要求

  • 可靠(Reliable) 即使在不穩定的網路環境下,也能瞬間載入並展現
  • 快速響應(Fast) 快速響應,並且有平滑的動畫響應使用者的操作
  • 粘性(Engaging) 像裝置上的原生應用,具有沉浸式的使用者體驗,使用者可以新增到桌面

PWA 本身強調漸進式,並不要求一次性達到安全、效能和體驗上的所有要求,開發者可以通過 PWA Checklist 檢視現有的特徵。

除以上的基準要求外,還應該包括以下特性:

  • 漸進式 - 適用於所有瀏覽器,因為它是以漸進式增強作為宗旨開發的
  • 連線無關性 - 能夠藉助 Service Worker 在離線或者網路較差的情況下正常訪問
  • 類似應用 - 由於是在 App Shell 模型基礎上開發,因為應具有 Native App 的互動和導航,給使用者 Native App 的體驗
  • 持續更新 - 始終是最新的,無版本和更新問題
  • 安全 - 通過 HTTPS 協議提供服務,防止窺探和確保內容不被篡改
  • 可索引 - 應用清單檔案和 Service Worker 可以讓搜尋引擎索引到,從而將其識別為『應用』
  • 粘性 - 通過推送離線通知等,可以讓使用者迴流
  • 可安裝 - 使用者可以新增常用的 webapp 到桌面,免去去應用商店下載的麻煩
  • 可連結 - 通過連結即可分享內容,無需下載安裝

看起來有點眼花繚亂,這又是一個新的飛起的輪子嗎?這裡重申一下,PWA背後不是一種新的技術,而是集合當前多種web技術的一種集合。分別利用各自的功能來完成漸進式的整體需求。下面就沿著前面提出的問題分別瞭解一下相關技術

三、技術組成

由以下幾種技術構成:

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

其中Service Worker是PWA技術的關鍵,它們可以讓app滿足上面的三基準。其他技術則是錦上添花,讓app更加的強大。

3.1 service worker背景

離線快取背景

針對網頁的體驗,從前到後都做了很多努力,極力去降低響應時間,這裡就不表述多樣的技術手段。 另一個方向的就是快取,減少與伺服器非必要的互動,不過對於離線的情況下瀏覽器快取就無力了, 這樣離線快取的需求就出現了。

離線快取的歷程

web應用在離線快取發展的過程中也不是一簇而就的,經歷了逐漸完善的過程。
初期的解決方案是AppCache 然而,事實證明這是一個失敗的嘗試,缺陷太多,已經被廢棄了。具體可以檢視Application Cache is a douchebag 但是方向還是正確的,那就繼續孜孜不倦的探索。

workers

持久化先放一邊,來談談另一個問題 基於瀏覽器中的 javaScript 單執行緒的現實逐漸不能滿足現代web需求的現狀,例如耗時的計算,使用者的互動顯然會受影響。 為了將這些耗時操作從主執行緒中解放出來,早期W3C新增了一個Web Worker 的 API,可以脫離主執行緒單獨執行,並且可以與主執行緒互動。 不過Web Worker是臨時性的依賴於建立頁面 ,不能滿足我們持久化的需求。 衝著這個目標,下面就比較容易解決了,搞個能持久存在的就行了。 在Web Worker的基礎上,W3C新增了service worker來滿足我們持久化的需求。 其生命週期與頁面無關,關聯頁面未關閉時,它也可以退出,沒有關聯頁面時,它也可以啟動 功能

Service Worker雖然滿足了離線快取來,其功能可不僅僅侷限於此。 可以提供

  • 豐富的離線體驗,
  • 週期的後臺同步,
  • 訊息推送通知,
  • 攔截和處理網路請求,
  • 管理資源快取 這些正好也是PWA的目的,所以說Service Worker是PWA的關鍵技術。

前提條件

Service Worker 出於安全性和其實現原理,在使用的時候有一定的前提條件。

  • 由於 Service Worker 要求 HTTPS 的環境
    當然一般瀏覽器允許除錯 Service Worker 的時候 host 為 localhost 或者 127.0.0.1
  • Service Worker 的快取機制是依賴 Cache API (略過)
  • 依賴 HTML5 fetch API(略過)
  • 依賴 Promise 實現
    由上可知,不是所有的瀏覽器都支援的,支援情況大概如下:
前端應該瞭解的PWA iOS 內的所有的瀏覽器都基於 safari,所以iOS要在11.3以上 IE是放棄支援了,不過Edge好歹支援了。

3.2 Cache

Cache是Service Worker衍生出來的API,配合Service Worker實現對資源請求的快取。 不過cache並不直接快取字串,而是直接快取資源請求(css、js、html等)。
cache也是key-value形式,一般來說key就是request,value就是response

  • caches.open(cacheName) 開啟一個cache
  • caches是global物件,返回一個帶有cache返回值的Promise
  • cache.keys() 遍歷cache中所有鍵,得到value的集合
  • cache.match(Request|url) 在cache中匹配傳入的request,返回Promise;
  • cache.matchAll只有第一個引數與match不同,需要一個request的陣列,當然返回的結果也是response的陣列
  • cache.add(Request|url) 並不是單純的add,因為傳入的是request或者url,在cache.add內部會自動去呼叫fetch取回request的請求結果,然後才是把response存入cache;
  • cache.addAll類似,通常在sw install的時候用cache.addAll把所有需要快取的檔案都請求一遍
  • cache.put(Request, Response) 這個相當於cache.add的第二步,即fetch到response後存入cache
  • cache.delete(Request|url) 刪除快取

3.3 註冊Service Worker

註冊即宣告sw檔案的位置,顯然應該在主js中引入。大概如下:

//基於promise
function registerServiceWorker(){
    // 註冊service worker
    return navigator.serviceWorker.register('./sw1.js').then(registration => {
        console.log('註冊成功');
        // 返回
        return registration;
    })
    .catch(err => {
        console.error('註冊失敗', err);
    });
}
window.onload = function () {
    //是否支援
    if (!('serviceWorker' in navigator)) {
        return;
    }
    registerServiceWorker()
}
複製程式碼

3.4 生命週期

Service worker 有一個獨立於web 頁面的生命週期。 如果在網站上安裝 serice worker ,你需要註冊,註冊後瀏覽器會在後檯安裝 service worker。然後進入下面的不同階段。 啟用之後,service worker 將控制所有的頁面,納入它的範圍,不過第一次在頁面註冊 service worker 時不會控制頁面,直到它再次載入。 service worker 生效之後,它會處於下面兩種狀態之一:

  • service worker 終止來節省記憶體,
  • 頁面發起網路請求後,它將處理請求獲取和訊息事件。

由上圖看知,分為這麼幾個階段:

  • Installing
    發生在 Service Worker 註冊之後,表示開始安裝,觸發 install 事件回撥指定一些靜態資源進行離線快取
  • Installed Service Worker 已經完成了安裝,並且等待其他的 Service Worker 執行緒被關閉。
  • Activating 在這個狀態下沒有被其他的 Service Worker 控制的客戶端,允許當前的 worker 完成安裝
  • Activated
    在這個狀態會處理 activate 事件回撥 (提供了更新快取策略的機會)。並可以處理功能性的事件 fetch (請求)、sync (後臺同步)、push (推送)
  • Redundant 被替換,即被銷燬

瞭解宣告週期其實是為了我們在不同時間段去監聽事件來完成相應操作。對PWA來說主要兩個事件。

  • install 事件回撥:

event.waitUntil():傳入一個 Promise 為引數,等到該 Promise 為 resolve 狀態為止。 self.skipWaiting():self 是當前 context 的 global 變數,執行該方法表示強制當前處在 waiting 狀態的 Service Worker 進入 activate 狀態。

  • activate 回撥:

event.waitUntil():傳入一個 Promise 為引數,等到該 Promise 為 resolve 狀態為止。 self.clients.claim():在 activate 事件回撥中執行該方法表示取得頁面的控制權, 這樣之後開啟頁面都會使用版本更新的快取。舊的 Service Worker 指令碼不再控制著頁面,之後會被停止。

const CURCACHE = 'CURCACHE_test_1'
const RUNTIME = 'runtime';
const CURCACHE_URLS = [
    './',
    '/asset/sw.jpg',
    'index.js'
]
self.addEventListener('install',e=>{
    e.waitUntil(
      //儲存快取路徑對應的資源
        caches.open(CURCACHE).then(cache=>{
            cache.addAll(CURCACHE_URLS)
        }).then(
            self.skipWaiting()
        )
    )
})
 
 
   
  //代理請求,使用快取,請求傳送之前
  self.addEventListener('fetch', e => {
    e.respondWith(
      //快取是否匹配 
      caches.match(e.request).then(function(response) {
        if (response != null) {
          //命中快取返回快取,結束請求
          return response
        }
        //未命中快取,正常請求
        return fetch(e.request.url)
      })
    )
  });
複製程式碼

更新service worker service worker 更新步驟如下:

  • 更新 service worker 的檔案
    網頁開啟時伺服器會進行對比,保持最新
  • 新的 service worker 啟動install
  • 當前頁面生效的依然是老的service worker,新的 service worker 會進入 “waiting” 狀態。
  • 頁面關閉之後,老的 service worker 會被幹掉,新的 servicer worker 接管頁面
  • 新的 service worker 生效後會觸發 activate 事件。
const CURCACHE = 'precache_test_1'
//假設上個版本的key為precache_test_2 反正不等於CURCACHE
self.addEventListener('activate', e => {
  e.waitUntil(
      //遍歷當前快取keys
      caches.keys().then(cacheNames=>{
        return Promise.all(
          cacheNames.map(function(cacheName) {
            //是否等於當前key,保留自己
            if (cacheName !== CURCACHE) {
              return caches.delete(cacheName);
            }
          })
    )}).then(() => self.clients.claim())
 )
}) 
複製程式碼

這樣一個簡單的service worker離線快取完成了。控制檯可以看到,來源是service worker

前端應該瞭解的PWA 關閉網路之後再次訪問,可以同樣得到上面的結果,並且sw.js請求未能拿到,但是不影響,舊的檔案依然在,這裡證明了每次都回去對比sw檔案以確保更新 前端應該瞭解的PWA 到這裡,離線快取就實現了。

四、新增到主螢幕

允許將站點新增至主螢幕,是 PWA 提供的一項重要功能。這樣就不用再依賴於瀏覽器作為平臺,符合移動端的使用者習慣。

manifest.json

需要 manifest.json 檔案去配置應用的圖示、名稱等基本資訊如下:

{
    //被提示安裝應用時出現的文字
    "name": "PQJ-PWA",
    //新增至主螢幕後的文字
    "short_name":"PQJ",
    "description": "測試demo",
    //新增之後,啟動地址
    "start_url": "/index.html",
    //圖示資訊
    "icons": {
      "128": "/asset/sw.jpg"
    },
    "developer": {
      "name": "pqj",
      "url": ""
    },
    "display": "standalone",
    "background_color": "#287fc5",
    "theme_color": "#fff",
    "permissions": {
        "desktop-notification": {
          "description": "Needed for creating system notifications."
        }
      }
}  
複製程式碼

然後以如下方式在html中引入

<link rel="manifest" href="/mainfest.json" />
複製程式碼

這樣完成之後,移動端安卓使用chrome(親測),首次訪問時會提示是否允許安裝到主螢幕,以應用icon的形式出現。 圖片和文字即由配置決定。

五、訊息通知

訊息通知也是使用service worker的通知功能進行的,允許伺服器想使用者發生通知,而非使用者主動請求才去響應某些行為。
正常的通知邏輯需要伺服器來參與實現,這次展示只實現功能。

  • 首先申請通知許可權
  • 註冊service worker
  • 處理邏輯,傳送通知
function getPermission(){
    return new Promise((resolve, reject) => {
        //許可權獲取
        const permissionPromise = Notification.requestPermission(result => {
            resolve(result);
        });
    }).then(result => {
            //判斷條件
            if (result === 'granted') {
                execute();
            }
            else {
                console.log('no permission');
            }
        });
} 
複製程式碼

傳送通知

function execute() {
    // 允許之後執行
    registerServiceWorker().then(registration => {
        // 通知
        registration.showNotification('Hello World!');
    });
}  
複製程式碼

結束語

參考文件

lavas.baidu.com/doc
developer.mozilla.org/zh-CN/Apps/…

至此,本文介紹就結束了,更多請參考例項雖然PWA目前來看,面對的限制還很多,但是也可以看出web組織在更好的提升web應用方向上做的努力。正如一直提到的那句話,未來可期。 目前國內百度這方面做的比較成熟,新浪微博已經有了pwa 測試版。

相關文章