PWA(Progressive Web App)漸進式Web APP,它並非單隻某一項技術,而是一系列技術綜合應用的結果,其中主要包含的相關技術就是Service Worker、Cache Api、Fetch Api、Push API、Notification API 和 postMessage API。使用PWA能夠給咱們帶來什麼好處呢?主要體如今以下幾方面javascript
1 離線緩存css
2 web頁面添加桌面快速入口html
3 消息推送java
簡單來講,Service Worker 是一個可編程的 Web Worker,它就像一個位於瀏覽器與網絡之間的客戶端代理,能夠攔截、處理、響應流經的 HTTP 請求。它沒有調用 DOM 和其餘頁面 api 的能力,但他能夠攔截網絡請求,包括頁面切換,靜態資源下載,ajax請求所引發的網絡請求。Service Worker 是一個獨立於JavaScript主線程的瀏覽器線程。Service Worker有以下特性:webpack
咱們須要在主線程中註冊Service Worker,而且通常是在頁面觸發load事件以後進行註冊。當Service Worker註冊成功後便會進入其生命週期。scope表明Service Worker控制該路徑下的全部請求,若是請求路徑不是在該路徑之下,則請求不會被攔截。git
// 註冊service worker
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then(function (registration) {
// 註冊成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (err) {
// 註冊失敗:(
console.log('ServiceWorker registration failed: ', err);
});
});
複製代碼
Service Worker生命週期大體以下github
install -> installed -> actvating -> Active -> Activated -> Redundantweb
在Service Worker註冊成功以後就會觸發install事件,在觸發install事件後,咱們就能夠開始緩存一些靜態資。waitUntil方法確保全部代碼執行完畢後,Service Worker 纔會完成Service Worker的安裝。須要注意的是隻有CACHE_LIST中的資源所有安裝成功後,纔會完成安裝,不然失敗,進入redundant
狀態,因此這裏的靜態資源最好不要太多。若是 sw.js 文件的內容有改動,當訪問網站頁面時瀏覽器獲取了新的文件,它會認爲有更新,因而會安裝新的文件並觸發 install 事件。可是此時已經處於激活狀態的舊的 Service Worker 還在運行,新的 Service Worker 完成安裝後會進入 waiting 狀態。直到全部已打開的頁面都關閉,舊的 Service Worker 自動中止,新的 Service Worker 纔會在接下來打開的頁面裏生效。爲了可以讓新的Service Worker及時生效,咱們使用skipWaiting
直接使Service Worker跳過等待時期,從而直接進入下一個階段。ajax
const CACHE_NAME = 'cache_v' + 2;
const CACGE_LIST = [
'/',
'/index.html',
'/main.css',
'/app.js',
'/icon.png'
];
function preCache() {
// 安裝成功後操做 CacheStorage 緩存,使用以前須要先經過 caches.open() 打開對應緩存空間。
return caches.open(CACHE_NAME).then(cache => {
// 經過 cache 緩存對象的 addAll 方法添加 precache 緩存
return cache.addAll(CACGE_LIST);
})
}
// 安裝
self.addEventListener('install', function (event) {
// 等待promise執行完
event.waitUntil(
// 若是上一個serviceWorker不銷燬 須要手動skipWaiting()
preCache().then(skipWaiting)
);
});
複製代碼
在安裝成功後,便會觸發activate
事件,在進入這個生命週期後,咱們通常會刪除掉以前已通過期的版本(由於默認狀況下瀏覽器是不會自動刪除過時的版本的),並更新客戶端Service Worker(使用當前處於激活狀態的Service Worker)。編程
// 刪除過時緩存
function clearCache() {
return caches.keys().then(keys => {
return Promise.all(keys.map(key => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
}))
})
}
// 激活 activate 事件中一般作一些過時資源釋放的工做
self.addEventListener('activate', function (e) {
e.waitUntil(
Promise.all([
clearCache(),
self.clients.claim()
])
);
});
複製代碼
在這裏還有一個問題就是sw.js文件有可能會被瀏覽器緩存,因此咱們通常須要設置sw.js不緩存或者較短的緩存時間 更多詳細參考 如何優雅的爲 PWA 註冊 Service Worker
以前說過,Service Worker 是能夠攔截請求的,那麼必定就會存在一個攔截請求的事件fetch。咱們須要在sw.js去監聽這個事件。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(function (response) {
// 若是 Service Worker 有本身的返回,就直接返回,減小一次 http 請求
if (response) {
console.log('cache 緩存', event.request.url, response);
return response;
} else {
if (navigator.online) {
return fetch(event.request).then(function(response) {
console.log('network', event.request.url, response);
// 因爲響應是一個JavaScript或者HTML,會認爲這個響應爲一個流,而流是隻能被消費一次的,因此只能被讀一次
// 第二次就會報錯 參考文章https://jakearchibald.com/2014/reading-responses/
cache.put(event.request, response.clone());
return response;
}).catch(function(error) {
console.error('請求失敗', error);
throw error;
});
} else {
// 斷網處理
offlineRequest(fetchRequest);
}
}
});
})
);
});
複製代碼
這裏咱們在fetch事件中監聽請求事件,咱們經過cache.match來進行請求的比較,若是存再這個請求的響應咱們就直接返回緩存結果,不然就去請求。在這裏咱們經過cache.add來添加新的緩存,他實際上內部是包含了fetch請求過程的(注意:Cache.put, Cache.add和Cache.addAll只能在GET請求下使用)。在match的時候,須要請求的url和header都一致纔是相同的資源,能夠設定第二個參數ignoreVary:true。caches.match(event.request, {ignoreVary: true})
表示只要請求url相同就認爲是同一個資源。另外須要提到一點,Fetch 請求默認是不附帶 Cookies 等信息的,在請求靜態資源上這沒有問題,並且節省了網絡請求大小。但對於動態頁面,則可能會由於請求缺失 Cookies 而存在問題。此時能夠給 Fetch 請求設置第二個參數。示例:fetch(fetchRequest, { credentials: 'include' } );
Cache API 不只在Service Worker中可使用,在主頁面中也可使用。咱們經過 caches.open(cacheName)來打開一個緩存空間,在,默認狀況下,若是咱們不手動去清除這個緩存空間,這個緩存會一直存在,不會過時。在使用Cache API以前,咱們都須要經過caches.open先去打開這個緩存空間,而後在使用相應的Cache方法。這裏有幾個注意點:
在使用cache.add和cache.addAll的時候,是先根據url獲取到相應的response,而後再添加到緩存中。過程相似於調用 fetch(), 而後使用 Cache.put() 將response添加到cache中
Fetch API不只能夠在主線程中進行使用,也能夠在Service Worker中進行使用。fetch 和 XMLHttpRequest有兩種方式不一樣:
當接收到一個表明錯誤的 HTTP 狀態碼時,從 fetch()返回的 Promise 不會被標記爲 reject, 即便該 HTTP 響應的狀態碼是 404 或 500。相反,它會將 Promise 狀態標記爲 resolve (可是會將 resolve 的返回值的 ok 屬性設置爲 false ),僅當網絡故障時或請求被阻止時,纔會標記爲 reject。
默認狀況下,fetch 不會從服務端發送或接收任何 cookies, 若是站點依賴於用戶 session,則會致使未經認證的請求(要發送 cookies,必須設置 credentials 選項)
// Example POST method implementation:
postData('http://example.com/answer', {answer: 42})
.then(data => console.log(data)) // JSON from `response.json()` call
.catch(error => console.error(error))
function postData(url, data) {
// Default options are marked with *
return fetch(url, {
body: JSON.stringify(data), // must match 'Content-Type' header
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include(始終攜帶), same-origin(同源攜帶cookie), omit(始終不攜帶)
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json'
},
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, cors, *same-origin
redirect: 'follow', // manual, *follow, error
referrer: 'no-referrer', // *client, no-referrer
})
.then(response => response.json()) // parses response to JSON
}
複製代碼
更多信息請查閱:使用 Fetch
Notification API 用來進行瀏覽器通知,當用戶容許時,瀏覽器就能夠彈出通知。這個API在主頁面和Service Worker中均可以使用,MDN文檔
// 先檢查瀏覽器是否支持
if (!("Notification" in window)) {
alert("This browser does not support desktop notification");
}
// 檢查用戶是否贊成接受通知
else if (Notification.permission === "granted") {
// If it's okay let's create a notification
new Notification(title, {
body: desc,
icon: '/icon.png',
requireInteraction: true
});
}
// 不然咱們須要向用戶獲取權限
else if (Notification.permission !== 'denied') {
Notification.requestPermission(function (permission) {
// 若是用戶贊成,就能夠向他們發送通知
if (permission === "granted") {
new Notification(title, {
body: desc,
icon: '/icon.png',
requireInteraction: true
});
} else {
console.warn('用戶拒絕通知');
}
});
}
複製代碼
// 發送 Notification 通知
function sendNotify(title, options={}, event) {
if (Notification.permission !== 'granted') {
console.log('Not granted Notification permission.');
// 經過post一個message信號量,來在主頁面中詢問用戶獲取頁面通知權限
postMessage({
type: 'applyNotify'
})
} else {
// 在Service Worker 中 觸發一條通知
self.registration.showNotification(title || 'Hi:', Object.assign({
body: '這是一個通知示例',
icon: '/icon.png',
requireInteraction: true
}, options));
}
}
複製代碼
咱們能夠看見當咱們在Service Worker中進行消息提示時,用戶可能關閉了消息提示的功能,因此咱們首先要再次詢問用戶是否開啓消息提示的功能,可是在Service Worker中是不可以直接詢問用戶的,咱們必需要在主頁面中去詢問,這個時候咱們能夠經過postMessage去發送一個信號量,根據這個信號量的類型,來作響應的處理(例如:詢問消息提示的權限,DOM操做等等)
function postMessage(data) {
self.clients.matchAll().then(clientList => {
clientList.forEach(client => {
// 當前打開的標籤頁發送消息
if (client.visibilityState === 'visible') {
client.postMessage(data);
}
})
})
}
複製代碼
在這裏咱們只向打開的標籤頁發送該信號量,避免重複詢問
因爲Service Worker是一個單獨的瀏覽器線程,與JavaScript主線程互不干擾,可是咱們仍是能夠經過postMessage實現通訊,並且能夠經過post特定的消息,從而讓主線程去進行相應的DOM操做,實現間接操做DOM的方式。
function sendMsg(msg) {
const controller = navigator.serviceWorker.controller;
if (!controller) {
return;
}
controller.postMessage(msg, []);
}
// 在 serviceWorker 註冊成功後,頁面上便可經過 navigator.serviceWorker.controller 發送消息給它
navigator.serviceWorker
.register('/test/sw.js', {scope: '/test/'})
.then(registration => console.log('ServiceWorker 註冊成功!做用域爲: ', registration.scope))
.then(() => sendMsg('hello sw!'))
.catch(err => console.log('ServiceWorker 註冊失敗: ', err));
複製代碼
在 ServiceWorker 內部,能夠經過監聽 message 事件便可得到消息:
self.addEventListener('message', function(ev) {
console.log(ev.data);
});
複製代碼
// self.clients.matchAll方法獲取當前serviceWorker實例所接管的全部標籤頁,注意是當前實例 已經接管的
self.clients.matchAll().then(clientList => {
clientList.forEach(client => {
client.postMessage('Hi, I am send from Service worker!');
})
});
複製代碼
在主頁面中監聽
navigator.serviceWorker.addEventListener('message', event => {
console.log(event.data);
});
複製代碼
3 manifest.json 做用 PWA 添加至桌面的功能實現依賴於 manifest.json,也就是說若是要實現添加至主屏幕這個功能,就必需要有這個文件
{
"short_name": "短名稱",
"name": "這是一個完整名稱",
"icons": [
{
"src": "icon.png",
"type": "image/png",
"sizes": "144x144"
}
],
"start_url": "index.html"
}
複製代碼
<link rel="manifest" href="path-to-manifest/manifest.json">
name —— 網頁顯示給用戶的完整名稱
short_name —— 當空間不足以顯示全名時的網站縮寫名稱
description —— 關於網站的詳細描述
start_url —— 網頁的初始 相對 URL(好比 /)
scope —— 導航範圍。好比,/app/的scope就限制 app 在這個文件夾裏。
background-color —— 啓動屏和瀏覽器的背景顏色
theme_color —— 網站的主題顏色,通常都與背景顏色相同,它能夠影響網站的顯示
orientation —— 首選的顯示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary。
display —— 首選的顯示方式:fullscreen, standalone(看起來像是native app),minimal-ui(有簡化的瀏覽器控制選項) 和 browser(常規的瀏覽器 tab)
icons —— 定義了 src URL, sizes和type的圖片對象數組。
複製代碼
使用workbox,若是使用webpack進行項目打包,咱們可使用workbox-webpack-plugin插件
Web Storage(例如 LocalStorage 和 SessionStorage)是同步的,不支持網頁工做線程,並對大小和類型(僅限字符串)進行限制。 Cookie 具備自身的用途,但它們是同步的,缺乏網頁工做線程支持,同時對大小進行限制。WebSQL 不具備普遍的瀏覽器支持,所以不建議使用它。File System API 在 Chrome 之外的任意瀏覽器上都不受支持。目前正在 File and Directory Entries API 和 File API 規範中改進 File API,但該 API 還不夠成熟也未徹底標準化,所以沒法被普遍採用。
同步的問題 就是負擔大,若是有大量請求緩存在本地緩存中,若是是同步,可能負擔重
resulted in a network error response: a Response whose "body" is locked cannot be used to respond to a request
這是由於在使用put的時候,是流的一個pipe操做,流是隻能被消費一次的。咱們能夠clone這個response或者reques參考文章
使用某些webpack插件,例如offline-plugin或者webpack-workbox-plugin
博客GitHub地址(歡迎star)
我的公衆號