PWA 知不知

什麼是 PWA

Progressive Web App, 簡稱 PWA,是「漸進式」提高 Web App 體驗的一種新方法,能給用戶相似原生應用的體驗。css

「高可靠,高性能,優體驗」是 PWA 慣用的形容詞,他的另一個優勢就是「漸進式」,開發者能夠對照 PWA Checklist 逐步對本身站點進行 PWA 化升級。html

PWA 的發展史

2007

蘋果前 CEO,Steve Jobs,2007 年 WWDC 上提出了爲初代 iPhone 開發應用的概念,當時所介紹的,就是 Web App——能夠從主屏直接啓動的 Web 應用。java

圖片來源 appleinsider.com

惋惜當時這個理念太過超前,並無引起太多關注,反而是後來的原生 App 應用更符合當時的市場需求,互聯網公司更願意投入人力在原生 App 的開發上,而忽略了 Web。所以原生 App 的大量出現,佔據了移動時代的主流地位,Web 彷佛就要被 App 所取代。git

2014

隨着 Web 技術的發展,時間來到 2014 年, W3C 公佈了 Service Worker 的相關草案,其生產環境在 2015 年被 Chrome 支持。隨後 PWA 加以完善,相關技術不斷升級優化,在用戶體驗和用戶保活兩方面的可發掘價值愈來愈大。程序員

2017

繼移動站點噴井式發展之焰末,原生 App 的弊端愈加明顯,對於它來說,最大的痛點即是其天生封閉的基因致使的內容沒法被索引,相對的 Web 站點可索引的優點開始凸顯,與此同時,PWA 遵循 W3C 標準開發的技術,徹底開放,可以快速地被各大瀏覽器廠商支持,市場支持度一晚上崛起。github

另一邊,App 的推廣並不順利,據調查統計,移動設備用戶 80% 的時間花費在了經常使用的 5 個應用上16年近一半的美國用戶平均每個月安裝「0」個新 App,用戶積極探索新 App 已經成爲了過去式,拉新和保活的成本愈來愈高。web

原生 App 的發展遇到了天花板,推廣也正向瓶頸一步步靠近,Web 看到了本身的機遇,PWA 以及支撐 PWA 的一系列關鍵技術應運而生。chrome

2018

2018 年對於 PWA 來講是里程碑的一年,萬衆矚目的 Apple 終於在 iOS 11.3 裏支持了 Web App Manifest,以及內置的 Safari 11.1 支持了 Service Worker編程

與此同時,全球頂級瀏覽器廠商,Google、Microsoft、Apple 已經全數宣佈支持 PWA 技術,這預示着,Web App 將會迎來全新的時代json

2019

截至當下的 PWA 支持度

依據 Can I use 的統計(20190515)

  • App Manifest 的支持度達到 58.82%
  • Service Worker 的支持度達到 90.36%
  • Notifications API 的支持度達到 76.12%
  • Push API 的支持度達到 78.35%
  • Background Sync 的支持度達到 71.35%

Service Worker 以全數「登船」。信息來源於 jakearchibald.github.io/isservicewo…

PWA 的核心

PWA 有幾個核心功能,分別是「離線,安裝,推送」

離線瀏覽

弱網或離線的狀況下依然能夠「正常訪問」甚至「秒開」,這種體驗甚至超過了 app。主要的技術點就是 Service Worker。

Service Worker

SW 相似於咱們熟知的 Web Worker,Web Worker 能夠脫離主線程,處理一些「髒累」活,幹完後經過 postMessage 向主線程彙報工做結果。因此,SW 也是脫離主線程的存在,與 Web Worker 不一樣的是,SW 具備持久化的能力。

SW 還具有有如下功能和特性:

  • 一個獨立的 worker 線程,獨立於當前網頁進程,有本身獨立的 worker context。
  • 一旦被 install,就永遠存在,除非被手動 unregister
  • 用到的時候能夠直接喚醒,不用的時候自動睡眠
  • 可編程攔截代理請求和返回,緩存文件,緩存的文件能夠被網頁進程取到(包括網絡離線狀態)
  • 離線內容開發者可控
  • 能向客戶端推送消息
  • 不能直接操做 DOM
  • 必須在 HTTPS 環境下才能工做
  • 異步實現,內部大都是經過 Promise 實現

基於以上咱們能夠看到 SW 要讓緩存作到極致優雅的偉大使命。

Service Worker 的生命週期

想要靈活的使用 SW 功能,就要充分了解他的生命週期,以及各階段的狀態。

如下是 MDN 給出的 SW 的詳細生命週期圖。

能夠看到,SW 的生命週期包含這麼幾個狀態 安裝中, 安裝後, 激活中, 激活後廢棄

  • 安裝( installing ):這個狀態發生在 Service Worker 註冊以後,表示開始安裝,觸發 install 事件回調指定一些靜態資源進行離線緩存。 install 事件回調中有兩個方法:

    • event.waitUntil():傳入一個 Promise 爲參數,等到該 Promise 爲 resolve 狀態爲止。

    • self.skipWaiting():self 是當前 context 的 global 變量,執行該方法表示強制當前處在 waiting 狀態的 Service Worker 進入 activate 狀態。

  • 安裝後( installed ):Service Worker 已經完成了安裝,而且等待其餘的 Service Worker 線程被關閉。

  • 激活( activating ):在這個狀態下沒有被其餘的 Service Worker 控制的客戶端,容許當前的 worker 完成安裝,而且清除了其餘的 worker 以及關聯的舊緩存資源,等待新的 Service Worker 線程被激活。

    activate 回調中有兩個方法:

    • event.waitUntil():傳入一個 Promise 爲參數,等到該 Promise 爲 resolve 狀態爲止。

    • self.clients.claim():在 activate 事件回調中執行該方法表示取得頁面的控制權, 這樣以後打開頁面都會使用版本更新的緩存。舊的 Service Worker 腳本再也不控制頁面,以後會被中止。

  • 激活後( activated ):在這個狀態會處理 activate 事件回調 (提供了更新緩存策略的機會)。並能夠處理功能性的事件,fetch (請求)、sync (後臺同步)和 push (推送)。

  • 廢棄狀態 ( redundant ):這個狀態表示一個 Service Worker 生命週期的結束。

    這裏特別說明一下,進入廢棄狀態的緣由可能爲這幾種:

    • 安裝 (install) 失敗

    • 激活 (activating) 失敗

    • 新版本的 Service Worker 替換了它併成功激活

Service Worker 支持的全部事件

MDN 也列出了 Service Worker 全部支持的事件:

  • install:Service Worker 安裝成功後被觸發的事件,在事件處理函數中能夠添加須要緩存的文件

  • activate:當 Service Worker 安裝完成後並進入激活狀態,會觸發 activate 事件。經過監聽 activate 事件你能夠作一些預處理,如對舊版本的更新、對無用緩存的清理等。

  • message:Service Worker 運行於獨立 context 中,沒法直接訪問當前頁面主線程的 DOM 等信息,可是經過 postMessage API,能夠實現他們之間的消息傳遞,這樣主線程就能夠接受 Service Worker 的指令操做 DOM。

Service Worker 有幾個重要的「功能性事件」,這些功能性的事件支撐和實現了 Service Worker 的特性。

  • fetch (請求):當瀏覽器在當前指定的 scope 下發起請求時,會觸發 fetch 事件,並獲得傳有 response 參數的回調函數,回調中能夠作各類代理和緩存操做。

  • push (推送):push 事件是爲推送準備的。不過首先須要瞭解一下 Notification API 和 PUSH API。經過 PUSH API,當訂閱了推送服務後,可使用推送方式喚醒 Service Worker 以響應來自系統消息傳遞服務的消息,即便用戶已經關閉了頁面。

  • sync (後臺同步):sync 事件由 background sync (後臺同步)發出。background sync 配合 Service Worker 推出的 API,用於爲 Service Worker 提供一個能夠實現註冊和監聽同步處理的方法。但它還不在 W3C Web API 標準中。在 Chrome 中這也只是一個實驗性功能,須要訪問 chrome://flags/#enable-experimental-web-platform-features ,開啓該功能,而後重啓生效。

Service Worker 的使用

有了以上 SW 的事件及 API,接下來就是實戰部分了。

先決條件
  • 瀏覽器支持,包括 Cache APIPromiseHTML5 fetch API。關於目前 SW 的瀏覽器支持狀況,後面將有介紹。
  • HTTPS,可用 127.0.0.1localhost 測試,但部署須在 https 協議下。

巧婦難爲無米之炊,以上兩個先決條件是必需要知足的。

註冊 Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
      // 註冊成功
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }, function(err) {
      // 註冊失敗 :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}
複製代碼

其實,關鍵代碼只有一行

navigator.serviceWorker.register('/sw.js', {scope: '/'})
複製代碼

注意,此處有坑

Service Worker 的註冊路徑決定了其 scope 默認做用域,如 SW 註冊文件的路徑爲 https://www.a.com/public/sw.js 時,對應默認 scope 是 /public/,其做用範圍以下

域名 是否生效
www.a.com/
www.a.com/page/
www.a.com/public/
www.a.com/public/page…
www.b.com/
www.b.com/public/

以上可看出,看成用域 scope 爲 /public/ 後,其做用範圍只限於自己和子域,父域和兄弟域皆無效,跨域就更免談了。

固然,咱們能夠經過設置 scope 來限定本身的做用域,可是!請注意,『如下寫法是錯誤的』。

navigator.serviceWorker.register('/public/sw.js', {scope: '/'})         // 錯誤寫法
navigator.serviceWorker.register('/public/sw.js', {scope: '/page'})     // 錯誤寫法
複製代碼

以上寫法均會報錯

The path of the provided scope ('/') is not under the max scope allowed ('/public/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.

因此,sw.js 文件最好放在根域名下

navigator.serviceWorker.register('/sw.js', {scope: '/page'}) 
複製代碼

當 scope 不一樣時,請求被監控狀況也有不一樣

代號 請求
r1 www.a.com/api
r2 www.a.com/page1/api
r3 www.a.com/page2/api
r4 www.a.com/static/img1.png
r5 www.b.com/api2
r6 www.b.com/static/img2.png
域名 scope 被監控請求
www.a.com/ / r1-6
www.a.com/ /page1
www.a.com/page1 / r1-6
www.a.com/page1 /page1 r1-6
www.a.com/page1 /page2

因此,scope 與被監控請求的域並無什麼關係,他只與站點域名有關

查看是否註冊成功

咱們能夠經過打開 Chrome 的 DevTools -> Application -> Service Workers 查看 SW 的註冊狀況。

看到類如 Status: #xxxx activated and is running,即說明註冊並激活成功。

也能夠經過打開 Chrome 的管理頁 chrome://inspect/#service-workers 查看

安裝 Service Worker

在受控頁面啓動註冊流程後,咱們來看看處理 install 事件的 Service Worker 腳本。

最基本的例子是,您須要爲安裝事件定義回調,並處理想要緩存的文件。

self.addEventListener('install', function(event) {
  // Perform install steps
});
複製代碼

在 install 回調的內部,咱們能夠執行如下步驟(固然也能夠啥也不幹):

  1. 打開緩存。
  2. 緩存文件。
  3. 確認全部須要的資產是否已緩存。
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});
複製代碼

此處,咱們以所需的緩存名稱調用 caches.open(),以後再調用 cache.addAll() 並傳入文件數組。 這是一個 promise 鏈(caches.open()cache.addAll())。 event.waitUntil() 方法帶有 promise 參數並使用它來判斷安裝所花費的時間,以及安裝是否成功。

若是全部文件都成功緩存,則將安裝 Service Worker。 若有任意文件沒法下載,則安裝失敗。此設計可保證 SW 啓動的正確性,但過長的資源列表也增長了安裝失敗的概率,可根據項目狀況自行定義,也可不定義。

自定義請求響應

在安裝 Service Worker 且用戶轉至其餘頁面或刷新當前頁面後,Service Worker 將開始接收 fetch 事件。下面提供了一個示例。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    //匹配緩存
    caches.match(event.request)     
      .then(function(response) {
        //命中走觀察
        if (response) {
          return response;
        }
        //未命中則透傳向網絡
        return fetch(event.request);
      }
    )
  );
});
複製代碼

若是但願連續緩存新請求,能夠經過處理 fetch 請求的響應並將其添加到緩存來實現,以下所示。

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

        /** * 經過檢查,則克隆響應。 * 這樣作的緣由在於,該響應是數據流, 所以主體只能使用一次。 * 因爲咱們想要返回能被瀏覽器使用的響應,並將其傳遞到緩存以供使用, * 所以須要克隆一份副本。咱們將一份發送給瀏覽器,另外一份則保留在緩存。 */
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
            // 只緩存成功的請求,第三方資源不緩存,固然也能夠處理緩存。
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});
複製代碼

workbox

上一部分介紹了 SW 的使用,以及自定義請求響應,實際上,fetch 的玩法有不少,但也都是大同小異。

所以,爲了使 SW 更容易使用,GoogleChrome 團隊在 Chrome Submit 2017 上首次推出的一套 Web App 靜態資源和請求結果本地存儲的解決方案 workbox。

來直接感覺下 workbox 的語法

// sw.js。 SW 的註冊不變,改變的只是 `sw.js` 的寫法
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

// 不一樣的資源使用不一樣的緩存策略,並存儲在不一樣的 storage 中
workbox.routing.registerRoute(
  /\.(?:s?html)/,
  workbox.strategies.staleWhileRevalidate({
    cacheName:'kl-main'
  })
);

workbox.routing.registerRoute(
  /\.(?:js|css)/,
  new workbox.strategies.CacheFirst({
    cacheName:'kl-static'
  })
);

workbox.routing.registerRoute(
  /http:\/\/(?:haitao\.nos\.netease\.com|haitao\.nosdn2\.127\.net)/,
  new workbox.strategies.CacheFirst({
    cacheName:'kl-cdn'
  })
);
複製代碼

一看就懂,是否是很簡單呢?

這裏有個緩存策略,簡略介紹一下。

  • Stale While Revalidate

此策略會優先匹配緩存,若是未命中,則透傳網絡,若是命中則返回緩存響應,同時在後臺更新緩存網絡響應,此策略比較安全,更像是競爭策略,誰快誰響應。

  • Network First

    網絡優先策略,若是網絡通暢,返回網絡響應並緩存,若是離線,則返回緩存響應

  • Cache First

    緩存優先策略,若是緩存匹配,返回響應,若是不匹配則透傳網絡,並緩存「有效」響應

  • Network Only

    強制網絡響應,即爲普通請求

  • Cache Only

    強制緩存響應,無匹配則返回 404

更多關於 workbox 請前往 developers.google.com/web/tools/w…

可「安裝」

PWA 另一個爆點就是「可安裝」,學名叫做「添加到主屏幕」,這歸功於一個配置文件 manifest.json,它給予開發者自定義圖標、顯示名稱、啓動方式等信息並添加至桌面的能力,同時也提供 API 方便開發者管理網絡應用安裝橫幅,讓用戶能夠方便快捷地將站點添加到主屏幕中。

支持度

當前 manifest.json 的標準仍屬於草案階段,Chrome、Firefox 和 Apple 都已經實現了這個功能或者功能的部分,微軟正努力在 Edge 瀏覽器上實現。

caniuse 中可查到 manifest 的支持度,數據顯示 92.64% 的移動瀏覽器已經達到了支持或者部分支持的程度,想必在不久之後,當規範標準經過後,manifest 的支持度能夠達到一個新高度。

配置

我以一個相對完整的 manifest.json 配置文件進行講解

<!-- 配置由於的引入 -->
<link rel="manifest" href="path-to-manifest/manifest.json">
複製代碼

配置介紹

// manifest.json
{
    /* 自定義名稱 */
    "short_name": "短名稱",
    "name": "這是一個完整名稱",
    
    /** 自定義安裝 icon * 當PWA添加到主屏幕時,瀏覽器會根據有效圖標的 sizes 字段進行選擇。 * 首先尋找與顯示密度相匹配而且尺寸調整到 48dp 屏幕密度的圖標; * 若是未找到任何圖標,則會查找與設備特性匹配度最高的圖標; * 若是匹配到的圖標路徑錯誤,將會顯示瀏覽器默認 icon。 * * 在啓動應用時,啓動畫面圖像會從圖標列表中提取最接近 128dp 的圖標進行顯示 */
    "icons": [
        {
            "src": "path-to-images/icon-96x96.png",
            "type": "image/png",
            "sizes": "96x96"
        },
        {
            "src": "path-to-images/icon-144x144.png",
            "type": "image/png",
            "sizes": "144x144"
        }
    ],
    
    /* 設置啓動網址 */
    "start_url": "index.html",
    
    /** 設置啓動背景顏色 * 完整色值 "#0000ff" * 縮寫 "#00f" * 預設色值 "blue" * rgb "rgb(0, 0, 255)" * transparent 背景色顯示爲黑色 */
    "background_color": "#0000ff",
    
    /** 設置啓動顯示類型 * fullscreen 應用的顯示界面將佔滿整個屏幕 * standalone 瀏覽器相關UI(如導航欄、工具欄等)將會被隱藏 * minimal-ui 顯示形式與standalone相似,瀏覽器相關UI會最小化爲一個按鈕,不一樣瀏覽器在實現上略有不一樣 * browser 瀏覽器模式,與普通網頁在瀏覽器中打開的顯示一致 */
    "display": "fullscreen",
    
    /** 指定頁面顯示方向 * 更多配置介紹:https://lavas.baidu.com/pwa/engage-retain-users/add-to-home-screen/improved-webapp-experience#%E6%8C%87%E5%AE%9A%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA%E6%96%B9%E5%90%91 */
    "orientation": "landscape",
    
    /* 設置主題顏色 */
    "theme_color": "#000",
    
    /** 設置做用域 * start_url 必須在做用域內 */
    "scope": "/"
    
}
複製代碼

測試

配置完以後就能夠在 Chrome 的 DevTools 中進行驗證測試了。

圖片來源於 developers.google.com

可推送消息

消息推送是 App 保活衝績效的經常使用手段,因爲 HTTP 是一個無狀態協議,推送功能在用戶關閉了瀏覽器以後便沒了辦法,這一次 PWA 賦予了 Web 這個能力。

其中便包含了兩個技術點

  • 推送 push:鏈接服務端和 SW 進行消息傳遞
  • 通知 notification:控制客戶端(瀏覽器)進行消息提示

下面咱們來解析一個通知體

// 消息體的 title
self.addEventListener('push', event => {
    const title = "Credit Card";
    const options = {
        // 主內容
        "body": "Did you make a $1,000,000 purchase at Dr. Evil...", 
        // 視覺配置,如 icon,Badge,image 等,不一樣的視覺配置展現的位置也不一樣
        // 詳情參看 https://lavas.baidu.com/pwa/engage-retain-users/notification/notification-display
        "icon": "images/ccard.png",
        // 震動設置,其中的數字以2個爲一組,分別表示震動的毫秒數,和不震動的毫秒數
        "vibrate": [200, 100, 200, 100, 200, 100, 400],
        // 鈴聲
        "sound": "path/to/sound.mp3",
        // 標籤,用於客戶端消息歸類
        "tag": "request",
        // actions,用戶操做後會將結果反饋給瀏覽器
        "actions": [
            { "action": "yes", "title": "Yes", "icon": "images/yes.png" },
            { "action": "no", "title": "No", "icon": "images/no.png" }
        ]
    }
    // 激活通知
    self.registration..showNotification(title, options);
});

self.addEventListener('notificationclick', event => {  
  // Do something with the event 
  event.notification.close();  
});

self.addEventListener('notificationclose', event => {  
  // Do something with the event 
});
複製代碼

以上的消息配置,展現的結果以下圖。

關於推送功能的更多實操不屬於本文探究的範疇,有實際需求的同窗能夠前往官網進行了解。

傳送門>>

developers.google.com/web/fundame…

lavas.baidu.com/pwa/engage-…

PWA & 小程序

有人說「PWA 是小程序的祖宗」,不無道理,PWA 對小程序確定存在必定的借鑑意義,可是否會擠壓 PWA 的市場?咱們應該放心,小程序的設計並非 Web 的替代者,而是介於原生 App 和 Web 之間的存在。

小程序更傾向於輕便及時觸手可得。既沒有原生 App 的「沉重」也沒有 Web 「遲鈍」。在此得天獨厚的基礎之上加之以「社交流量」的加持,微信小程序的存在並不是偶然。

可是,若是沒了網絡,同樣玩不轉;主流的搜索引擎並沒有法捕獲小程序的內容。因此,App、Web 和小程序是相輔相成的。

另外,筆者想表達另一個觀點

「存在即合理,合理未必長久」

在經歷過一段痛苦的微信小程序洗禮以後,咱們「欣然」接受了。奈何衆XX小程序『競相開放,爭奇鬥豔,不亦樂乎』。卻不知,我等不才,竟要爲了這區區語法之差別,徹夜沒法停歇,然,產與出相比,孰輕孰重?

唉~程序員何須爲難程序員~~

總結

關於 PWA 的技術早在 2 年前即已相對完整,只是因爲「天朝人民太過賦予」,對支持與否未發佈意見的 Apple 在天朝市場有着舉足輕重的地位,而「外圍」彷彿對 Android 機更爲推崇,因此,PWA 在國內的發展和推廣並不理想。

此時此刻,PWA 的支持度也達到了一個相對讓人滿意的水平,雖然體驗依然沒法和原生 App 相提並論,但做爲 App 短板的補丁已經是綽綽有餘。

因此,『架構師』們,能夠盤起來了。

以上就是 PWA 的相關知識點,但願對你有所幫助。

[1]. developers.google.com/web/fundame…

[2]. developers.google.com/web/fundame…

[3]. developers.google.com/web/tools/w…

[4].下一代 Web 應用模型 —— Progressive Web App

[5]. lavas.baidu.com/


首發:zwwill/blog#33

做者:木羽

轉載請標明出處

相關文章
相關標籤/搜索