深入理解 PWA

webkong發表於2018-12-05

深入理解 PWA

What is pwa?

Progressive Web App, 簡稱 PWA,是提升 Web App 的體驗的一種新方法,能給使用者原生應用的體驗。

PWA 能做到原生應用的體驗不是靠特指某一項技術,而是經過應用一些新技術進行改進,在安全、效能和體驗三個方面都有很大提升,PWA 本質上是 Web App,藉助一些新技術也具備了 Native App 的一些特性,兼具 Web App 和 Native App 的優點。

技術依賴:

  • Service Worker
  • Web storage(IndexedDB, Caches)
  • Fetch
  • Promises

PWA advantages

PWA應用應該是:

  • discoverable 可發現, 可以被識別為應用程式,通過搜尋引擎容易找到
  • installable 可安裝,可用於移動裝置,新增到使用者主螢幕
  • linkable 可連線 通過URL共享,無需複雜安裝
  • network independent 網路獨立 可工作在離線或者低速的網路環境
  • progressive 漸進增強 適用於所有使用者,支援的瀏覽器可以體驗更好,不支援的瀏覽器訪問不會郵到影響
  • re-engageable 再參與 通過提醒,讓使用者容易進行參與
  • responsive 響應式 適合任何形式的裝置
  • safe 安全 內容傳遞機制可以防止監聽,並保證內容不被篡改

Progressive web app advantages. To find out how to implement PWAs, consult the guides listed in the below section.

Progressive(漸進性)

強調是漸進式的,改造過程中可以逐步進行,降低站點的改造成本,新技術支援程度不完整,跟著新技術逐步進化。 PWA 涉及到從安全、效能和體驗等方面的優化,可以考慮以下步驟:

  • 第一步,應該是安全,將全站 HTTPS 化,因為這是 PWA 的基礎,沒有 HTTPS,就沒有 Service Worker
  • 第二步,應該是 Service Worker 來提升基礎效能,離線提供靜態檔案,把使用者首屏體驗提升上來
  • 第三步,App Manifest,這一步可以和第二步同時進行
  • 後續,再考慮其他的特性,離線訊息推送等

支援程度/覆蓋率

image

image

image

image

image

image

image

image

image

Service Worker

Service Worker,是一個瀏覽器和network之間的代理,解決的是如何快取頁面的資產和如果在離線狀態下仍然正常工作的問題。獨立於當前網頁程式,有自己獨立的 worker context,沒有對於DOM的訪問許可權,與傳統的API不同,它是非阻塞的,並基於promise方法在就緒時返回結果。它不但只是離線能力,還有訊息通知、新增桌面圖示等功能。

前提條件

  • HTTPS, 由於 Service Worker 要求 HTTPS 的環境,我們可以藉助 github page 進行學習除錯。一般瀏覽器允許除錯 Service Worker 的時候 host 為 localhost。
  • Service Worker 的快取機制是依賴 Cache API實現的
  • 依賴 HTML5 fetch API
  • 依賴 Promise 實現

Lifecycle

A more detailed introduction to The Service Worker Lifecycle

A service worker goes through three steps in its lifecycle:

  • Registration 註冊
  • Installation 安裝
  • Activation 啟用

1. 註冊

在install Server Worker之前,要在主程式JavaScript程式碼裡面註冊它,註冊是為了告訴瀏覽器我們的Servic e Worker檔案是哪個,然後在後臺,Service Worker就開始安裝啟用。

註冊程式碼可以放到html檔案的<script></script>標籤中,也可以單獨放到main.js檔案在引入html檔案中。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
  .then(function(registration) {
    console.log('Registration successful, scope is:', registration.scope);
  })
  .catch(function(error) {
    console.log('Service worker registration failed, error:', error);
  });
}
複製程式碼

程式碼中,先檢測是瀏覽器是不是支援Service Worker,如果支援,就用navigator.serviceWorker.register註冊,如果成功,就會在promise的 .then 裡面得到registration.

service-worker.js檔案就是我們要編寫Service Worker功能的檔案。

註冊時,還可以指定可選引數scope,scope是Service Worker 可以以訪問到的作用域,或者說是目錄。

navigator.serviceWorker.register('/service-worker.js', {
  scope: '/app/'
});
複製程式碼

程式碼中指定作用域是/app/,意思就是說,Service Workder 可以控制的path是類似於app /app/home/ /app/abbout/等內部目錄,而不能訪問 / '/images'等 /app更上一次層的path。

如果Service Worker 已經安裝了,再次註冊會返回當前活動的registration物件。

chrome瀏覽器已經很好的支援了Service Worker的debug功能,可在瀏覽器輸入chrome://inspect/#service-workers檢視是否註冊成功了。 或者在控制檯的application選項檢視。

2.安裝

install事件繫結在Service Worker檔案中,當安裝成功後,install事件就會被觸發。 一般我們會在install事件裡面進行快取的處理,用到之前提到的Cahce API,它是一個Service Worker上的全域性物件[5],可以快取網路相應的資源,並根據他們的請求生成key,這個API和瀏覽器標準的快取工作原理相似,但是隻是針對自己的scope域的,快取會一直存在,知道手動清楚或者重新整理。

var cacheName = 'cachev1'
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(
        [
          '/css/bootstrap.css',
          '/css/main.css',
          '/js/bootstrap.min.js',
          '/js/jquery.min.js',
          '/offline.html'
        ]
      );
    })
  );
});
複製程式碼
  1. 新增install的監聽器,並用event.waitUntil()來確保,Service Worker不會在waitUntil()執行完成之前安裝完成。
  2. 使用caches.open()建立一個cachev1的新快取,返回一個快取的promise物件,當它resolved時候,我們在then方法裡面用caches.addAll來新增想要快取的列表,列表是一個陣列,裡面的URL是相對於origin的。
  3. 如果promise被rejected,安裝失敗,我們並沒有catch,所以並不會做任何事情,也可以修改程式碼,加上重新註冊的程式碼。
  4. 當安裝完成時,Service Worker就會啟用成功。

3. 啟用

當 Service Worker 安裝完成後並進入啟用狀態,會觸發 activate 事件。通過監聽 activate 事件你可以做一些預處理,如對舊版本的更新、對無用快取的清理等。

Service Worker 如何更新呢?

service-worker.js控制著頁面資源和請求的快取,如果 js 內容有更新,當訪問網站頁面時瀏覽器獲取了新的檔案,逐位元組比對js 檔案發現不同時它會認為有更新啟動 更新演算法,於是會安裝新的檔案並觸發 install 事件。但是此時已經處於啟用狀態的舊的 Service Worker 還在執行,新的 Service Worker 完成安裝後會進入 waiting 狀態。直到所有已開啟的頁面都關閉,舊的 Service Worker 自動停止,新的 Service Worker 才會在接下來重新開啟的頁面裡生效。

如果希望在有了新版本時,所有的頁面都得到及時自動更新怎麼辦呢?可以在 install 事件中執行 self.skipWaiting() 方法跳過 waiting 狀態,然後會直接進入 activate 階段。接著在 activate 事件發生時,通過執行 self.clients.claim() 方法,更新所有客戶端上的 Service Worker。

// 安裝階段跳過等待,直接進入 active
self.addEventListener('install', function (event) {
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', function (event) {
    event.waitUntil(
        Promise.all([
            // 更新客戶端
            self.clients.claim(),

            // 清理舊版本
            caches.keys().then(function (cacheList) {
                return Promise.all(
                    cacheList.map(function (cacheName) {
                        if (cacheName !== 'cachev1') {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
        ])
    );
});
複製程式碼

當js 檔案可能會因為瀏覽器快取問題,當檔案有了變化時,瀏覽器裡還是舊的檔案。這會導致更新得不到響應。如遇到該問題,可嘗試這麼做:在 Web Server 上新增對該檔案的過濾規則,不快取或設定較短的有效期。

或者手動呼叫update()來更新

navigator.serviceWorker.register('/service-worker.js').then(reg => {
  // sometime later…
  reg.update();
});
複製程式碼

可以結合localStorage來使用,不必每次載入更新

var version = 'v1';

navigator.serviceWorker.register('/service-worker.js').then(function (reg) {
    if (localStorage.getItem('sw_version') !== version) {
        reg.update().then(function () {
            localStorage.setItem('sw_version', version)
        });
    }
});
複製程式碼

示意圖

image

每個狀態都會有ing,進行態。

image

Web Storage

選擇正確的儲存機制對於本地裝置儲存和基於雲的伺服器儲存都非常重要。 良好的儲存引擎可確保以可靠的方式儲存資訊,並減少頻寬和提升響應能力。正確的儲存快取策略是實現離線移動網頁體驗的核心構建基塊。

儲存的類別,儲存的持久化,瀏覽器支援情況等原因,如何更高效的儲存是我們討論的重點。

資料

Web Storage Overview

Using Cache API

Offine Storage for PWA

Best Practices for Using IndexedDB

Inspect and Manage Storage, Databases, and Caches

cache API和 IndexedDB

針對於離線儲存資料,建議可以有:

基本原理

上面的兩個API都是非同步的(IndexedDB是基於事件的,而Cache API是基於Promise的)。他們可以與web Workers windows service workers一起使用。IndexedDB基本可以在所有瀏覽器環境使用(參看上面的CanIUse),Service Wokers和Cahce API的支援情況,可以通過上面的圖看到,已經支援Chrome,Firefox,Opera。IndexedDB的Promise包裝器隱藏了IndexedDB庫自帶的一些強大但同時非常複雜的machinery(例如:事務處理 transactions,架構版本schema versioning)。IndexedDB將支援observers,這個特性可以輕鬆實現標籤之間的同步。

對於PWA,我們可以快取靜態資源,從而使用 Cache API 編寫的應用 Application Shell(JS/CSS/HTML 檔案),並從 IndexedDB 填充離線頁面資料。

對於Web Storage(LocalStorage/SessionStorage)是同步的,不支援 Web worker執行緒,並且有大小和型別(僅限字串)的限制。

新增到桌面

允許將站點新增至主螢幕,是 PWA 提供的一項重要功能。雖然目前部分瀏覽器已經支援向主螢幕新增網頁快捷方式以方便使用者快速開啟站點,但是 PWA 新增到主螢幕的不僅僅是一個網頁快捷方式,它將提供更多的功能,讓 PWA 具有更加原生的體驗。

PWA 新增至桌面的功能實現依賴於 manifest.json

為了實現 PWA 應用新增至桌面的功能,除了要求站點支援 HTTPS 之外,還需要準備 manifest.json 檔案去配置應用的圖示、名稱等資訊。舉個例子,一個基本的 manifest.json 應包含如下資訊:

{
    "name": "Easyify Docs",
    "short_name": "Easyify Docs",
    "start_url": "/",
    "theme_color": "#FFDF00",
    "background_color": "#FFDF00",
    "display":"standalone",
    "description": "A compilation tools for FE, built with webpack4.x, compile faster and smart, make work easier.",
    "icons": [
      {
        "src": "./_assets/icons/32.png",
        "sizes": "32x32",
        "type": "image/png"
      }
    ],
    ...
  }

複製程式碼

使用 link 標籤將 manifest.json 部署到 PWA 站點 HTML 頁面的頭部,如下所示:

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

引數解釋:

name: {string} 應用名稱,用於安裝橫幅、啟動畫面顯示
short_name: {string} 應用短名稱,用於主螢幕顯示
icons: {Array.<ImageObject>} 應用圖示列表
    src: {string} 圖示 url
    type {string=} 圖示的 mime 型別,非必填項,該欄位可讓瀏覽器快速忽略掉不支援的圖示型別
    sizes {string} 圖示尺寸,格式為widthxheight,寬高數值以 css 的 px 為單位。如果需要填寫多個尺寸,則使用空格進行間隔,如"48x48 96x96 128x128"
start_url: {string=} 應用啟動地址
scope: {string} 作用域
    // scope 應遵循如下規則:

    //如果沒有在 manifest 中設定 scope,則預設的作用域為 manifest.json 所在資料夾;
    //scope 可以設定為 ../ 或者更高層級的路徑來擴大PWA的作用域;
    //start_url 必須在作用域範圍內;
    //如果 start_url 為相對地址,其根路徑受 scope 所影響;
    //如果 start_url 為絕對地址(以 / 開頭),則該地址將永遠以 / 作為根地址;
background_color: {Color} css色值 可以指定啟動畫面的背景顏色。
display: {string} 顯示型別
    //fullscreen	應用的顯示介面將佔滿整個螢幕
    //standalone	瀏覽器相關UI(如導航欄、工具欄等)將會被隱藏
    //minimal-ui	顯示形式與standalone類似,瀏覽器相關UI會最小化為一個按鈕,不同瀏覽器在實現上略有不同
    //browser	瀏覽器模式,與普通網頁在瀏覽器中開啟的顯示一致
orientation: string 應用顯示方向
    //orientation屬性的值有以下幾種:
    //landscape-primary
    //landscape-secondary
    //landscape
    //portrait-primary
    //portrait-secondary
    //portrait
    //natural
    //any
theme_color: {Color} // css色值theme_color 屬性可以指定 PWA 的主題顏色。可以通過該屬性來控制瀏覽器 UI 的顏色。比如 PWA 啟動畫面上狀態列、內容頁中狀態列、位址列的顏色,會被 theme_color 所影響。
related_applications: Array.<AppInfo> 關聯應用列表 可以引導使用者下載原生應用
    platform: {string} 應用平臺
    id: {string} 應用id
複製程式碼

Push Notifications

我們都是通知就是在我們裝置上彈出的訊息。通知可以是本地觸發的,也可以是伺服器推送的,而且我們的應用當時並沒有執行。訊息推送可以使App的更新提醒,也可能是我們感興趣的內容。

當我們的web可以實現push的時候,web的體驗就裡Native APP更近一步了。

Push Notifications 由兩個API構成:

  • Notifications API 用來顯示系統的通知
  • Push API 用來處理Server下發的push訊息

這兩個API都是建立在在Service Worker API基礎上的,Service Worker 在後臺響應推送訊息時間,並把他們傳遞給應用。

Notification

獲取許可

在建立通知之前,應該先獲取使用者的許可:

// main.js
Notification.requestPermission(function(status) {
    console.log('Notification permission status:', status);
    //status 會有三個取值default granted denied 分別代表: 預設值(每次訪問頁面都詢問)、 允許、拒絕
});
複製程式碼

新增通知

獲取到使用者的許可之後,就可以通過 showNotification()方法來限制主應用程式的通知。

// main.js
function displayNotification() {
  if (Notification.permission == 'granted') {
    navigator.serviceWorker.getRegistration().then(function(reg) {
      reg.showNotification('Hello world!');
    });
  }
}
複製程式碼

要注意showNotification,在Service Woker註冊物件上呼叫該方法。將在活動Service Worker上建立通知,以便監聽與通知互動觸發的事件。

showNotification方法有可選項引數options,用於配置通知。

// main.js
function displayNotification() {
  if (Notification.permission == 'granted') {
    navigator.serviceWorker.getRegistration().then(function(reg) {
      var options = {
        body: 'Here is a notification body!', // 對通知新增描述
        icon: 'images/example.png', // 新增一個icon影象
        vibrate: [100, 50, 100], // 指定通知的電話振動模式,手機將振動100ms,暫停50ms,再次振動100ms
        data: {
          dateOfArrival: Date.now(),
          primaryKey: 1
        }, // 給通知新增自定義資料,當監聽到通知的時候,可以捕獲到這些資料,方便使用。
        actions: [
          {action: 'explore', title: 'Explore this new world',
            icon: 'images/checkmark.png'},
          {action: 'close', title: 'Close notification',
            icon: 'images/xmark.png'},
        ] // 自定義的操作
     };
      reg.showNotification('Hello world!', options);
    });
  }
}
複製程式碼

監聽事件

使用者收到通知之後,通過對通知的操作,就會觸發監聽的Notifications的相關事件,比如在關閉通知的時候就會有notificationclose事件。

// service-worker.js
self.addEventListener('notificationclick', function(e) {
  var notification = e.notification;
  var primaryKey = notification.data.primaryKey;
  var action = e.action;

  if (action === 'close') {
    notification.close();
  } else {
    clients.openWindow('http://www.example.com');
    notification.close();
  }
});
複製程式碼

Push

通知操作要結合push,才能實現與使用者的互動,主動通知、提醒使用者

Push service

每個瀏覽器都有一個push service(推送服務),當使用者授權當前網站的push許可權的時候,就可以將當前網站訂閱到瀏覽器的push service。這就會建立一個訂約物件,其中包含推送服務的endpoint和公鑰(keys)。當下發push訊息的時候,就會傳送到endpoint這個URL,並用公鑰進行加密,push service就會傳送到正確的客戶端。

推送服務如何知道將訊息傳送到哪個客戶端?端點URL包含唯一識別符號。此識別符號用於路由您傳送到正確裝置的訊息,並在瀏覽器處理時標識應處理請求的Service Worker。

推送通知和Service Worker是匹配工作的,所以要求推送通知也必須是HTTPS,這就確保了伺服器和push service之間通訊是安全的,並且從push service到使用者也是安全的。

但是,HTTPS不能確保push service本身是安全的。我們必須確保從伺服器傳送到客戶端的資料不會被任何第三方篡改或直接檢查。所以必須加密伺服器上的訊息。

整個傳送接收展示的過程

在客戶端:

1.訂閱推送服務

2.將訂閱物件傳送到伺服器

在伺服器:

1.生成給使用者下發的資料

2.使用使用者的公鑰加密資料

3.使用加密資料的有效負載將資料傳送的endpoint URL

訊息將路由到使用者的裝置。喚醒瀏覽器,找到正確的Service Worker並呼叫推送事件。

1.在推送事件中接收訊息資料(如果有)

2.在推送事件中執行自定義邏輯

3.顯示通知

處理推送事件

當支援推送訊息的瀏覽器收到訊息時,它會向Service Worker傳送一個push事件。我們可以在Service Worker中建立一個 push事件監聽器來處理訊息:

// service-worker.js

self.addEventListener('push', function(e) {
  var options = {
    body: 'This notification was generated from a push!',
    icon: 'images/example.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: '2'
    },
    actions: [
      {action: 'explore', title: 'Explore this new world',
        icon: 'images/checkmark.png'},
      {action: 'close', title: 'Close',
        icon: 'images/xmark.png'},
    ]
  };
  e.waitUntil(
    self.registration.showNotification('Hello world!', options)
  );
});
複製程式碼

與之前不同的地方就是,這裡監聽的是push事件,之前是notification事件,並且,這裡用了event.waitUntil方法來延長push事件的生命週期,到showNotification非同步操作執行完成。

訂閱推送通知

在傳送推送訊息之前,我們必須首先訂閱推送服務。訂閱返回訂閱物件或者是一個subscription。它是整個過程中很關鍵一個部分,我們才能知道push傳送到哪裡。

// main.js
//檢查是否訂閱了
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (reg) {
                console.log('Service Worker Registered!', reg);

                reg.pushManager.getSubscription().then(function (sub) {
                    if (sub === null) {
                        // Update UI to ask user to register for Push
                        console.log('Not subscribed to push service!');
                    } else {
                        // We have a subscription, update the database
                        console.log('Subscription object: ', sub);
                    }
                });
            })
            .catch(function (err) {
                console.log('Service Worker registration failed: ', err);
            });
    }

    function subscribeUser() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.ready.then(function (reg) {

                reg.pushManager.subscribe({
                    userVisibleOnly: true
                }).then(function (sub) {
                    console.log('Endpoint URL: ', sub.endpoint);
                }).catch(function (e) {
                    if (Notification.permission === 'denied') {
                        console.warn('Permission for notifications was denied');
                    } else {
                        console.error('Unable to subscribe to push', e);
                    }
                });
            })
        }
    }

複製程式碼

Web推送協議

Web Push協議是傳送發往瀏覽器的推送訊息的正式標準。它描述瞭如何建立推送訊息,加密推送訊息並將其傳送到推送訊息傳遞平臺的結構和流程。該協議抽象出使用者具有哪個訊息傳遞平臺和瀏覽器的細節。

Web Push協議很複雜,但我們不需要了解所有細節。瀏覽器自動負責使用推送服務訂閱使用者。作為開發人員,我們的工作是獲取訂閱令牌,提取URL並向那裡傳送訊息。

{"endpoint":"https://fcm.googleapis.com/fcm/send/dpH5lCsTSSM:APA91bHqjZxM0VImWWqDRN7U0a3AycjUf4O-byuxb_wJsKRaKvV_iKw56s16ekq6FUqoCF7k2nICUpd8fHPxVTgqLunFeVeB9lLCQZyohyAztTH8ZQL9WCxKpA6dvTG_TUIhQUFq_n",
"keys": {
    "p256dh":"BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
    "auth":"4vQK-SvRAN5eo-8ASlrwA=="
    }
}
複製程式碼

通常用VSPID身份驗證來識別身份。 直接上一個例子

//main.js
    var endpoint;
    var key;
    var authSecret;

    // We need to convert the VAPID key to a base64 string when we subscribe
    function urlBase64ToUint8Array(base64String) {
      const padding = '='.repeat((4 - base64String.length % 4) % 4);
      const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

      const rawData = window.atob(base64);
      const outputArray = new Uint8Array(rawData.length);

      for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
      }
      return outputArray;
    }

    function determineAppServerKey() {
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      return urlBase64ToUint8Array(vapidPublicKey);
    }

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('sw.js').then(function (registration) {

        return registration.pushManager.getSubscription()
          .then(function (subscription) {

            if (subscription) {
              // We already have a subscription, let's not add them again
              return;
            }

            return registration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: determineAppServerKey()
              })
              .then(function (subscription) {

                var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                authSecret = rawAuthSecret ?
                  btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';

                endpoint = subscription.endpoint;

                return fetch('http://localhost:3111/register', {
                  method: 'post',
                  headers: new Headers({
                    'content-type': 'application/json'
                  }),
                  body: JSON.stringify({
                    endpoint: subscription.endpoint,
                    key: key,
                    authSecret: authSecret,
                  }),
                })

              });
          });
      }).catch(function (err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err);
      });
    }

複製程式碼
// server.js

const webpush = require('web-push');
const express = require('express');
var bodyParser = require('body-parser');
var path = require('path');
const app = express();

// Express setup
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({     // to support URL-encoded bodies
  extended: true
}));

function saveRegistrationDetails(endpoint, key, authSecret) {
  // Save the users details in a DB
}

webpush.setVapidDetails(
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);

// Send a message
app.post('/sendMessage', function (req, res) {

  var endpoint = req.body.endpoint;
  var authSecret = req.body.authSecret;
  var key = req.body.key;

  const pushSubscription = {
    endpoint: req.body.endpoint,
    keys: {
      auth: authSecret,
      p256dh: key
    }
  };

  var body = 'Breaking News: Nose picking ban for Manila police';
  var iconUrl = 'https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/master/chapter-6/push-notifications/public/images/homescreen.png';

  webpush.sendNotification(pushSubscription,
    JSON.stringify({
      msg: body,
      url: 'http://localhost:3111/article?id=1',
      icon: iconUrl,
      type: 'actionMessage'
    }))
    .then(result => {
      console.log(result);
      res.sendStatus(201);
    })
    .catch(err => {
      console.log(err);
    });
});

// Register the user
app.post('/register', function (req, res) {

  var endpoint = req.body.endpoint;
  var authSecret = req.body.authSecret;
  var key = req.body.key;

  // Store the users registration details
  saveRegistrationDetails(endpoint, key, authSecret);

  const pushSubscription = {
    endpoint: req.body.endpoint,
    keys: {
      auth: authSecret,
      p256dh: key
    }
  };

  var body = 'Thank you for registering';
  var iconUrl = '/images/homescreen.png';

  webpush.sendNotification(pushSubscription,
    JSON.stringify({
      msg: body,
      url: 'https://localhost:3111',
      icon: iconUrl,
      type: 'register'
    }))
    .then(result => {
      console.log(result);
      res.sendStatus(201);
    })
    .catch(err => {
      console.log(err);
    });

});

// The server
app.listen(3111, function () {
  console.log('Example app listening on port 3111!')
});

複製程式碼

後面再詳細說整個push過程。 也可以看先Google給出的教程描述developers.google.com/web/ilt/pwa…

參考

Progressive Web Apps Training

App Shell

Service Workers: an Introduction

MDN Progressive web apps

MDN WorkerGlobalScope [1]

The Service Worker Lifecycle

W3C IndexedDB API 3.0

Introduction to Push Notifications