漸進式 Web 應用首先是一種應用,它根據設備的支持狀況來提供更多功能,提供離線能力,推送通知,甚至原生應用的外觀和速度,以及對資源進行本地緩存。css
漸進式 Web 應用是一個網站,它使用了某些開發技術,使其體驗比普通針對移動優化的網站體驗更好。它使用起來就像是在使用一個原生應用同樣html
漸進式 Web 應用多是一個不清晰的術語,而更好的定義是:它們是一種 Web 應用,利用現代瀏覽器特性(好比 Web Worker 和 Web 應用清單),讓移動設備對其「升級」,使之成爲一等公民角色的應用程序。webpack
PWA結合了最好的Web應用和最好的原生應用的用戶體驗。包含如下:git
漸進式 Web 應用的定義中有部分是這樣說的:它必須支持離線工做。github
因爲容許 Web 應用程序脫機工做的是 Service Worker,這意味着 Service Worker 是漸進式 Web 應用強制要求的部分。web
漸進式Web應用程序須要使用HTTPS鏈接。雖然使用HTTPS會讓您服務器的開銷變多,但使用HTTPS可讓您的網站變得更安全 ,如何給網站開啓https編程
應用程序清單提供了和當前漸進式Web應用的相關信息,如:json
本質上講,程序清單是頁面上用到的圖標和主題等資源的元數據。api
程序清單是一個位於您應用根目錄的JSON文件。該JSON文件返回時必須添加Content-Type: application/manifest+json 或者 Content-Type: application/jsonHTTP頭信息。程序清單的文件名不限,在本文的示例代碼中爲manifest.json:跨域
// manifest.json { "dir": "ltr", "lang": "en", "name": "D.D Blog", "scope": "/", "display": "standalone", "start_url": "/", "short_name": "D.D Blog", "theme_color": "transparent", "description": "Share More, Gain More. - D.D Blog", "orientation": "any", "background_color": "transparent", "related_applications": [], "prefer_related_applications": false, "icons": [{ "src": "assets/img/logo/size-32.png", "sizes": "32x32", "type": "image/png" }, { "src": "assets/img/logo/size-48.png", "sizes": "48x48", "type": "image/png" } //... ], "gcm_sender_id": "...", "applicationServerKey": "..." }
程序清單文件創建完以後,你須要在每一個頁面上引用該文件:
<link rel="manifest"href="/manifest.json">
如下屬性在程序清單中常用,介紹說明以下:
manifest注意事項
- 站點離線存儲的容量限制是5M
- 若是manifest文件,或者內部列舉的某一個文件不能正常下載,整個更新過程將視爲失敗,瀏覽器繼續所有使用老的緩存
- 引用manifest的html必須與manifest文件同源,在同一個域下
- 在manifest中使用的相對路徑,相對參照物爲manifest文件
- CACHE MANIFEST字符串應在第一行,且必不可少
- 系統會自動緩存引用清單文件的 HTML 文件
- manifest文件中CACHE則與NETWORK,FALLBACK的位置順序沒有關係,若是是隱式聲明須要在最前面
- FALLBACK中的資源必須和manifest文件同源
- 當一個資源被緩存後,該瀏覽器直接請求這個絕對路徑也會訪問緩存中的資源。
- 站點中的其餘頁面即便沒有設置manifest屬性,請求的資源若是在緩存中也從緩存中訪問
- 當manifest文件發生改變時,資源請求自己也會觸發更新
Service Worker 是一個可編程的服務器代理,它能夠攔截或者響應網絡請求。ServiceWorker 是位於應用程序根目錄的一個個的JavaScript文件。
您須要在頁面對應的JavaScript文件中註冊該ServiceWorker:
//main.js if ('serviceWorker' in navigator) { // 註冊 service worker navigator.serviceWorker.register('/service-worker.js'); }
若是您不須要離線的相關功能,您能夠只建立一個 /service-worker.js文件,這樣用戶就能夠直接安裝您的Web應用了!
Service Worker這個概念可能比較難懂,它實際上是一個工做在其餘線程中的標準的Worker,它不能夠訪問頁面上的DOM元素,沒有頁面上的API,可是能夠攔截全部頁面上的網絡請求,包括頁面導航,請求資源,Ajax請求。
上面就是使用全站HTTPS的主要緣由了。假設您沒有在您的網站中使用HTTPS,一個第三方的腳本就能夠從其餘的域名注入他本身的ServiceWorker,而後篡改全部的請求——這無疑是很是危險的。
Service Worker 會響應三個事件:install,activate和fetch。
Service Worker 和 Web Worker 不是同一個東西 ,不要搞混淆了
該事件將在應用安裝完成後觸發。咱們通常在這裏使用CacheAPI緩存一些必要的文件。
首先,咱們須要提供以下配置
// configuration const version = '1.0.0', CACHE = version + '::PWAsite', offlineURL = '/offline/', installFilesEssential = [ '/', '/manifest.json', '/css/styles.css', '/js/main.js', '/js/offlinepage.js', '/images/logo/logo152.png' ].concat(offlineURL), installFilesDesirable = [ '/favicon.ico', '/images/logo/logo016.png', '/images/hero/power-pv.jpg', '/images/hero/power-lo.jpg', '/images/hero/power-hi.jpg' ];
installStaticFiles() 方法使用基於Promise的方式使用CacheAPI將文件存儲到緩存中。
// 安裝 靜態資源 function installStaticFiles() { return caches.open(CACHE) .then(cache => { // 緩存靜態文件 cache.addAll(installFilesDesirable); // 緩存主要的文件 return cache.addAll(installFilesEssential); }); }
最後,咱們添加一個install的事件監聽器。waitUntil方法保證了service worker不會安裝直到其相關的代碼被執行。這裏它會執行installStaticFiles()方法,而後self.skipWaiting()方法來激活service worker:
// 程序安裝 self.addEventListener('install', event => { console.log('service worker: install'); // 緩存核心文件 event.waitUntil( installStaticFiles() .then(() => self.skipWaiting()) ); });
這個事件會在service worker被激活時發生。你可能不須要這個事件,可是在示例代碼中,咱們在該事件發生時將老的緩存所有清理掉了:
// 清理舊的緩存 function clearOldCaches() { return caches.keys() .then(keylist => { return Promise.all( keylist .filter(key => key !== CACHE) .map(key => caches.delete(key)) ); }); } // 程序激活 self.addEventListener('activate', event => { console.log('service worker: activate'); // 刪除舊的緩存 event.waitUntil( clearOldCaches() .then(() => self.clients.claim()) ); });
注意self.clients.claim()執行時將會把當前service worker做爲被激活的worker。
Fetch 事件該事件將會在網絡開始請求時發起。該事件處理函數中,咱們可使用respondWith()方法來劫持HTTP的GET請求而後返回:
// 程序獲取網絡數據 self.addEventListener('fetch', event => { // 放棄非get請求 if (event.request.method !== 'GET') return; let url = event.request.url; event.respondWith( caches.open(CACHE) .then(cache => { return cache.match(event.request) .then(response => { if (response) { // 還回緩存文件 console.log('cache fetch: ' + url); return response; } // 發起網絡請求 return fetch(event.request) .then(newreq => { console.log('network fetch: ' + url); if (newreq.ok) cache.put(event.request, newreq.clone()); return newreq; }) // 程序離線 .catch(() => offlineAsset(url)); }); }) ); });
offlineAsset(url)方法中使用了一些helper方法來返回正確的數據:
// 判斷是否是圖片資源? let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f); function isImage(url) { return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false); } // 返回離線資源 function offlineAsset(url) { if (isImage(url)) { // return image return new Response( '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>', { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'no-store' }} ); } else { // 返回頁面 return caches.match(offlineURL); } }
offlineAsset()方法檢查請求是否爲一個圖片,而後返回一個帶有「offline」文字的SVG文件。其餘請求將會返回 offlineURL 頁面。
Chrome開發者工具中的ServiceWorker部分提供了關於當前頁面worker的信息。其中會顯示worker中發生的錯誤,還能夠強制刷新,也可讓瀏覽器進入離線模式。
Cache Storage 部分例舉了當前全部已經緩存的資源。你能夠在緩存須要更新的時候點擊refresh按鈕。
離線頁面能夠是靜態的HTML,通常用於提醒用戶當前請求的頁面暫時沒法脫機使用。然而,咱們能夠提供一些能夠閱讀的頁面連接。
Cache API能夠在main.js中使用。然而,該API使用Promise,在不支持Promise的瀏覽器中會失敗,全部的JavaScript執行會所以受到影響。爲了不這種狀況,在訪問/js/offlinepage.js的時候咱們添加了一段代碼來檢查當前是否在離線環境中:
// 加載腳本以填充脫機頁列表 if (document.getElementById('cachedpagelist') && 'caches' in window) { var scr = document.createElement('script'); scr.src = '/js/offlinepage.js'; scr.async = 1; document.head.appendChild(scr); }
/js/offlinepage.js 中以版本號爲名稱保存了最近的緩存,獲取全部URL,刪除不是頁面的URL,將這些URL排序而後將全部緩存的URL展現在頁面上:
// 緩存名稱 const CACHE = '::PWAsite', offlineURL = '/offline/', list = document.getElementById('cachedpagelist'); // 獲取全部緩存 window.caches.keys() .then(cacheList => { // 按最近的查找緩存和排序 cacheList = cacheList .filter(cName => cName.includes(CACHE)) .sort((a, b) => a - b); // 打開第一個 caches.open(cacheList[0]) .then(cache => { // 獲取已經緩存的頁面 cache.keys() .then(reqList => { let frag = document.createDocumentFragment(); reqList .map(req => req.url) .filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL)) .sort() .forEach(req => { let li = document.createElement('li'), a = li.appendChild(document.createElement('a')); a.setAttribute('href', req); a.textContent = a.pathname; frag.appendChild(li); }); if (list) list.appendChild(frag); }); }) });
根據《深刻淺出Webpack》
只須要安裝 serviceworker-webpack-plugin 組件
//webpack config import ServiceWorkerWebpackPlugin from 'serviceworker-webpack-plugin'; plugins: [ new ServiceWorkerWebpackPlugin({ entry: path.join(__dirname, 'src/sw.js'), }), ], //mian.js import runtime from 'serviceworker-webpack-plugin/lib/runtime'; if ('serviceWorker' in navigator) { const registration = runtime.register(); } //sw.js { assets: [ './main.256334452761ef349e91.js', .... ], }
事實上能構建除想要的結果也能達到,離線緩存. 可是離線緩存文件除了圖片等靜態變的資源外, 每次打包構建的hash 他也會隨之改變, 不可能每次都手動修改靜態文件資源列表. 因而:
推薦使用sw-precache-webpack-plugin
//webpack config const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); plugins: [ new SWPrecacheWebpackPlugin( { cacheId: 'appName', dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', minify: true, navigateFallback: '/index.html', staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], } ), ] //main.js if ('serviceWorker' in navigator && process.env.NODE_ENV == "production") { navigator.serviceWorker.register('/service-worker.js'); }
每次編譯代碼以後會自動生成靜態資源列表.
能夠打開瀏覽器的調試器 Application -> Service Workers 看到 服務已經啓動
在Application -> Cache -> Cache Storage 裏面能夠看到緩存的靜態文件
Service Worker 本質上提供了相似 Web Worker 的功能,其做爲 Web Application 以及 Server 之間的代理服務器,能夠截獲用戶的請求。可是爲了實現離線緩存功能,還須要結合 Cache API。
使用 Cache Storage 還須要注意如下幾點:
在切換到 Network -> all 就能夠看到被緩存的文件的Size 那欄 (from ServiceWorker 不一樣於 from disk cache)
爲了驗證網頁在離線時能訪問的能力,須要在開發者工具中的 Network 一欄中經過 Offline 選項禁用掉網絡,再刷新頁面能正常訪問,而且網絡請求的響應都來自 Service Workers,正常的效果如圖:
使用分享功能,須要知足如下幾點:
// CommonService.js export const isSupportShareAPI = () => !!navigator.share; export const sharePage = () => { navigator .share({ title: document.title, text: document.title, url: window.location.href }) .then(() => console.info('Successful share.')) .catch(error => console.log('Error sharing:', error)); };
PWA消息推送(傳送門)