終於要來補一下怎麼在 Angular 專案中,使用 Angular 自己提供的 Service Worker package

如果還不清楚 PWA & Service Worker 在幹嘛可以左轉到另一篇 PWA 的文章 了解一下大概,裡面也有闡述一些在 iOS Safari 的限制

由於我是將 Service Worker 加入既有的專案,所以可能步驟上會跟從零開始建立專案時,就順便先引入 Service Worker 有點不一樣,但應該不會是什麼大問題,因為 Angular 幾乎都把困難的事情做完了,雖然還是有些地方要自己調整


Angular Service Worker

從 Angular 5.0.0 開始,提供了 service worker 的 package 直接讓開發者使用,好處就是不需要自己刻一個 service worker 來做 Caching strategy 之外,也不需要自己去處理 compile/build 的設置,Angular 幾乎都幫你打包整理好了

Angular 設計 service worker 時的幾項準則如下

  • Caching 一個應用程式像是安裝原生的應用一樣,一個應用程式會被 Cache 為一個單位,所有包含在內的檔案會一起被更新
  • 正在被執行的應用程式會繼續執行現在的版本,他不會馬上收到新版本的更新檔案,因為有可能有相容性的問題
  • 使用者在同個頁面重新整理應用程式後,會先看到最近的 fully cached 版本,新開一個 tab 也是一樣
  • 更新會在背景執行,當更新完全結束之前,應用程式還是跑舊的版本
  • Service worker 會盡可能保留 bandwidth,只有在 Resources 被改變的時候才會被下載

@angular/pwa

使用 CLI 來引入 @angular/pwa 到專案中,Angular 會幫你建立 PWA 需要的檔案

1
ng add @angular/pwa --project <project-name>

如果你的專案架構比較龐大,有不止一個 application,去 angular.json 中找到你要加入的專案對應的 <project-name>,然後要確認他的 projectType 是屬於 application

打完 command line 後會看到以下訊息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Skipping installation: Package already installed
CREATE projects/<your-project-path>/ngsw-config.json (592 bytes)
CREATE projects/<your-project-path>/src/manifest.webmanifest (1073 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-128x128.png (1253 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-144x144.png (1394 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-152x152.png (1427 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-192x192.png (1790 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-384x384.png (3557 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-512x512.png (5008 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-72x72.png (792 bytes)
CREATE projects/<your-project-path>/src/assets/icons/icon-96x96.png (958 bytes)
UPDATE angular.json (80032 bytes)
UPDATE package.json (4868 bytes)
UPDATE projects/<your-project-path>/src/app/app.module.ts (6556 bytes)
UPDATE projects/<your-project-path>/src/index.html (1802 bytes)

裝好 PWA 套件, @angular/service-worker 也會隨之被加入到你的專案中

package.json

package.json 中可以看到下列這行

1
2
// package.json
"@angular/service-worker": "8.2.0",

angular.json

angular.json 找到你的的 project name,在裡面的 configurations 中應該會看到 production 的屬性中 serviceWorker 是 true,然後也定義了 ngswConfigPath 的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// angular.json
"<project-name>": {
"root": "...",
"sourceRoot": "...",
...
"configurations": {
"prod-mock": {
...
},
"production": {
...
"serviceWorker": true,
"ngswConfigPath": "projects/gfn/mall/ngsw-config.json"
},
...

ngsw-config.json 是管理 service worker cache 策略的設定檔

app.module.ts

app.module.ts 中會自動幫你 import Service Worker module,在 @NgModule decorator 內也會自動幫你加上 module 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ServiceWorkerModule } from '@angular/service-worker';
...
@NgModule({
declarations: [AppComponent],
imports: [
...
...
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
],
providers: [
...

})

index.html

自動幫你在專案 src/ 底下的 index.html 中引入 manifest path

1
2
3
4
<head>
...
<link rel="manifest" href="manifest.json">
</head>

BTW,我把 manifest.webmanifest 的副檔名改成 .json

production build

需要注意的是 Angular service worker 只有在 production build 的時候才會被啟用

1
2
3
ng build --prod
or
npm run <your-script-commend> // which includes prod build

在 production build 的過程中,Angular 就會生成跟 service worker 有關的 ngsw.jsonngsw-worker.jsdist/ (output folder) 底下

ngsw.json

ngsw.json 這個檔案事實上是根據 ngsw-config.json 生成出來的,作用是告訴 service worker 怎麼去 cache,跟要 cache 哪些 resources

所以 Angular 是把 service worker script 跟 cache strategy 分開管理,cache strategy 留給開發者做調整,service worker 的實作與如何採用策略,則留給 Angular 幫你處理

ngsw-worker.js

ngsw-worker.js 就是 Angular service worker 本身,也就是我們加入的 ServiceWorkerModule

app.module.ts 加入的 ServiceWorkerModule.register() 實際上是去 trigger 在 ngsw-worker.jsnavigation.serviceWorker.register()

這個檔案沒意外在每次 build 都會長一樣,除非更新的 Angular 版本中有了新的 Angular Service Worker 版本

serve

另外還要注意的點是 Service Worker 只有應用程式運行在 localhost or https 下才會被啟用

如果是想自己加密,我的另一篇文章:在 Angular 專案下將本地端網站加密裡面有說到用 ng serve 可以加自己 gen 出來的 certificate & key 沒錯,但不幸的是 Service Worker 無法被用 ng serve 的方式啟用,

所以只能選擇用 npm package 的 http-server 或是 python http-server 之類的指令,到你的專案 dist/ 的位置,開始執行應用程式在你的 localhost

1
http-server -p <port-number> // 8080 or other number

到 Google dev tool 的左側 Application 的 section 中,就能成功看到 Service worker 在運行了,沒意外的話你的 Manifest 應該也是正常運作的

manifest 不存在?

這一步可能不是大家都需要做,但如果你做完 prod build 然後開始執行你的 Angular 應用程式,在 Google 的 dev tool 卻發現 Service worker 沒有被啟動,然後 dev tool 告訴你他沒有偵測到 manifest file

那就很有可能你的 manifest.json 在 compile/build 打包完後,沒有在想要的 output folder 下面,也就是 dist/<your-project-path>/ 下並不存在 manifest.json

可以到 dist/ 底下檢查看看,如果如上述所說,那就需要移駕到我另一個文章: 在 Angular 專案加入 Manifest 中看如何設置好 build path

Cache Strategy

這邊要講的就是如何制定 Angular service worker 的 Cache 策略,也是 PWA 中最重要的環節

不管哪種策略,Service worker 都是在背景執行載入資料

當應用程式第一次啟動時,Service worker 就會同時被註冊,而當使用者下次再啟動應用程式時(refresh or revisit),Service worker 這時才開始發揮它的作用,他會根據策略選擇要不要 intercept HTTP requests,從 Cache 拿資料,或是直接發出 HTTP requests 更新 Cache 的資料

Overview

只要在 ngsw-config.json 中調整參數就可以了

ngsw-config.json 中的 assetGroups 就是用來指定你要 cache 哪些 resources

預設的設定是建立一個 Group 取名為 “app”,將在 build output root folder dist/ 下的index.htmlfavicon.ico 、所有 .css 與所有 .js 列為一個 Group,然後設 installMode 為 prefetch 的狀態

另外也建立了一個 Group,取名為 “assets”,使用 installMode 為 lazy,updateMode 為 prefetch

你也可以自己分類,Group 的用意就是將使用同一種策略的檔案集合在一起,利用 regular expression 來指定檔案們,可以到這個網站來簡單測試一下或是練習

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"$schema": "../../../node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/*.css",
"/*.js"
]
}
}, {
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}

assetGroup

Angular 提供了兩個 Modes: installMode & updateMode 給每個 assetGroup 使用

而在不同 Mode 中可以指定兩個 options: prefetch or lazy

installMode

這個選項決定應用程式啟動後要如何 cache 指定的 resources,是要用 prefetch or lazy

updateMode

這個選項決定當 resources 已經在 cache 時的執行方式,當 Angular 發現檔案有更動要更新時是要用 prefetch or lazy

prefetch

Service worker 會下載所有在 assetGroup 中被指定的 resources,然後盡快將它們放到 cache 中

簡單來說就是不管有沒有要用到,通通先 cache 起來,或是通通都 update

lazy

Service worker 只會在 assetGroup 中被指定的 resources 被需要時才下載他們

dataGroup

適合用來處理需要透過 API 做 request 的 resources,像是 fonts 或是 fetch 遠端的網站的資源等等

1
2
3
4
5
6
7
8
9
10
11
"dataGroups": [
{
"name": "random.org",
"urls": ["https://www.random.org/**"],
"cacheConfig": {
"maxSize": 3,
"maxAge": "7d",
"strategy": "freshness"
}
}
]

在 dataGroup 中一樣是可以自定義箇個 Group 來管理使用同樣的 cacheConfig 的 API request,也有參數供開發者做調整

Typescript interface

1
2
3
4
5
6
7
8
9
10
11
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
}

urls

放置 URLs,一樣可以用 regular expression,可是不接受 negate glob pattern ! (e.g. https/website/lib/!fileName),只能 cache non-mutating 的 request (GET & HEAD),

version

可加也可不加

控制 API 在 cache 的版本,原因是當 cache 下來的 API 版本如果更新了可能會有 backward-incompatible 的問題,是一個整數值,而預設值是 1

version provides a mechanism to indicate that the resources being cached have been updated in a backwards-incompatible way, and that the old cache entries—those from previous versions—should be discarded.

cacheConfig

定義如何 cache 這些 API requests 回傳的資料

  • maxSize: 指定最多要 cache 多少 entry or response
  • maxAge: 要存多久的時間在 cache,是一個字串型態,e.g. 1d2h3m4s
    • d: days
    • h: hours
    • m: minutes
    • s: seconds
    • u: milliseconds
  • timeout: network timeout,超過這個等待時間後,Service worker 就會放棄不會繼續等待 network 的 response
  • strategy: 提供兩種方式
    • performance(像是 Google 定義的 cache falling back to network): 預設值,如果 cache 有就從 cache 拿直到這個 cache 過期為止(maxAge),不會發出 network request,會儲存新的 entry 到 cache 直到達到 maxSize 為止;這個策略適合不常會更新的資料
    • freshness (像是 Google 定義的 network falling back to cache): 拿最新的,所以會先發出 network request,如果 timeout 則變成從 cache 拿,從 cache 拿的時候也是直到 cache 過期為止,會將新的 entry 放到 cache 直到達到 maxSize;這個策略適合常常需要更新的資料

當應用程式 offline 時,不管是 performance 還是 freshness,都會從 cache 拿資料

檢查被 cache 的檔案

到 run time configuration 的檔案,ngsw.json (會在 output folder dist/ 下),就會看到哪些檔案會被 Service worker cache 起來

所有 asset 都會有一個 hash entry 在 hash table,如果只是改變了一點東西,就算是一個字,都會有不一樣的 hash 在之後的 prod build


後記

Angular Service Worker 套件雖然引入起來方便,但會發現它不夠彈性,上述介紹的 Cache strategies 並沒有完全考慮 Google 文件中描述的所有策略,只有提供簡單的幾種選項讓我們組合,所以有時後情況變得比較複雜的話就沒辦法符合比較大型的專案需求,不過應付大部分簡單的情況很足夠了,也省去不少麻煩

References: