# PWA:讓 Web 彷彿手機原生 App
最近遇到一個專案,需要利用 PWA 的技術,
讓網站能建立捷徑到手機主畫面上,而且使用起來像手機 App,
藉著這次專案的機會,來整理一下 PWA 的技巧與更進階的樣式設定。
# 漸進式網頁是什麼?
PWA 的全名是 Progressive Web Apps(漸進式網頁應用程式,以下皆簡稱 PWA),
有 PWA 功能的網頁,會跳出「將 App 新增置主畫面」、「安裝 App」的按鈕,
安裝後從主選單點開,同一網頁就不會有網址列,
看起來就像一個原生的 App,還可設定推播通知與離線存取功能。
### ▍PWA 是什麼時候發展的?
2015 年由 [設計師 Frances Berriman 和 Google Chrome 工程師 Alex Russell](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/) 提出。
目的是希望瀏覽手機網頁時,能夠像原生 App 一樣有使用者友善的體驗。
2014 年 Google 開發者大會開始提出相關技術,隨後 Google 大力推動 Android 的 PWA 開發。
Firefox 在 2016 年開始支援 PWA 的核心技術(Service Worker)。
Microsoft Edge 和 Apple Safari 在 2018 年隨後跟上。
現在所有主要系統上都可使用(參考自 [vuestore 的整理](https://vuestorefront.io/blog/pwa))。
### ▍PWA 字面上意思是?
漸進式網路應用程式(Progressive web app)的譯名,可能讓第一次看到的人較難直接理解意涵。
這具有「逐漸增強功能」的意涵,說明這種網站可以是電腦版網頁、行動版網頁,
也可以成為桌面應用程式,甚至離線使用、推播訊息。
讓網站「逐漸成為『應用程式』([They progressively become "apps"](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/#:~:text=They%20progressively%20become%20%22apps%22))」。
### ▍PWA 具有哪些特徵?
傳統上,網站不太像是使用者「擁有的東西」而更像是「訪問的地方」。
當使用者不訪問該網站時,該網站不會出現在使用者的裝置上;
當使用者訪問網站時,只能藉由開啟瀏覽器並依賴網路連線到該網站。
雖然與原生 App 相比,網站具有這樣的限制,這樣的限制,但網站也有另一些優勢,例如:
* 單一程式碼:
* 由於網路是跨平台的,因此網站可藉由單一程式碼,在不同的作業系統和裝置上運行。
* 透過網路分發:
* 網路是很好的分發平台。只需使用網址,即可共享和存取網站,無需透過應用程式商店。
此外,PWA 也結合了原生 App 的優點,例如:
* 可以安裝在裝置上:
* 可以從平台的應用程式商店安裝,也可以直接從網路安裝。
* 可以像原生 App 一樣安裝,並且可以自訂安裝過程。
* 安裝後,PWA 會在裝置上獲得一個應用程式圖示以及原生 App。
* 安裝後,PWA 可以作為獨立應用程式啟動,而不是瀏覽器中的網站。
* 可以在背景和離線狀態下運作:
* 在設備沒有網路連線時工作。
* 後台更新內容。
* 響應來自伺服器的推播訊息。
* 使用作業系統通知系統顯示通知。
* 使用整個螢幕,而不是在瀏覽器 UI 中運行。
* 整合到設備中,註冊為共享目標和來源,並存取設備功能。
(參考自:[What is a progressive web app, MDN](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/What_is_a_progressive_web_app))
整體來說,PWA 有以下數點特性:
1. Progressive 漸進式 - 使用者於瀏覽器中即可操作 (註:各瀏覽器於各平台上支援度不一)
2. Responsive 響應式 - 可操作於桌機、手機或平板等裝置上
3. Connectivity independent 連結獨立 - 可基於service workers架構執行,於離線或在有限網路下操作
4. App-like 近似APP - 類似APP的操作介面
5. Fresh 維持新版 - 因 service worker 架構,讓應用程式隨時都是在更新狀態
6. Safe 安全性 - 必須於加密模式之下進行,因此安全較受到保障
7. Discoverable 可被搜尋 - 透過 manifest 設定檔案及 service worker 使搜尋引擎可正常搜尋到
8. Re-engageable 有互動性 - 透過類似推播方式與使用者更加互動
9. Installable 可安裝 - 可以拉存到手機的桌面,感覺就像是傳統的APP (註:非每種瀏覽器均支援)
10. Linkable 可連結 - 可經由連結輕易分享
(取自:[PWA介紹 (Progressive Web App),優、缺點及範例介紹,Arshire](https://www.arshire.com/blog/pwa))
### ▍PWA 運行在瀏覽器嗎?
當你在瀏覽器中造訪某個網站時,瀏覽器 UI 讓你很明顯感受到「網頁正在瀏覽器中運行」。
PWA 可以在沒有瀏覽器 UI 的情況下使用,但從技術角度來看,它們仍然是網站。
這意味著 PWA 需要藉由瀏覽器引擎(如 Chrome、Firefox )來管理和運行它們。
相較之下,原生 App 通常由平台作業系統管理該應用程序,提供其運行的環境。

### ▍PWA 有哪些優缺點?
##### 優點
只要將網站製作完成,再透過PWA技術,就可以將網頁轉換成原生App的效果。
對企業來說,不用花大錢額外製作原生App,節省成本。
* 迅速進入手機市場:
* Progressive Web Apps(PWA)是最簡單的進入手機市場的方式,可在幾個月內設置完成,並且適用於所有設備。
* 一次提供所有功能:
* 使用PWA技術,團隊只需構建一個無縫運行於任何設備的應用程序,無需單獨開發iOS和Android應用。
* 成本效益最佳化:
* 由於PWA的全功能,節省時間和降低開發成本,無需支付應用商店費用。
* 降低顧客獲取成本(CAC):
* PWA允許用戶直接從移動瀏覽器安裝應用程序,提高試用機會,且無需下載更新。
* 發揮無頭商務的優勢:
* PWA使用無頭架構提供卓越的靈活性,並分離前端和後端,使營銷團隊獨立進行更改。
* 優化的SEO結果:
* PWA加速Google索引,並且採用標準URL和完整的伺服器端渲染(SSR),有助於提高搜尋引擎排名。
* 降低跳出率:
* PWA無論網絡條件如何,均可即時加載,並在離線狀態下工作,降低跳出率。
* 提高參與度、轉換率和收入:
* PWA提供優越性能、移動優化和優秀的UX,通過全屏功能、易於訪問以及推送通知提高用戶參與度。
(取自:https://vuestorefront.io/blog/pwa)
##### 缺點
* 瀏覽器/平台的支援度不一
* * [iOS很多不支援](https://www.youtube.com/watch?v=eoUvIm8Pl6I&ab_channel=DevTrends)
* 大部分消費者並不清楚如何操作
* 並不是說不懂如何操作網站,而是說不知道該如何把該網站轉成類應用程式的操作模式
* PWA 會比原生 App 耗電量來的高
* 由於它們是用複雜的程式編寫的,手機必須更加努力的轉譯程式碼
(參考自:[PWA,Arshire](https://www.arshire.com/blog/pwa))
# 基礎設定
### ▍讓網頁可安裝
PWA 最基礎的設定,就是讓網頁可安裝下來,從主畫面進入、沒有瀏覽器 UI。
基礎設定上,我們只需要增加一個 App 用的圖示,以及一個設定檔。

##### 程式碼
在你的網頁的 head 中引入 manifest.json,就完成 PWA 的設定。
```htmlembedded
<!doctype html>
<html lang="en">
<head>
<link rel="manifest" href="manifest.json" />
</head>
<body></body>
</html>
```
##### 應用程式圖示
放入自己想要的 Icon,個人建議一開始用 192x192(手機版適用)。
* 圖示大小要正方形,並且與實際大小相同,不然Chrome會無法下載。
##### 設定檔:manifest.json
瀏覽器是透過這個檔案,來知道如何將網頁安裝在用戶的電腦或行動裝置上。
裡面是對 PWA 顯示的一些設定,有四項必填:name、start_url、display、icons。
通常會放在根目錄。
``` json
{
"name": "PWA 範例網", // App 名稱,■■必填■■
"short_name": "PWA", // App 名稱縮寫,顯示空間不足時使用
"description": "這是一個簡單的 PWA 範例。", // App 的描述
"start_url": ".", // 首頁路徑,依 manifest 檔案位置來看,■■必填■■
"display": "standalone", // 顯示模式,■■必填■■
"orientation": "portrait", // 定義預設的顯示方向
"background_color": "#5a0fc8", // 預設背景色
"icons": [ // 圖示,■■必填■■
{
"src": "icon.png", // 圖示路徑
"sizes": "192x192", // 圖示尺寸
"type": "image/png" // 圖示格式,可省略
}
]
}
```
* display
* fullscreen 全螢幕
* standalone 原生 App 模式
* minimal-ui 最基本的瀏覽器 UI
* browser 瀏覽器樣式
* orientation
* Arial 不限制
* naturl 設備預設的方向
* portrait 直向
* landscape 橫向
##### 成果
設定完成後,瀏覽器點開網頁,會看到一個下載的圖示,就完成設定。
可以嘗試下載看看,就會變成一個沒有網址列的介面。
手機上的話也可以有類似效果,然而 iOS 使用者需要用 Safari 開啟,並手動將網頁加到主畫面。
(參考自:[Making PWAs installable, MDN](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable))
### ▍離線存取功能
離線存取功能是 PWA 的一個重要技術,利用 Service Worker 監控前端事件與回應。
Service Worker 本質上充當位於 Web 應用程式、瀏覽器和網路(如果可用)之間的代理伺服器。
除此之外,它們的目的是創建有效的離線體驗、攔截網路請求並根據網路是否可用採取適當的操作,
以及更新伺服器上的資產。它們還允許存取推播通知和後台同步 API。
Server Worker 是大型商業網展常使用的技術,可以將部分的 JS 程式放在瀏覽器背景執行,以做到像是推播的功能。
##### 原理
JS 程式是由瀏覽器的主執行緒(Main Thread)負責執行,
Service Worker 則是在不同執行緒非同步運行,不會影響網頁的渲染。
一般網頁:客戶端發送http請求給伺服器,伺服器再回應給客戶端。
Service Worker:聽fetch事件,攔截網頁的http請求,藉由 Service Worker 的快取(Catch)功能,
可以選擇由快取取得回應,因此就算離線,使用者也能夠正常離覽網頁。

Service Worker 有自己的生命週期,從下載、安裝到啟用,在不同的生命週期可以利用監聽事件,進行相對應的處理。
**Service Worker 不能直接操作DOM物件**,如果有需要可以透過postMessage()的方法發送訊息,然後透過message事件來溝通。
##### 主要功能
* 離線瀏覽網頁
* 前面提到的,由快取回應開啟網頁的請求。
* 離線送出表單
* 離線狀態下無法送出表單,這種情況可以先將資料存在IndexedDB,並註冊sync事件,SW 監聽 Background Sync 事件,網路重新連線時,再將 IndexedDB 資料上傳到伺服器,達到離線送出表單的功能。
* 推播通知
* SW 會監聽 PUSH 事件,當收到伺服器發出的推播通知時,會顯示給用戶。
* 加入主畫面(Add to Home Screen, A2HS)」
* serviceWorkerContainer.register() 註冊 SW 檔案
* 註冊成功的話,SW 會運行在全域環境
##### 操作方法
先提供監聽方法,讓你了解 Service Worker 運作過程
``` javascript
// 監聽 install 事件
self.addEventListener('install', (e) => {
console.log('安裝')
self.skipWaiting();
});
// 監聽 activate 事件
self.addEventListener('activate', (e) => {
console.log('啟用')
});
// 監聽 fetch 事件
self.addEventListener('fetch', (e) => {
console.log('fetch')
});
// 引入 Workbox,一個用於簡化 Service Worker 開發的函式庫
importScripts('https://cdnjs.cloudflare.com/ajax/libs/workbox-sw/7.0.0/workbox-sw.js');
// 註冊 Service Worker 路由,使用 Workbox 的 registerRoute 方法,需要帶入兩個參數
workbox.routing.registerRoute(
new RegExp('.*'), // 使用正規表達式匹配所有 URL,.* 表示所有字符零次或多次
new workbox.strategies.NetworkFirst(), // 使用 NetworkFirst 快取策略,優先嘗試從網路獲取資源
);
```
index.html 寫下這些來註冊 Service Worker。
```javascript
navigator.serviceWorker.register(scriptURL, options)
.then(() => {
// 註冊成功時執行
}).catch((error) =>{
// 註冊失敗時執行
});
// scriptURL 是 SW 檔案,在這個情況下是 sw.js
// options 是 SW 的 scope,預設為「./」,也就是根目錄
// 如果 sw.js 放在網站根目錄,options 可以不用寫
// scriptURL 的相對路徑是相對於 sw.js
```
註冊前要檢查瀏覽器(navigator)是否支援 Service Worker,等到網頁資源都載入再註冊 Service Worker。
```javascript
if('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then(() => console.log('註冊成功'))
.catch((err) => console.log('註冊失敗'));
});
}
```
如果有多個 html 可以把註冊程式拉出來,再引入每個 html
##### 成果
可以離線載入。
# 進階技巧
### ▍各尺寸 Icon 與啟動畫面
主要是 iOS 目前不支援 Web App Manifest API 規範,需要引入自訂 html 標籤來為您的 PWA 設定圖示和啟動畫面。
您需要為 Apple 建立每個尺寸的啟動畫面(splash),並為每個影像建立各自的 html 標籤。
可以藉由套件 [pwa-asset-generator](https://github.com/elegantapp/pwa-asset-generator) 來產生各尺寸的 icon、啟動畫面。
##### 特性
* 生成類型:圖示、啟動畫面
* 自動更新您的 manifest.json 和 index.html 文件
* 提供 iOS 的深色、淺色啟動畫面選擇
* 因為要生成不同尺寸,建議使用SVG檔
* 用CSS設定樣式
##### 指令
```htmlmixed
// npx pwa-asset-generator [圖片檔名] -i [index.html路徑] -m [manifest.json路徑]
npx pwa-asset-generator logo.svg -i ./index.html -m ./manifest.json
```
終端機輸入後,在同一層目錄就可以得到:
* 圖示:192x192、512x512、180x180(iOS用)
* 啟動畫面:30種尺寸
* 在 index.hmlt 寫好引入的標籤
會產出這麼多的圖片!

實際使用時,有時專案的 index.html 與 logo.svg 不會放在同一層,
我會創一個新資料夾與空的 html,把資料都放在同一層,產生檔案與標籤後再移回專案,
就不用一直研究指令如何在不同資料夾取放資料的問題。
以下是一些好用的指令:
* 生成類型
* `--icon-only` 圖示
* `--splash-only` 啟動畫面
* `--landscape-only‵` 啟動畫面(僅橫的)
* `--portrait-only` 啟動畫面(僅直的)
* 帶有透明度的png:`--opaque false`
* 設定padding,預設`--padding "calc(50vh - 10%) calc(50vw - 10%)"`,可自行調整
* 設定漸層底色
* `-b "linear-gradient(90deg, rgba(207, 234, 255, 1) 0%, rgba(240, 243, 255, 1) 50%, rgba(223, 205, 255, 1) 100%)"`

另外,他們提供 iOS 的深色、淺色啟動畫面設定。
以下是語法說明,基本上就是想要設定什麼就一直往後加。
```htmlmixed=
// npx pwa-asset-generator [圖片檔名] [圖片資料夾路徑] [iOS啟動畫面:深色] --background [背景色] [僅啟動畫面] --type [輸出類型] --qality [圖片品質] --index [index.html路徑]
npx pwa-asset-generator light-logo.svg ./assets --dark-mode --background dimgrey --splash-only --type jpeg --quality 80 --index ./src/app/index.html
// npx pwa-asset-generator [圖片檔名] [圖片資料夾路徑] --background [背景色] [僅啟動畫面] --type [輸出類型] --qality [圖片品質] --index [index.html路徑]
npx pwa-asset-generator dark-logo.svg ./assets --background lightgray --splash-only --type jpeg --quality 80 --index ./src/app/index.html
```
### ▍樣式設定(iOS)
* Add a Short Name
在 manifest.json 設定 PWA 的短名稱,如果使用者的介面不允許太多字時可自動調整。
* Make the Status Bar transparent
預設情況下,iOS 的 PWA 頂部狀態列會為黑底,可用 meta 標籤使其透明。
```htmlmixed
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />
```
而 iPhone 的瀏海可能會擋到 Header 文字,可以做以下設定。
```css
header {
padding-top: env(safe-area-inset-top);
}
```
* 固定 header
在使用手機App時,header 通常會固定在畫面上方,
讓 PWA 網站加入這樣的設定可以增進使用者體驗。
```css
@media screen and (display-mode: standalone) {
position: fixed;
}
```
* 禁止使用者縮放
手機瀏覽器可以輕易縮放網頁大小,
但一般原生 App 沒有這樣的功能,我們可以停用縮放功能來達到這個效果。
```htmlmixed
<meta name="viewport"
content="initial-scale=1, viewport-fit=cover, user-scalable=no"
/>
```
* Set the tap highlight color to transparent
預設情況下,iOS 的 PWA 會像網頁一樣以灰色方塊突出顯示所有連結點擊。
關掉灰色顯示會更接近原生 App 的樣式。
```css
body {
-webkit-tap-highlight-color: transparent;
}
```
### ▍自動安裝功能(Android)
若您的網站是可安裝的 PWA 網頁,那麼瀏覽器將顯示按鈕來顯示此網頁可安裝。

但是,我們也可以自己製作觸發安裝的按鈕,也能讓使用者更容易發現。
```htmlmixed
<button class="addBtn">安裝程式</button>
```
```javascript
// 檢查是否安裝
window.addEventListener("beforeinstallprompt", async (event) => {
const relatedApps = await navigator.getInstalledRelatedApps();
// Search for a specific installed platform-specific app
const psApp = relatedApps.find((app) => app.id === "com.example.myapp");
if (psApp) {
event.preventDefault();
// Update UI as appropriate
}
});
// 安裝
let deferredPrompt; // 將事件隱藏用(??)
const addBtn = document.querySelector('.addBtn');
addBtn.style.display = 'none';
window.addEventListener('beforeinstallprompt', (e) => {
// 防止在 Chrome 67 之前的版本中自動顯示安裝提示
e.preventDefault();
// 將事件隱藏以便稍後觸發
deferredPrompt = e;
// 更新UI以通知用戶可以將應用添加到主屏幕
addBtn.style.display = 'block';
addBtn.addEventListener('click', (e) => {
// 顯示安裝提示
deferredPrompt.prompt();
// 等待用戶響應安裝提示
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('使用者接受 A2HS 的請求。');
// 隱藏顯示 A2HS 按鈕的用戶界面
addBtn.style.display = 'none';
} else {
console.log('使用者拒絕 A2HS 的請求。');
}
deferredPrompt = null;
});
});
});
```
(參考自:[Trigger installation from your PWA, MDN](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Trigger_install_prompt)、[ホーム画面に追加, MDN](https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable))
### ▍分享功能
也可以觸發瀏覽器的分享功能,讓別人看看你的 PWA 網站。
```javascript
// sharing.js
const shareBtn = document.querySelector('.shareBtn');
shareBtn.onclick = async (filesArray) => {
if (navigator.canShare) {
navigator.share({
url: 'https://charliewuuu.github.io/PWA/',
title: 'PWA 超酷!',
text: 'PWA 超酷!我學會怎麼建立一個 PWA 程式了!',
});
}
};
```
<!-- # 可能提問
### 如果我有很多頁要怎麼辦?
*在每一頁都要引入server-worker?*
### 有沒有效能問題?
*畢竟這個app是藉由瀏覽器運行,等於多一層系統,會不會很吃資源?*
### 還有什麼其他功能?
推播功能等。
### PWA會是未來的趨勢嗎?
不一定。根據國外的報導(2021的下半年),雖然GOOGLE努力推廣多年,但是PWA架構並沒有很普及,對大多數人來說還是相對較新的概念。2018年曾有許多科技網站大力吹捧PWA將會取代傳統APP,不過許多之前用PWA開發模式的網站已變更回單純網站。 -->
# 參考資料
* 網站
* 文件
* MDN:[Progressive web apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
* MDN:[What is a progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/What_is_a_progressive_web_app)
* MDN:[Making PWAs installable](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable)
* MDN:[ホーム画面に追加](https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable)
* 樣式教學文
* Sam Selikoff:[8 Tips to Make Your Website Feel Like an iOS App](https://samselikoff.com/blog/8-tips-to-make-your-website-feel-like-an-ios-app)
* 提出 PWA 此名詞的文章
* Alex Russell:[Progressive Web Apps: Escaping Tabs Without Losing Our Soul](https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/)
* 首次提出 service worker 與 menifest 的開發者大會
* [Chrome Dev Summit 2014](https://web.dev/case-studies/chrome-dev-summit)
* PWA 的發展歷史以及商業上的好處
* [vuestore:PWA](https://vuestorefront.io/blog/pwa)
* PWA 介紹,寫得很清楚,優缺點、普及性與對發展性的存疑都很清楚
* [Arshire:PWA](https://www.arshire.com/blog/pwa)
* Youtube
* PWA 初步介紹,有很簡單的教學步驟
* Fireship:[Progressive Web Apps in 100 Seconds // Build a PWA from Scratch](https://www.youtube.com/watch?v=sFsRylCQblw)
* PWA 的進階功能,例如分享功能
* Fireship:[7 Web Features You Didn’t Know Existed](https://www.youtube.com/watch?v=ppwagkhrZJs&ab_channel=Fireship)
* 書
* [HTML/CSS/JavaScript與前端框架的完美結合:使用Bootstrap與PWA技術, 新手從這開始!](https://www.tenlong.com.tw/products/9786263333109?list_name=c-css)