Angular 6 PWA 開發踩坑

更新中javascript

提示:在測試程序的時候儘可能使用Chrome的隱身模式,確保 Service Worker 不會從之前的殘留狀態中讀取數據!!css

PWA在Angular 6 工程上的初始化:

sudo ng new pwa新建工程以後,在工程的根目錄上運行sudo ng add @angular/pwa,此時就會自動添加Service Worker文件,Manifest.json文件和各類不一樣尺寸的icon文件。 Angular PWA中文網傳送門html

PWA程序的更新

app.component.ts中引入import { SwUpdate } from '@angular/service-worker';來加載SW的更新模塊,每次PWA程序有更新均可以在這裏使用SwUpdate模塊獲取更新,並使用以下代碼可實現程序的更新操做:java

export class AppComponent {
  update: boolean;
  constructor(updates: SwUpdate, private data: DataService) {
    updates.available.subscribe( event => {
      this.update = true;
      updates.activateUpdate().then(() =>
        document.location.reload()
      );
      }
    );
  }
  title = 'PWA';
}
複製代碼

SwUpdate文檔傳送門node

而後在html中使用一個*ngIf來判斷是否更新,(是則顯示text,不是則不顯示):web

<span *ngIf="update">There's an update associated with your progressive web application!</span>
複製代碼

每次更新了程序都要從新build production程序,在根目錄上運行sudo ng build --prod,而後進入cd dist/PWA,最後運行http-server -o在服務器上運行更新後的程序。npm

因爲 ng serveService Worker 無效,因此必須用一個獨立的 HTTP 服務器在本地測試項目。 可使用任何 HTTP 服務器,我使用的是來自 npm 中的 http-server 包。固然也能夠自定義端口以防止port衝突:json

http-server -p 8080 -c-1 dist/<project-name>
複製代碼

當使用http-server打開服務器後,卻沒法正常打開網頁的時候,我曾遇到過ERR_INVALID_REDIRECT這樣的問題致使沒法正常顯示網頁。更換http-server版本就能夠解決這個問題: npm install -g http-server@0.9.0後端

注意: 若是想按期更新PWA,也就是使用interval建立一個週期輪詢方法,須要先讓應用註冊Aervice worker的進程進入穩定狀態,再讓它開始執行輪詢的過程,若是不斷輪詢更新(好比調用 interval())將阻止應用程序達到穩定態,也就永遠不會往瀏覽器中註冊 ServiceWorker 腳本。另外:應用中所執行的各類輪詢都會阻止它達到穩定態api

constructor(appRef: ApplicationRef, updates: SwUpdate) {
        // Allow the app to stabilize first, before starting polling for updates with `interval()`.
        const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
        const everySixHours$ = interval(6 * 60 * 60 * 1000);
        const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
        everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate());
  }
複製代碼

因此對於自動更新模塊的使用總結:

constructor(appRef: ApplicationRef, updates: SwUpdate, private data: DataService) {
    const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
    const everySixHours$ = interval(6 * 1000);
    const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
    everySixHoursOnceAppIsStable$.subscribe(() => {
      updates.checkForUpdate();
      // console.log('check update in Service Worker');
    });
    updates.available.subscribe(event => {
      console.log('gotta new version here', event.available);
      updates.activateUpdate().then(() => document.location.reload());
    });
  }
複製代碼

每6秒檢測一次更新版本,若是沒有updates.activateUpdate().then(() => document.location.reload());則只是在檢測到新版本時候提醒並不刷新並更新程序。 測試的時候須要從新ng build --prod而後http-server -p 8080 -c-1 dist/PWA從新運行http服務器,這時候在原來的頁面上的console上就會發現出現了新版本的提醒。

(其實每次運行build命令都會出現版本更新不管是否更改代碼,當應用的一個新的構建發佈時,Service Worker 就把它看作此應用的一個新版本,版本是由 ngsw.json 文件的內容決定的,包含了全部已知內容的哈希值。 若是任何一個被緩存的文件發生了變化,則該文件的哈希也將在ngsw.json中隨之變化,從而致使 Angular Service Worker 將這個活動文件的集合視爲一個新版本)

如何緩存文件以及API的地址以及其餘項目?

全在nsgw-config.json文件中定義PWA緩存,好比想緩存google的Montserrat字體和API地址,該文件中全部的代碼形式都是glob格式,也就是:

  • ' ** ' 匹配 0 到多段路徑
  • ' * ' 匹配 0 個或更多個除 / 以外的字符
  • ? 匹配除 / 以外的一個字符
  • ! 前綴表示該模式是反的,也就是說只包含與該模式不匹配的文件

好比:

  • /**/*.html 指定全部 HTML 文件
  • /*.html 僅指定根目錄下的 HTML 文件
  • !/**/*.map 排除了全部源碼映射文件

在實際代碼中這樣作:

<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
複製代碼

在已經被建立的assetGroups中添加:

"urls": [
    "https://fonts.googleapis.com/**"
  ]
複製代碼

AssetGroup遵循的TypeScript接口規則爲:

interface AssetGroup {
  name: string;
  installMode?: 'prefetch' | 'lazy';
  // prefetch 告訴 Angular Service Worker 在緩存當前版本的應用時要獲取每個列出的資源。 這是個帶寬密集型的模式,但能夠確保這些資源在請求時可用,即便瀏覽器正處於離線狀態
  // lazy 不會預先緩存任何資源。相反,Angular Service Worker 只會緩存它收到請求的資源。 這是一種按需緩存模式。永遠不會請求的資源也永遠不會被緩存。 這對於像爲不一樣分辨率提供的圖片之類的資源頗有用,那樣 Service Worker 就只會爲特定的屏幕和設備方向緩存正確的資源。
  updateMode?: 'prefetch' | 'lazy';
  // prefetch 會告訴 Service Worker 當即下載並緩存更新過的資源
  // lazy 告訴 Service Worker 不要緩存這些資源,而是先把它們看做未被請求的,等到它們再次被請求時才進行更新。 
  lazy 這個 updateMode 只有在 installMode 也一樣是 lazy 時纔有效。
  resources: {
    files?: string[];
    /** @deprecated As of v6 `versionedFiles` and `files` options have the same behavior. Use `files` instead. */
    versionedFiles?: string[];
    urls?: string[];
  };
}
複製代碼

在下方建立dataGroups緩存API地址:

"dataGroups": [
    {
      "name": "jokes-api",
      "urls": [
        "https://api.chucknorris.io/jokes/random"
      ],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 20,
        "maxAge": "1h",
        "timeout": "5s"
      }
    }
  ]
複製代碼

dataGroups的配置遵循下面的接口:

export interface DataGroup {
  name: string;
  urls: string[];
  version?: number;
  cacheConfig: {
    maxSize: number;
    maxAge: string;
    timeout?: string;
    strategy?: 'freshness' | 'performance';
  };
}
複製代碼

其中的緩存設置中的幾個項目分別是:

  • "strategy" :
    1. performance,默認值,爲儘快給出響應而優化。若是緩存中存在某個資源,則使用這個緩存版本。 它容許資源有必定的陳舊性(取決於 maxAge)以換取更好的性能。適用於那些不常常改變的資源,例如用戶頭像。
    2. freshness 爲數據的即時性而優化,優先從網絡獲取請求的數據。只有當網絡超時時,請求才會根據 timeout 的設置回退到緩存中。這對於那些頻繁變化的資源頗有用,例如帳戶餘額。
  • "maxSize" : (必需)緩存的最大條目數或響應數。開放式緩存能夠無限增加,並最終超過存儲配額,建議適時清理。
  • "maxAge" : d(必需)maxAge 參數表示在響應因失效而要清除以前容許在緩存中留存的時間。(d:天數,h:小時數,m:分鐘數,s:秒數,u:微秒數)
  • "timeout" : 這個表示持續時間的字符串用於指定網絡超時時間。 若是配置了它,Angular Service Worker 在開始使用緩存以前就會先等待網絡給出響應,這個等待時間就是網絡超時時間。

PWA Push Notification

測試push notification API的功能沒法在隱身模式下測試

  1. 首先生成VAPID (Voluntary Application Server Identification),使用node的webpush庫直接引入依賴:npm install web-push -g,而後建立VAPID key: web-push generate-vapid-keys --json。得到相似以下的VAPID:
{
    "publicKey":"BApAO10ISTLAR1bWho_6f4yL5-5z2RWHgnkqzG7SB81WdcsLkDdxrc1iWwHZ49trIUFekIEFGyBjomxjuKDZGc8",
    "privateKey":"7y1-NPiG_igcck_iIJ5sidurBa7ghC4Py0MTQPOFLGM"
}
複製代碼
  1. 在component中咱們須要引入Service Worker的push模塊(SwPush)來支持咱們的代碼,同時也要引入Service服務來獲取網絡請求。
subscribeToNotifications() {
        this.swPush.requestSubscription({
            serverPublicKey: this.VAPID_PUBLIC_KEY
        })  // 瀏覽器彈出消息請求,若是請求贊成會得到一個Promise
        .then(sub => this.newsletterService.addPushSubscriber(sub).subscribe())  // 這裏會得到一個PushSubscription object
        .catch(err => console.error("Could not subscribe to notifications", err));
    }
複製代碼

PushSubscription object:

{
      "endpoint": "https://fcm.googleapis.com/fcm/send/cbx2QC6AGbY:APA91bEjTzUxaBU7j-YN7ReiXV-MD-bmk2pGsp9ZVq4Jj0yuBOhFRrUS9pjz5FMnIvUenVqNpALTh5Hng7HRQpcUNQMFblTLTF7aw-yu1dGqhBOJ-U3IBfnw3hz9hq-TJ4K5f9fHLvjY",
      "expirationTime": null,
      "keys": {
        "p256dh": "BOXYnlKnMkzlMc6xlIjD8OmqVh-YqswZdut2M7zoAspl1UkFeQgSLYZ7eKqKcx6xMsGK7aAguQbcG9FMmlDrDIA=",
        "auth": "if-YFywyb4g-bFB1hO9WMw=="
      }
    }
複製代碼
  1. 在後端的Node服務器上能夠設置payload來自定義Notification內容:
const notificationPayload = {
    "notification": {
        "title": "Angular News",
        "body": "Newsletter Available!",
        "icon": "assets/main-page-logo-small-hat.png",
        "vibrate": [100, 50, 100],
        "data": {
            "dateOfArrival": Date.now(),
            "primaryKey": 1
        },
        "actions": [{
            "action": "explore",
            "title": "Go to the site"
        }]
    }
};
複製代碼
  1. 而後使用webpush模塊推送通知:
Promise.all(allSubscriptions.map(sub => webpush.sendNotification(
    sub, JSON.stringify(notificationPayload) )))
    .then(() => res.status(200).json({message: 'Newsletter sent successfully.'}))
    .catch(err => {
        console.error("Error sending notification, reason: ", err);
        res.sendStatus(500);
    });
複製代碼

給Service worker添加新的listener

本身定義的話須要本身創建兩個新的文件:sw-custom.jssw-master.js

  • sw-custom.js裏面定義咱們想添加的listener,好比:
(function () {
 'use strict';
  self.addEventListener('notificationclick', (event) => {
    event.notification.close();
    console.log('notification details: ', event.notification);
  });
}());
複製代碼
  • sw-master.js用於將sw-custom.jsngsw-worker.js兩個Service Worker文件結合,不丟失原有功能:
importScripts('./ngsw-worker.js');
importScripts('./sw-custom.js');
複製代碼

文件創建好了以後須要讓Angular在初始化渲染的時候可以將咱們自定義的文件包含進去,因此在angular.json文件中的assets部分添加"src/sw-master.js"。最後在app.module.ts中註冊service worker的地方註冊咱們的新service worker文件:

ServiceWorkerModule.register('/sw-master.js', { enabled: environment.production })
複製代碼

煩人的一點:由於Angular封裝好的 ngsw-worker.js 只能在 ng build --prod以後建立的dist文件夾中,因此不build就無法使用service worker。這就是爲何測試PWA的Service Worker功能時候無法在ng serve上運行。因此若是咱們在測試本身的功能的時候,爲了不麻煩能夠將ServiceWorkerModule.register('/sw-master.js', { enabled: environment.production })註釋掉,換成ServiceWorkerModule.register('sw-custom.js'),這時候咱們無法使用原來的那些Angular封裝好的功能可是能夠測試咱們本身的listener。

解決方法: 在environment.tsenvironment.prod.ts文件中添加新的環境變量,這樣就能夠在不同的環境下運行不一樣的Service Worker依賴包。

  • environment.ts中添加: serviceWorkerScript: 'sw-custom.js'
  • environment.prod.ts中添加: serviceWorkerScript: 'sw-master.js'
  • 註冊Service Worker中時候使用: ServiceWorkerModule.register(environment.serviceWorkerScript)
相關文章
相關標籤/搜索