渐进式 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消息推送(传送门)