>[success]部分內容來源:《PWA 實戰》
[TOC]
推薦閱讀:
[PWA 詳解](https://lzw.me/a/pwa-service-worker.html)
[PWA 國內外應用分析報告](https://36kr.com/p/5124142)
# 第 1 章
PWA 的關鍵:Service Worker,Service Worker 可以讓你全權控制網站發起的每一個請求,它可以重定向你的請求,甚至徹底停止。
其具有以下幾個特點:
- 運行在它自己的全局腳本上下文中
- 不綁定到具體的網頁
- 無法修改網頁中的元素,因此它無法訪問 DOM
- 只能使用 HTTPS(除非是本地調試,如域名使用 localhost)
Service Worker 運行在 worker 上下文中,這意味著它無法訪問 DOM,它與應用的主要 JavaScript 運行在不同的線程中,所以它不會被阻塞。它們是完全異步的,因此你無法使用諸如同步 XHR 和 localStorage 之類的功能。
## Service Worker 的生命周期
1.調用 register() 函數,瀏覽器會下載、解析并執行 Service Worker,如果此步驟中出現任何錯誤,register() 返回的 Promise 都會執行 reject 操作,并且 Service Worker 會被廢棄。
2.一旦 Service Worker 成功執行了,安裝事件就會激活(Service Worker 是基于事件的)
3.一旦完成安裝,Service Worker 便會激活并發揮其作用(如攔截 HTTP 請求等)


## 基礎示例
我們先寫一個 index.html:
```html
<!DOCTYPE html>
<html lang="en">
<head>
...
<title>service worker test</title>
</head>
<body>
<script>
// 注冊 service worker
if ('serviceWorker' in navigator) { // 檢查當前瀏覽器是否支持 Service Worker
// 如果支持,注冊一個叫做 sw.js 的 Service Worker 文件
navigator.serviceWorker.register('/sw.js').then(function (registration) {
// 注冊成功
console.log('ServiceWorker registration successful with scope: ', registration.scope)
// github Page 是 https,向 http 協議的服務器發起請求會被 block
fetch('https://chenmingk.github.io./images/img2.jpg').then(response => {
console.log(response)
})
}).catch(function (err) {
// 注冊失敗
console.log('ServiceWorekr registration failed: ', err)
})
}
</script>
</body>
</html>
```
這里使用 github pages 來做測試,自行百度。其邏輯是打開訪問網頁時先檢查瀏覽器是否支持 Service Worker,如果支持,注冊一個叫做 sw.js 的 Service Worker 文件,然后我們嘗試發起一個 fetch 請求,注意我們要拿的是 img2.jpg,然后我們寫一個 sw.js 文件。
```js
// 為 fetch 事件添加事件監聽器
// self 為當前 scope 內的上下文
self.addEventListener('fetch', function (event) {
// 檢查傳入的 HTTP 請求 URL 是否請求以 .jpg 結尾的文件
console.log(event.request.url)
if (/\.jpg$/.test(event.request.url)) {
console.log('攔截 fetch 請求')
// 嘗試獲取某個圖片并用它作為替代圖片來響應請求
event.respondWith(fetch('https://chenmingk.github.io./images/img.jpg'))
}
})
```
我們監聽 fetch 事件,攔截 url 以 .jpg 結尾的 http 請求,并指定攔截后的操作。這里我們獲取 images 目錄下的名為 img 的圖片來響應請求。
然后訪問 `https://chenmingk.github.io./`,得到如下結果:

可以看到我們的 fetch 請求被攔截了,即我們本來要拿的是 img2 現在變成了 img。
我們的 githubPage 目錄結構是這樣的:

# 第 2 章 構建 PWA 的前端架構方式
## 應用外殼架構
使用 Service Worker 緩存,可以緩存網站的 UI 外殼,以便用戶重復訪問。所謂 UI 外殼,即用戶界面所必需的最小化的 HTML、CSS 和 JavaScript。它可能會是類似網站頭部、底部和導航這樣沒有任何動態內容的部分。
當用戶訪問網站時,我們可以讓 UI 外殼立即呈現,然后使用一個 “loading” 標識表示一些資源正在動態加載,這比等待一個空白頁面要好多了。
## 緩存
Service Worker 緩存和 HTTP 緩存并不是同一概念。
參考鏈接:[https://zhuanlan.zhihu.com/p/28113197](https://zhuanlan.zhihu.com/p/28113197)
[https://www.cnblogs.com/kenkofox/p/8732428.html](https://www.cnblogs.com/kenkofox/p/8732428.html)
## 離線瀏覽
如果你身處某個區域,網絡信號很弱,甚至會掉線。那么手機瀏覽網頁時,就會顯示無法連接或某些錯誤。使用 Service Worker 緩存可以將網站資源保存到用戶的設備上,可以攔截任何 HTTP 請求并直接用設備上緩存的資源進行響應。甚至不需要訪問網絡就可以獲取緩存的資源。考慮到這一點,我們可以構建離線頁面,為用戶展示一個自定義的離線頁面。
# 第 3 章 緩存
使用 HTTP 緩存最大的一個問題就是資源更新不同步的情況,因為其依賴服務器來告訴何時緩存資源以及資源何時過期。現在一般的做法是每次更新版本時使用哈希的方式重新生成文件名。
Service Worker 緩存的不同之處在于,無須再由服務器來告知瀏覽器資源要緩存多久,它賦予了你程序式的精準控制能力。Service Worker 緩存是對 HTTP 緩存的增強,并可以與之配合使用。
不啰嗦了直接上代碼:[https://github.com/ChenMingK/ChenMingK.github.io/tree/master/chapter3](https://github.com/ChenMingK/ChenMingK.github.io/tree/master/chapter3)
首先是 index.html:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello Caching World!</title>
</head>
<body>
<!-- Image -->
<img width="300" height="300" src="./images/hello.png" />
<!-- JavaScript -->
<script async src="./js/script.js"></script>
<script>
// Register the service worker
if ('serviceWorker' in navigator) {
// 嘗試注冊名為 service-worker.js 的文件
navigator.serviceWorker.register('./service-worker.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope)
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err)
});
}
</script>
</body>
</html>
```
然后我們寫這個 service-worker.js 文件:
```js
var cacheName = 'helloWorld'
// install 事件發生在瀏覽器安裝并注冊 Service Worker 時
// 這是把后面階段可能會用到的資源添加到緩存中的絕佳時間
self.addEventListener('install', event => {
// event.waitUntil() 方法返回一個 Promise 對象
event.waitUntil(
caches.open(cacheName) // 使用指定的緩存名稱來打開緩存
.then(cache => cache.addAll([ // addAll() 方法傳入一個文件數組
'./js/script.js',
'./images/hello.png'
]))
)
})
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request) // 檢查傳入的請求 URL 是否匹配當前緩存中存在的任何內容
.then(function(response) {
if (response) { // 如果有 response 并且它不是未定義的或空的,就將它返回
return response
}
// 否則,通過網絡讀取預期的資源
return fetch(event.request)
})
)
})
```
打開我們的網頁看看報不報錯:

檢查 Cache Storage 中是否緩存了對應的資源:

## 攔截并緩存
上面的緩存方式可以稱為預期緩存,即你完全知到你要緩存的資源,并在 Service Worker 安裝期間緩存這些資源。但是資源可能是動態的,或者你可能對資源完全不了解,Service Worker 如何緩存這些資源呢?
因為 Service Worker 能夠攔截 HTTP 請求,這意味著我們可以先請求資源,然后立即將其緩存起來,然后對于同一資源的下一次 HTTP 請求,就可以立即將其從 Service Worker 緩存中取出。
代碼清單:[https://github.com/ChenMingK/ChenMingK.github.io/tree/master/chapter3/cachefirst](https://github.com/ChenMingK/ChenMingK.github.io/tree/master/chapter3/cachefirst)
其他部分差不多,這里介紹如何動態地向緩存中添加資源。
```js
var cacheName = 'helloWorld';
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) { // 如果匹配,就此返回緩存并不再執行
return response;
}
// 復制請求。請求是一個流,只能使用一次。
var fetchRequest = event.request.clone();
// 嘗試按照預期發起原始的 HTTP 請求
return fetch(fetchRequest).then(
function(fetchResponse) {
// 如果請求失敗或服務器響應了錯誤代碼,則立即返回錯誤消息
if(!fetchResponse || fetchResponse.status !== 200) {
return fetchResponse;
}
// 再一次復制了響應,因為你需要將其添加到緩存中,而且它還將用于最終返回響應
var responseToCache = fetchResponse.clone();
caches.open(cacheName) // 打開名為 helloWorld 的緩存
.then(function(cache) {
cache.put(event.request, responseToCache); // 將響應添加到緩存中
});
return fetchResponse;
}
);
})
);
});
```
需要注意的是,需要復制請求和響應。請求是一個流,它只能使用一次,因為已經通過緩存使用了一次請求,接下來發起 HTTP 請求還要再用一次,所以需要復制請求。響應也是同理。
## 推薦工具
WebPagetest.org 用于網站性能測試
Workbox (https://workboxjs.org)幫助我們快速創建自己的 Service Worker
```js
// Service Worker 可以訪問一個叫作 importScripts() 的全局函數
// 該函數可以將同一域名下的腳本導入至它們的作用域
importScripts('workbox-sw.prod.v1.1.0.js') // 加載 Workbox 庫
const workboxSW = new self.workboxSW()
workboxSW.router.registerRoute(
'https://test.org/css/(.*)', // 開始緩存匹配'/css'路徑的任何請求
workboxSW.strategies.cacheFirst()
)
```
# 第 4 章 fetch 事件
fetch API 這里就不再介紹了,用一個 POST 請求的格式簡單過一下:
```js
fetch('/some/url', { // 請求的 URL
method: 'POST',
// 請求中包含的 header
headers: {
'auth': '1234'
},
// POST 請求的 body
body: JSON.stringify({
name: 'dean',
login: 'dean123'
})
})
.then(function (data) { // 成功的回調
console.log('Request success: ', data)
})
.catch(function (error) { // 失敗的回調
console.log('Request failure', error)
})
```
以上示例我們監聽的都是 fetch 事件,Service Worker 可以攔截瀏覽器發出的任何 HTTP 請求,屬于此 Service Worker 作用域內的每個 HTTP 請求都會觸發 fetch 事件(而不是一定要用 fetch 發送的請求)。我們可以攔截 HTTP 請求轉而檢查 Service Worker 緩存中是否有該資源,也可以自定義 HTTP 響應,代碼如下。
```js
self.addEventListener('fetch', function (event) {
if (/\.jpg$/.test(event.request.url)) {
event.respondWith(
new Response('<p>This is a response that comes from your service worker!</p>', {
headers: { 'Content-Type': 'text/html' } // 自定義 Response
})
)
}
})
```
可以說 Service Worker 的權限太大了,同時我們也可以理解為什么它一定需要通過 HTTPS 提供請求了。
## 首次訪問的問題
當用戶第一次訪問網站時,并不會有激活的 Service Worker 來控制頁面,只有當 Service Worker 安裝完成并且用戶刷新了頁面或跳轉至網站的其他頁面時,Service Worker 才會激活并開始攔截請求。如果我們希望 Service Worker 能盡快開始工作,包括在其未激活期間所發起的請求該怎么辦?
書中提供了一個小技巧:
```js
self.addEventListener('install', function (event) {
event.waitUntil(self.skipWaiting())
})
```
skipWaiting() 函數強制等待中的 Service Worker 成為激活的 Service Worker,self.skipWaiting() 函數還可以與 self.clients.claim() 一起使用,以確保底層 Service Worker 的更新立即生效。
```js
self.addEventListener('active', function (event) {
event.waitUntil(self.clients.claim())
})
```
同時使用以上兩段代碼,可以立即激活 Service Worker。
## 使用 WebP 圖片的示例
WebP 圖片格式相比 PNG、JPEG 等有文件體積小且圖像質量沒有顯著差異的優點,目前只有 Chrome、Opera 和 Andorid 支持 WebP 圖片,Safari、Firefox 和 IE 還不支持。
支持 WebP 格式的瀏覽器會在每個 HTTP 請求中添加:accept:image/webp 請求頭來告知服務端它支持 WebP 格式。
我們可以添加如下代碼來實現對圖片請求的攔截同時返回 WebP 格式的圖片(如果瀏覽器支持)
代碼清單:[https://github.com/ChenMingK/ChenMingK.github.io/tree/master/chapter-4/WebP-Images](https://github.com/ChenMingK/ChenMingK.github.io/tree/master/chapter-4/WebP-Images)
可以通過訪問:[https://chenmingk.github.io/chapter-4/WebP-Images/](https://chenmingk.github.io/chapter-4/WebP-Images/) 打開調試工具來檢查
```js
// Listen to fetch events
self.addEventListener('fetch', function(event) {
// Check if the image is a jpeg
if (/\.jpg$|.png$/.test(event.request.url)) {
// Inspect the accept header for WebP support
var supportsWebp = false;
if (event.request.headers.has('accept')) {
supportsWebp = event.request.headers
.get('accept')
.includes('webp');
}
// If we support WebP
if (supportsWebp) {
// Clone the request
var req = event.request.clone();
// Build the return URL
var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp";
event.respondWith(
fetch(returnUrl, {
mode: 'no-cors'
})
);
}
}
});
```
> 運行后發現,確實能攔截到請求并得到 WebP 格式的圖片,但是顯示在網頁上的仍然是 .jpg 格式,暫時不知道為什么。
# 第 5 章 Web 應用清單(manifest.json)
Web 應用清單是簡單的 JSON 文件,它在文本文件中提供了應用相關的有用信息(如應用的名稱、作者、圖標和描述)。其還可以使用戶將 Web 應用安裝到設備的主屏幕上,并允許開發者自定義啟動頁面、模板顏色,甚至打開 URL。
先來看這個 manifest.json 示例:
```json
{
"name": "Progressive Times web app",
"short_name": "Progressive Times",
"start_url": "./index.html",
"theme_color": "#FFDF00",
"background_color": "#FFDF00",
"display": "standalone",
"icons": [
{
"src": "./images/homescreen.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./images/homescreen-144.png",
"sizes": "144x144",
"type": "image/png"
}
]
}
```
- name:表示當提示用戶安裝應用時出現的文本
- short_name:表示當應用安裝后出現在用戶主屏幕上的文本
- start_url:決定了當用戶從設備的主屏幕開啟 Web 應用時所出現的第一個頁面
- display:根據構建的 Web 應用類型,可能需要預設如何首次加載。display 字段表示開發者希望他們的 Web 應用如何向用戶展示
- theme_color:該字段可以對瀏覽器地址欄著色,以符合網站的主色調
- icons:表示在設備主屏幕上的圖標
為了引用清單文件,需要為 Web 應用的所有頁面都添加如下的 link 標簽
```html
<link rel="manifest" href="./manifest.json">
```
你可以用手機打開瀏覽器訪問:[https://chenmingk.github.io/chapter-5/look-and-feel/](https://chenmingk.github.io/chapter-5/look-and-feel/)不出意外的話應該能看到添加到主屏幕的提示。
> PS:QQ 瀏覽器不行,使用 Chrome 瀏覽器...
另外注意,要顯示添加到主屏幕的提示,需要滿足幾個條件:
- 需要 manifest.json 文件
- 清單文件需要啟動 URL
- 需要 144X144 像素的 PNG 圖標
- 網站必須使用通過 HTTPS 運行的 Service Worker
- **用戶需要至少訪問過網站兩次,并且兩次訪問間隔在 5min 以上**
這些要求是瀏覽器內置的,這也可以理解,如果訪問過的網站都顯示添加到主屏幕的提示那很快就會令人反感。
關于 display 有以下模式可供選擇:
- fullscreen:打開 Web 應用并占用整個可用的顯示區域
- standalone:打開 Web 應用以看起來像一個獨立的原生應用。在此模式下,用戶代理將排除諸如 URL 欄等標準瀏覽器 UI 元素,但可以包括諸如狀態欄和系統返回按鈕的其他系統 UI 元素。
- minimal-ui:此模式類似于 fullscreen,但為用戶提供了最小 UI 元素集合。
- browser:使用操作系統內置的標準瀏覽器來打開 Web 應用。(默認)
## 控制默認行為
### 取消提示
可以通過如下代碼取消添加到主屏幕的提示:
```js
window.addEventListener('beforeinstallprompt', function (e) {
e.preventDefault()
return false
})
```
### 判斷使用情況
監聽 beforeinstallprompt 事件,可以判斷出用戶是否決定添加 Web 應用到主屏幕或者直接關掉提示,可以根據這些反饋來跟蹤此功能的使用情況。
```js
window.addEventListener('beforeinstallprompt', function (event) {
event.userChoice.then(function (result) { // 判斷用戶的選擇并返回 Promise
console.log(result.outcome)
if (result.outcome === 'dismissed') {
// 發送數據以進行分析
} else {
// ...
}
})
})
```
### 推遲提示
如果我們想讓用戶通過單擊某個按鈕來將網站添加到主屏幕上可以這么做:
```js
window.addEventListener('beforeinstallprompt', function (e) {
e.preventDefault()
btnSave.removeAttribute('disabled') // 此時,啟用按鈕
savedPrompt = e // 將事件保存在變量中,這樣在稍后可以觸發它
return false
})
btnSave.addEventListener('click', function () {
if (savedPrompt !== undefined) {
savedPrompt.prompt() // 用戶與應用進行了積極的交互,所系顯示提示
savedPrompt.userChoice.then(function (result) {
if (result.outcome === 'dismissed') {
// ...
} else {
// ...
}
savedPrompt = null // 不再需要提示,所以清除它
})
}
})
```
## 調試文件清單
可以通過瀏覽器調試工具查看:

或者打開 manifest-validator.appspot.com 向驗證工具提供 URL 或粘貼 Web 應用清單的內容進行驗證
# 第 6 章 推送通知
推送通知的最大的優點是即使用戶沒有瀏覽你的網站也會收到這些通知內容,這種體驗類似于原生應用,而且即使瀏覽器沒有運行也可以工作。一旦用戶接收或屏蔽推送通知提示,提示就不會再出現,需要注意:只有當站點通過 HTTPS 運行時,同時有一個注冊過的 Service Worker,并且已經為其編寫好了代碼,才會出現此提示。
推送通知相比添加主屏幕就麻煩多了,需要前后端聯調(需要搭一個推送通知服務器),還需要遵守一定的規范(VAPID),暫時不做記錄......
現在有一些現成的第三方解決方案來實現將推送通知發送到多種不同瀏覽器的業務。[https://onesignal.com/](https://onesignal.com/)
# 第 7 章 離線應用
使用 Service Worker,可以判斷出用戶是否在離線狀態下獲取資源,我們可以檢查任何失敗的請求,然后返回用戶要查看的頁面的緩存版本。
首先需要將離線頁面添加到 Service Worker 緩存中:
```js
const cacheName = 'latestNews-v1';
const offlineUrl = 'offline-page.html';
// Cache our known resources during install
self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheName)
.then(cache => cache.addAll([
'./js/main.js',
'./js/article.js',
...,
offlineUrl
]))
);
});
```
上述代碼在 Service Worker 安裝階段將名為`offline-page.html`
......等會,webpack 好像提供了插件幫我們完成離線功能......[https://www.webpackjs.com/guides/progressive-web-application/](https://www.webpackjs.com/guides/progressive-web-application/)
>TODO:第 6 章、第 7 章補全
# 其他
書中更偏向于底層實現,現在都是各種配置項直接寫,關于 vue 項目配置 pwa 可以參考這篇文章:
[https://juejin.im/post/5ba3d205e51d450e8477af33#heading-16](https://juejin.im/post/5ba3d205e51d450e8477af33#heading-16)
話雖這么說,但是知道原理后我們可以更靈活地應用而不是總是 Google 找各種配置。
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs