前言

由於組織改組的關係,沒有想到最初決定不打算往前端發展的我,最終卻還是接觸了這塊,命運的造化XD

對網頁開發不討厭,還可以說是滿喜歡的,因為可以立即看到效果,設計頁面也需要有一點美感,學生時期也有接觸過寫網頁,前後端都有(當時還是寫過氣的 php + ajax 哈哈)

現在再回來看前端技術,真的比當初看到的又更五花八門了,一直在演進,框架除了常聽到的 Angular, React,現在還有時下最潮 Vue

團隊給我工作的任務是希望我研究在 iOS Safari 上的 PWA,一研究才發現 PWA 是滿大的 Topic,且在 iOS 上有很多障礙還沒解決,稍微記錄下之前的一些筆記,實質上並不算太完整,因為目前來看 Apple 的 WebKit Team 對於完全支援 PWA 還有很多路要走

由於公司專案是採用 Angular,所以 PWA 系列的筆記只針對 Angular 框架,因為其他的框架我也沒有深入研究,不過各個框架是因應不同專案需求,也沒有絕對的好壞之分

Angular 生態系的學習曲線,聽前端工程師的同學說是比其他框架還陡峭QQ,實際體驗後確實如此…

因為內容有一點多,這篇先給一個概略,下幾篇會寫多點在 iOS 上的實作面與一些碰到的問題

什麼是 PWA

由 Google 的 Chrome team 在 2016 Google I/O 大會上提出,但實際上 Web App 這個點子的發想卻是 Apple 早在 2007 年首支 iPhone 的發表大會上由 Steve Jobs 展示

PWA 全名是 Progressive Web Application,翻譯是”漸進式網頁”

Application 的稱呼是希望他能像 native app 在手機上的行為一樣,讓使用者覺得自己不是瀏覽一個網頁,而是在使用一個應用程式的感覺

網頁應用程式的優點比起原生的應用,除了跨平台外,還有不受應用程式商店的限制,有新版本只要在伺服器端發布後,使用者開啟網頁連進來就能享受新版本了,對開發者與使用者都很方便,也免去了應用程式上架的審核 (比如 Apple 的嚴格審核機制= =)

  • Progressive - 漸進式體驗,環境越完善就提供越多服務,若是有所限制也能提供目前環境下最優的服務
  • Responsive - 畫面與元件需要適應不同尺寸並因應不同的輸入方式給予相對的回饋
  • Native APP like - 提供類原生模式的使用者介面與操作方式,還有資料處理的行為
  • Fast update - 不需要透過應用程式平台來發佈軟體,開發者能快速提供最新版本的軟體給使用者
  • Secure - 使用 TLS 加密,所以 PWA 只能運行在有 HTTPS 支援的 Server 上
  • Installable - 透過 Add to home screen 的方式讓 PWA 像原生應用程式一樣能從桌面啟動
  • Linkable - 只要分享 URL 即可分享 PWA

PWA 並不是一種技術,正確來講他是好多種網頁技術的結合後,透過這些技術來成為一個合格的漸進式網頁

Google 對此有很好的一段解釋

A PWA is not an API or a technology, but it is a web development approach that uses a combination of tools and technologies already available to create targeted, ideal user experiences. It shows how to use service workers, APIs, and an application shell architecture for meaningful offline experiences, fast first load, and easy user re-engagement upon repeat visits.

Lighthouse

至於什麼樣才叫合格 PWA? 其實並沒有一個絕對,大多都是共識,大部分人也都是 Follow Google web team 的文件,目前可以參考 Google 推出的工具 Lighthouse 來檢驗

Lighthouse 本身的功能並不只限於檢查 PWA 的標準,他還提供了很多面向的 Performance 數據來讓開發者檢視

Google Training

Google 針對 PWA 有很好的線上文件加上 YouTube 課程影片,能讓第一次聽到 PWA 的人快速理解背後的概念跟如何實現

Google PWA Training

Chrome 也是目前所有瀏覽器中支援 PWA 標準最多的,Google 團隊正持續開發更多功能,期許讓 PWA 的生態圈更大更穩定

Service Worker

Service Worker 是 PWA 最重要的技術之一,也是為何 PWA 稱為 Progressive 漸進式的關鍵

他是一支獨立運行在 browser 後台的 Script (separate from main thread),有自己的生命週期,可以讓 Web APP 在 Loading 時比原生 APP 快速且在離線時也能正常瀏覽

Service workers enable applications to control network requests, cache those requests to improve performance, and provide offline access to cached content. Depends on Fetch/Cache web API to make app work offline.

它的最大功用是能加速網頁重複瀏覽的載入速度,還有支援離線瀏覽,像是一個在 Web App, Browser 與 Network 之間的 proxy server

  • 被設計成 Fully asynchronous,所以不能使用 synchronous XHR 和 Local Storage,只能使用 Cache Storage
  • 不能直接使用 DOM object
    • postMessage() method to send data and a “message” event listener to receive data.
  • 可以接收 server 的 push notification (not support in Safari)
  • 必須在 HTTPS 或 localhost 的環境下執行,因為 Service Worker 可以 intercept network request / modify responses,會有中間人攻擊(Man in the Middle)的可能
  • 如果 Service Worker 不再被使用,會進入閒置狀態, 在下一次使用時會重啟狀態
    • 要保存狀態的話 Google 建議使用 IndexedDB databases
  • event driven, 使用大量的 promise object

Life cycle

Service Worker 生命週期完全獨立於 Web App (Browser) 外,避免製造殭屍 worker,要小心控制好新舊 Service Worker 的產生與回收

If you decide to create a service worker yourself from scratch, do consider putting in a “kill-switch” – a way for the service worker to completely invalidate the cache and reinstall itself.

Install

當 Service Worker 註冊 (register) 完成,開啟 Web App 的時候 install event 就會被觸發,可以在這個 event handler 內做

  • Set up cache
  • Add static assets into cache

Activate

當新的 Service Worker 安裝 (install) 完且上一次的 Service Worker 也沒有再被使用的狀態下,新的 Service Worker 就會被 activated,這時候 activate event 被觸發,我們可以在這個 event handler 內刪除 cached data 或是移除整個 cache

當 Service Worker 啟動後可以開始控制所有在 Service Worker 範圍內載入的頁面,監聽所有事件,但如果有頁面是在 Service Worker 啟用前被載入,那麼這些頁面不會受到 Service Worker 的控制,要立即控制這些頁面要用到 clients.claim()

Idle

當啟動後沒有使用,Service Worker 就會進入閒置狀態等待被使用

Fetch/Cache

當有 fetch 請求發生,就會觸發 fetch event,在這個 event handler,我們可以開始實作 cache strategy,可以選擇讓 Service Worker 從 Cache 取出資料或是真的對 network 發出 fetch 來載入資料

Caching

既然是類似 Proxy server 的概念,那麼 Service Worker 的設計當然就跟暫存 Cache 機制有關

You are responsible for implementing how your script (service worker) handles updates to the cache. All updates to items in the cache must be explicitly requested; items will not expire and must be deleted

由於暫存的空間 (offline storage) 並不是無限大甚至還有點小 (iOS Safari 最多只提供 50MB 的容量,但桌面版的 Safari 卻不受限制),選擇要儲存哪些檔案就很重要

大概有以下幾種策略

Cache only

When to use : 只有 static assets 需要被使用

Network only

When to use: 沒有任何離線的資料需求, 都需要網路,像是 analytics ping, non-GET request

Cache falling back to network

最普遍與常用的策略

先從 Cache 拿資料,失敗的話再從 Network 拿

When to use: offline first Web app

Network falling back to cache

先從 Network 拿資料,失敗的話再從 Cache 拿

When to use: 需要高更新頻率的網站

缺點是如果網路狀況不好,就要等到網路失敗後才從 Cache 拿資料,造成不好的 user experience

Cache then network

先從 Cache 拿資料,然後再從 Network 拿,會先呈現從 Cache 拿到的資料,接著再更新成從Netowork 拿到的最新資料

When to use: 需要高更新頻率的網站

Google web developer 頁面對於如何針對不同情況下做暫存設計,有更完整的文件參考: Caching Files With Service Worker

Application shell

Google 提出要實現 PWA 應該 Base 的基本架構為何

An application shell (or app shell) architecture is one way to build a Progressive Web App that reliably and instantly loads on your users’ screens, similar to what you see in native applications.

將應用程式的 infrastructure, UI 與資料做分離,利用 Service Worker 將 infra & UI cache 起來,就不需要重複載入了,也可以選擇將部分需要網路的資料先 cache 起來

Shell - minimal HTML, CSS, and JavaScript required to power the UI and when cached offline, can ensure instant, reliably good performance to users on repeat visits.

提供給使用者完善的離線體驗,第一次存取網頁時就要把 Static asset & UI cache 起來

有效率且最小化資料的存取次數,決定甚麼是常用或必要的資料,優先 cache 它們

在這樣的設計架構下我們需要考量甚麼東西該放到 App Shell

  • 比如當網頁一啟動,最先看到的是哪些元件? 這些元件也許最適合 cache 起來

The app shell should ideally:

  • Load fast
  • Use as little data as possible
  • Use static assets from a local cache
  • Separate content from navigation
  • Retrieve and display page-specific content (HTML, JSON, etc.)
  • Optionally, cache dynamic content

Angular 框架下的 View/Component/Service 就符合 App shell 將 Data 與 UI 分離的概念

iOS Safari

目前只有 Google 自家的 Chrome 達到最好的支援,最慢的就是 Apple 的 Safari,2018/3/30 的新版本才終於支援 Service Worker,而真正完全支援是到 iOS 12.2 (What’s new on iOS 12.2 for Progressive Web Apps)

Is Service Work ready?

PWA Support Detector

不支援的清單

有空的話這清單應該會慢慢更新

Web App manifest

對於一個應用程式而言最重要就是要有一個 manifest,方便集中做管理與針對不同應用情況做不同設定

Chrome 是 fully support 但是 iOS Safari 還沒完全支援 manifest 的存在,目前是 In Development 的狀態

參考這個頁面追蹤進度:Apple Webkit Feature Status - Manifest

在桌面上產生 App icon 的 Bug 追蹤 thread

2020/08/22: start_url 安全疑慮

在 7 月底的時候,在 W3C Github 一個曾經討論過但被遺忘的安全疑問又浮出來,關於 start_url 是否應該開放給 user 自行編輯, Github issue thread

原因在於,start_url 在 manifest 中可以由開發者自行指定,決定 web app 從 home screen 被 launch 起來時第一個到的地方,而 web app 這時候是全螢幕的狀態,user 有可能在不知情的情況下透過加入 query string 的方式被追蹤

It’s conceivable that the start_url could be crafted to indicate that the application was launched from outside the browser (e.g., “start_url”: “index.html?launcher=homescreen”). This can be useful for analytics and possibly other customizations. However, it is also conceivable that developers could encode strings into the start_url that uniquely identify the user (e.g., a server assigned UUID). This is fingerprinting/privacy sensitive information that the user might not be aware of

後來的討論結果趨向這不只是 PWA specific issue,任何建立 bookmark 的行為都有可能觸發此安全疑慮

Launching screen

iOS 不支援從 manifest 設定應用程式的啟動畫面

啟動畫面是指從桌面上打開 PWA 後,進入到主畫面之前的一小段畫面,又稱為 splash screen

以 Twitter 的 PWA 為示意,splash screen 就如下圖,這個畫面結束後就會開啟 Twitter 首頁

Installation prompt API

beforeinstallprompt 這個 API 只有在 Chrome 支援而已,目的是讓網頁可以自動跳出一個提示視窗建議使用者把網頁加到桌面上(像是一個應用程式一樣),但是 iOS Safari 並沒有提供這個 API 讓我們直接使用

相關的 bug 追蹤

w3c 關於 manifest 的 github issue 有針對 installation prompt API 做討論,有興趣可以看一下,要留著還是要移除的兩派說法

Push notification

iOS 不支援網頁應用程式像原生應中一樣有推播提醒的功能

Github尚有大神寫了個簡單的功能測試網頁來測試瀏覽器是否支持這個功能

相關 bug 追蹤

Background Sync

利用 Service Worker 的機制在後台去同步儲存一些重要的資料,比如一些 user 的設定或是訊息,一但網路不穩斷線時,仍然可以 restore 這些重要的狀態回來,雖然 Safari 已經支援 Service Worker,但還沒支援 Background sync

相關 bug 追蹤

In-App Browser

PWA 在 iOS Safari 如果在 full-screen 的模式下,開啟外部網頁連結都會進到 In-App browser 的畫面,上面會有一個 address bar

以 Twitter 的 PWA 開啟外部連結為例

2020/08/22 更新

從 iOS 13.4 開始在 PWA In-APP Browser 中提供了 “Open in Safari” 的選項在底部

WebRTC

WebRTC(Web Real Time Communication),網頁即時通訊,支援瀏覽器進行即時視訊的開源 API

目前主流的瀏覽器都支援 WebRTC 了,但 iOS Safari 跟桌面版的 Safari 是不一樣的,在 iOS 上使用 Safari 是支援 WebRTC 的,但是其他家第三方的瀏覽器在 iOS 上沒有辦法支援 WebRTC (2019的事實)

原因是因為在 iOS 上的第三方瀏覽器都是以 WkWebView 的形式在 iOS 上執行,PWA 如果以 full screen 的方式在 iOS 上執行也是以 WkWebView 的形式執行

而 WkWebView 有一個 issue 是關於 getUserMedia() 的錯誤,導致 WebRTC 沒辦法順利運行,除非使用 WebRTC 進行即時串流的時候,不要獲取使用者的語音或視訊,而是讓使用者單純接收媒體資料的話就沒事(但這樣對於多數即時串流的互動應用來說毫無意義)

相關 Bug 追蹤

好消息是 2020 的最新進度是這個問題似乎在 iOS 13.4 beta 1 (Build 17E5223h) 被修復了

詳情可以爬 bug thread 大家的留言,裡面有一位一直 對 Apple WebKit Team push 這個 issue 的人叫 Thomas Steiner,目前是在德國 Google 的工程師,似乎就是在 Chrome Team 吧,可以搜尋他,也有 PWA 在 iOS 的相關文章可以看

2020/11/24 更新

超級好消息!
官方正式宣告 Safari 在 iOS 14.3 beta 的 WKWebView 中修正了 getUserMedia() API!

navigator.mediaDevices.getUserMedia can now be exposed to WKWebView applications. navigator.mediaDevices.getUserMedia is automatically exposed if the embedding application is able to natively capture either audio or video. Please refer to Apple documentation to meet these requirements. Access to camera and microphone is gated by a user prompt similar to Safari and SafariViewController prompts. We hope to extend WKWebView APIs to allow applications to further control their camera and microphone management in future releases.

Cache Limit

前面提到 iOS Safari 對於 Service Worker 的 cache storage 大概只有 50MB,更精確來說是 52MB

另個廣泛推薦的選擇是使用 IndexedDB 來儲存,iOS Safari 的上限有到 500MB

網上其他開發者有觀察到如果 PWA 過了幾週 (網上評估大概是兩週) 都沒有使用的話,iOS 裝置就會自動清理掉 PWA 儲存的資料

也有開發者發現同一個網域下的 PWA, 以不同名字命名被新增到桌面後,Service Worker 雖然會註冊兩次但卻是 share 同個 cache storage

2020/09/04 更新

iOS 14 beta 開始,PWA 開始跟 safari browser 共享同個 Cache storage,也就是說如果使用者做了一些行為被記錄在 Cache storage 的話,切換成 PWA 打開,資料並不會 duplicate,在 iOS 13 時是會有兩份 entry (這個改動仍然在觀察中,iOS 14 正式 release 前都還說不準)

  • 同個網域下的 Service worker 不論在 Safari or PWA 中被開啟,都只會被註冊一次,但會有多個 Service worker instances
  • Cookie, Web storage, IndexedDB 還是分離的

Gesture

從 iOS 12.2 開始,支援 navigation gesture 功能,像瀏覽 Safari 網頁一樣,可以用 Swipe 手勢來返回上一頁切換網頁,這樣在 standalone mode 的時候不需要特別放返回鍵在頁面上了

2020/09/04 更新

在 iOS 14 beta,PWA 用手勢做 swipe back 可以正常使用

回測了 iOS 13,沒想到也是正常,所以 Gesture 目前看起來是沒什麼問題了

Navigation gesture 需要有 manifest file,如果在 iOS Safari 只使用 meta-tags, 使用者從桌面啟動 Web APP 後,進入某個子畫面做 swipe back 會沒有任何反應

能運用 Swipe back 的情況有

  • 切換頁面,比如進到子畫面
  • 所有從 PWA 打開的 URL (In-APP Browser)
    • 如果在 In-APP Browser 的網頁內又再進入其他 URL,swipe back 會回到上一頁
    • 如果已經在 top level 層,swipe back 會回到進入 In-APP Browser 之前的 PWA 頁面

iOS PWA 追蹤

Twitter who keep updating information of PWA or web dev

Thomas 發了不少 Safari 的 Bug 給 Apple WebKit Team,多數都是跟 PWA features 相關的,而 Jake 是在自己的推會講滿多 web 的東西,他也是 Google I/O 大會上常代表 Chrome team 出來報告的人,Firtman 的 Medium 文章一定要看一下,在實際測試跟比較上非常詳細

因為 Apple 對於發展 PWA 的態度還不明確,在 iOS Safari 新版本的 release notes 也不怎麼提到跟 PWA 有關的更新,都要前沿的前端大神們去測試跟互相回報才知曉一二,所以基本上要追蹤相關東西可以多少看一下這幾位的推,都是有在關注 PWA 的資深前端開發者

後記

誠如前言所說,前端的領域的東西很雜很多,而且常常在變,不止框架的不同,在同個框架內做同一件事的寫法也有不少種

我感覺前端技術像是有好多工具可以用,要學得好就要像技師一樣要知道在什麼樣的 use case 下用什麼工具會最有效率最合適

不得不抱怨下 Apple,自從接觸了這個生態系,對他真是又愛又恨,iOS APP 還好,但是前端在 iOS 上真的處處都是障礙,第三方瀏覽器沒辦法使用 iOS 的一些功能,連自家的 Safari 都不一定能完全 access 所有 iOS 功能,更別說實現 PWA

Apple 對 Web APP 非常消極,似乎還有點抵觸,原因不外乎是保護他們的 Apple Store 生態圈,但開始支援 Service Worker 無疑對很多前端開發者而言是一道曙光,只是離完全支援要多久不得而知,等到 iOS Safari 完全支援 PWA 的那天我可能又跑去寫別的東西了說不定= =,或是已經提早退休去種田

希望之後 PWA 在 iOS 上的實作心得篇可以如期產出XDD,可能要花點時間整理一番