PWA
全稱 Progressive Web Apps
(漸進式 Web
應用程序),旨在使用現有的 Web
技術提供用戶更優的使用體驗。 基本要求javascript
可靠(Reliable)
一方面是指 PWA
的安全性,PWA
只能運行在 HTTPS
上;另外一方面是指在網絡不穩定或者沒網狀況下,PWA
依然能夠訪問。快速響應(Fast)
快速響應用戶的交互行爲,而且具備平滑流暢的動畫、加載速度、渲染速度和渲染性能等。粘性(Engaging)
經過添加到桌面以及離線消息推送,能帶來用戶的第二次訪問,而且依靠良好的用戶體驗吸引用戶再次訪問。官網連接:Progressive Web Appscss
DEMO地址:夢魘小棧html
PWA
不是一項單獨的技術,技術包括 Manifest
、Service Worker
、Push API
& Notification API
、App Shell
& App Skeleton
等等技術,接下來咱們重點介紹幾項技術以及相關問題的解決方法。java
manifest
是支持站點在主屏上建立圖標的技術方案,而且定製 PWA 的啓動畫面的圖標和顏色等,以下圖:web
chrome > 桌面圖標 > 啓動樣式 > 打開效果 ![]()
manifest
內容{
"name": "夢魘小棧-專一於分享",
"short_name": "夢魘小棧",
"description": "心,若沒有棲息的地方,到哪裏都是流浪......",
"start_url": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#ffffff",
"theme_color": "#8a00f9",
"icons": [
{
"src": "images/icons/icon_32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "images/icons/icon_72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/icons/icon_128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "images/icons/icon_144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/icons/icon_192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icons/icon_256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "images/icons/icon_512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
複製代碼
manifest
屬性name
— 網頁顯示給用戶的完整名稱;short_name
— 這是爲了在沒有足夠空間顯示 Web
應用程序的全名時使用;description
— 關於網站的詳細描述;start_url
— 網頁的初始相對 URL
好比 /
)display
— 應用程序的首選顯示模式;
fullscreen
- 全屏顯示;standalone
- 應用程序將看起來像一個獨立的應用程序;minimal-ui
- 應用程序將看起來像一個獨立的應用程序,但會有瀏覽器地址欄;browser
- 該應用程序在傳統的瀏覽器標籤或新窗口中打開.orientation
— 應用程序的首選顯示方向;
any
natural
landscape
landscape-primary
landscape-secondary
portrait
portrait-primary
portrait-secondary
background_color
— 啓動屏的背景顏色;theme_color
— 網站的主題顏色;icons
— 定義了 src
、sizes
和 type
的圖片對象數組,各類環境中用做應用程序圖標的圖像對象數組.
MDN
提供了完整的manifest
屬性列表: Web App Manifest propertieschrome
manifest
功能雖然強大,可是技術上並不難,就是一個外鏈的json
文件,經過link
來引入:json
<!-- 在 html 頁面中添加如下 link 標籤 -->
<link rel="manifest" href="/manifest.json" />
複製代碼
在開發者工具中的 Application Tab 左邊有 Manifest 選項,你能夠驗證你的 manifest JSON 文件,並提供了 "Add to homescreen" .bootstrap
Service Worker
是 PWA
中最重要的概念之一,它是一個特殊的 Web Worker
,獨立於瀏覽器的主線程運行,特殊在它能夠攔截用戶的網絡請求,而且操做緩存,還支持 Push
和後臺同步等功能。api
在 install Service Worker
以前,要在主進程 JavaScript
代碼裏面註冊它,註冊是爲了告訴瀏覽器咱們的 Service Worker
文件是哪一個,而後在後臺,Service Worker
就開始安裝激活。數組
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(() => {
console.log('註冊成功!')
})
})
}
複製代碼
註冊時,還能夠指定可選參數 scope,scope 是 Service Worker 能夠以訪問到的做用域,或者說是目錄。
navigator.serviceWorker.register('/sw.js', {
scope: '/app/'
})
複製代碼
註冊成功後,您能夠經過轉至 chrome://inspect/#service-workers
並尋找您的網站來檢查 Service Worker
是否已啓用。
install
事件綁定在 Service Worker
文件中,當安裝成功後,install
事件就會被觸發。 通常咱們會在 install
事件裏面進行緩存的處理,用到以前提到的 Cahce API
,它是一個 Service Worker
上的全局對象,能夠緩存網絡相應的資源,並根據他們的請求生成 key
,這個 API
和瀏覽器標準的緩存工做原理類似,可是隻是針對本身的 scope
域的,緩存會一直存在,知道手動清楚或者刷新。
const cacheName = 'bs-0-0-1' // 緩存名稱
const cacheFiles = ['/', '/favicon.ico', '/images/icons/icon_32.png', '...'] // 需緩存的文件
// 監聽 install 事件,安裝完成後,進行文件緩存
self.addEventListener('install', e => {
e.waitUntil(
caches
.open(cacheName)
.then(cache => {
console.log('Opened cache')
return cache.addAll(cacheFiles)
})
.then(() => self.skipWaiting())
)
})
// e.waitUntil 確保 Service Worker 不會在 e.waitUntil() 執行完成以前安裝完成。
// caches.open(cacheName) 建立一個 cacheName 的新緩存,返回一個緩存的 promise 對象,當它 resolved 時候,咱們在 then 方法裏面用 caches.addAll 來添加想要緩存的列表,列表是一個數組,裏面的 URL 是相對於 origin 的。
// self.skipWaiting() 跳過 waiting 狀態,下面更新第3條~
複製代碼
當你的 Service Worker
須要更新時, 須要通過如下步驟
JavaScript
文件。 用戶導航至您的站點時,瀏覽器會嘗試在後臺從新下載定義 Service Worker
的腳本文件。 若是 Service Worker
文件與其當前所用文件存在字節差別,則將其視爲新 Service Worker。Service Worker
將會啓動,且將會觸發 install
事件。Service Worker
仍控制着當前頁面,所以新 Service Worker
將進入 waiting
狀態。Service Worker
將會被終止,新 Service Worker
將會取得控制權。Service Worker
取得控制權後,將會觸發其 activate
事件。若是但願在有了新版本時,全部的頁面都獲得及時自動更新怎麼辦呢?能夠在 install 事件中執行 self.skipWaiting() 方法跳過 waiting 狀態,而後會直接進入 activate 階段。接着在 activate 事件發生時,經過執行 self.clients.claim() 方法,更新全部客戶端上的 Service Worker。
當 Service Worker
安裝完成後並進入激活狀態,會觸發 activate
事件。經過監聽 activate
事件你能夠作一些預處理,如對舊版本的更新、對無用緩存的清理等。
// 監聽 activate 事件,激活後經過cache的key來判斷是否更新、刪除 cache 中的靜態資源
self.addEventListener('activate', e => {
console.log('sw: activate')
e.waitUntil(
caches
.keys()
.then(keys => {
return Promise.all(
keys.map(key => {
if (key !== cacheName && key !== apiCacheName) {
return caches.delete(key)
}
})
)
})
.then(() => self.clients.claim()) // 更新客戶端
)
})
複製代碼
在 Service Worker
的做用域中,當有網絡請求時發生時,fetch
事件將被觸發。它調用 respondWith()
方法來劫持網絡請求緩存並返回:
var apiCacheName = 'api-0-1-1'
self.addEventListener('fetch', e => {
var currentUrl = e.request.url
// 只處理同源
if (new URL(currentUrl).hostname != location.hostname) {
return
}
// 須要緩存的 xhr 請求
var cacheRequestUrls = ['/message.json', '/manifest.json']
// 判斷當前請求是否須要緩存
var needCache = cacheRequestUrls.includes(new URL(currentUrl).pathname)
if (needCache) {
// 須要緩存
// 使用 fetch 請求數據,並將請求結果 clone 一份緩存到 cache
// 此部分緩存後在 browser 中使用全局變量 caches 獲取
caches.open(apiCacheName).then(cache => {
return fetch(e.request).then(response => {
cache.put(e.request.url, response.clone())
return response
})
})
} else {
// 不須要緩存,直接查詢 cache
// 若是有 cache 則直接返回,不然經過 fetch 請求
e.respondWith(
caches
.match(e.request)
.then(cache => {
return cache || fetch(e.request)
})
.catch(err => {
console.log('respondWithErr:', err)
return fetch(e.request)
})
)
}
})
複製代碼
到這裏,離線緩存動靜態資源就完成了。
至此,咱們完成了 PWA
的兩大基本功能:Web App Manifest
和 Service Worker
的離線緩存。這兩大功能能夠很好地提高用戶體驗與應用性能。咱們用 Chrome
中的 Lighthouse
來檢測一下目前的應用:
能夠看到,在 PWA
評分上,咱們的這個 WebApp
已經很是不錯了。
完整代碼 -> 夢魘小棧 PWA 完整代碼
var cacheName = 'bs-0-0-2'
var apiCacheName = 'api-0-0-2'
var cacheFiles = [
'/',
'/favicon.ico?v=6.2.0',
'/css/main.css?v=6.2.0',
'/js/src/set.js',
'/js/src/utils.js',
'/js/src/motion.js',
'/js/src/bootstrap.js',
'/images/cursor.ico',
'/images/icons/icon_32.png',
'/images/icons/icon_72.png',
'/images/icons/icon_128.png',
'/images/icons/icon_192.png',
'/images/icons/icon_256.png',
'/images/icons/icon_512.png'
]
// 監聽 install 事件,安裝完成後,進行文件緩存
self.addEventListener('install', e => {
console.log('sw: install')
e.waitUntil(
caches
.open(cacheName)
.then(cache => {
console.log('Opened cache')
return cache.addAll(cacheFiles)
})
.then(() => self.skipWaiting())
)
})
// 監聽 activate 事件,激活後經過 cache 的 key 來判斷是否更新 cache 中的靜態資源
self.addEventListener('activate', e => {
console.log('sw: activate')
e.waitUntil(
caches
.keys()
.then(keys => {
return Promise.all(
keys.map(key => {
if (key !== cacheName && key !== apiCacheName) {
return caches.delete(key)
}
})
)
})
// 更新客戶端
.then(() => self.clients.claim())
)
})
self.addEventListener('fetch', e => {
var currentUrl = e.request.url
// 只處理同源
if (new URL(currentUrl).hostname != location.hostname) {
return
}
// 須要緩存的 xhr 請求
var cacheRequestUrls = ['/message.json', '/manifest.json']
// 判斷當前請求是否須要緩存
var needCache = cacheRequestUrls.includes(new URL(currentUrl).pathname)
if (needCache) {
// 須要緩存
// 使用 fetch 請求數據,並將請求結果 clone 一份緩存到 cache
// 此部分緩存後在 browser 中使用全局變量 caches 獲取
caches.open(apiCacheName).then(cache => {
return fetch(e.request).then(response => {
cache.put(e.request.url, response.clone())
return response
})
})
} else {
// 不須要緩存,直接查詢 cache
// 若是有 cache 則直接返回,不然經過 fetch 請求
e.respondWith(
caches
.match(new URL(currentUrl).pathname)
.then(cache => {
return cache || fetch(e.request)
})
.catch(err => {
console.log('respondWithErr:', err)
return fetch(e.request)
})
)
}
})
複製代碼
因爲如今博客僅需 Manifest
、Service Worker
後面的技術、Push API
& Notification API
、App Shell
& App Skeleton
等打算之後有時間在考慮場景加上~