一.What?css
A new way to deliver amazing user experiences on the web.
一种提高Web用户体验的方式。除了Web天生的(便捷)体验外,还有3个特色:Reliable, Fast, Engaginghtml
可靠:在不肯定的网络环境下,也能当即加载,而不会(由于断网而)瞬间回到远古时代git
可靠指的是离线缓存,断网状态走缓存,保证离线场景仍然可用,service worker配合cache API创建缓存-代理机制github
快速:迅速以丝滑的动画做为交互反馈,而不存在掉帧卡顿的滚动web
快速,只是强调交互反馈“感受快”,与推崇的Material Design有关,并无真正的速度优点(至少首屏没有)shell
另外,得益于缓存-代理机制,再次访问时走本地缓存会至关快json
类native:像设备原生App同样,具备沉浸式的用户体验(即全屏)promise
除了全屏外,还有主屏图标(让Web App在主屏幕有一席之地)和系统通知(“拉活”的能力),经过Web App Manifest配置来实现,依赖用户环境支持浏览器
P.S.Engaging这个抽象形容词真很差翻译,这里暂且取其实际意义,类native缓存
因此,表面上看,PWA的亮点分2部分:
(离线)缓存-代理机制
全屏,主屏图标和系统通知等类native特性
缓存机制在Web App/SPA里一点不新鲜,抽离出数据层以后,缓存顺手就作了。但侧重点不一样,PWA的缓存机制偏向于静态资源缓存,而Web App/SPA的缓存层多用来作动态内容缓存(上次的内容没过时的话,再也不从新获取动态部分,而是直接作客户端渲染)
至于全屏,主屏图标以及系统通知等类native特性,算是渐进加强中的加强,在支持的用户环境是可用的(一些浏览器提供了支持,但更普遍的WebView环境在不久的未来可能仍是不行)。但这代表Web正在以渐进加强的方式走出PC时代,向着移动化发展
二.试玩
依赖环境
HTTPS
要求服务源必须是安全的,因此须要HTTPS环境。除了出于Web信息安全的考虑,想要推动HTTPS普及也是一个重要缘由,HTTPS做为Web技术发展的必要基础设施,对于拍照,录音,push API等新特性,都须要得到用户许可,而HTTPS是权限工做流的关键部分,必不可少
P.S.在permission.site可以体验到HTTPS与HTTP环境在获取用户受权方面的差别
类native加强
经过引入Web App Manifest配置文件来实现类native加强,在支持PWA的浏览器生效(在不支持的环境最坏结果也就是多请求一个JSON文件):
<link rel="manifest" href="./manifest.json">
注意,有个比较类似的东西,叫Application Cache(HTML5特性,已过期),其manifest引入方式不一样:
<html manifest="example.appcache"> ... </html>
由于两者引入方式不一样,因此Web App Manifest与Application Cache是不相干的,没有历史包袱的后顾之忧
P.S.Application Cache对SPA支持较好,对多页应用则不适用,但存在不少问题,这里很少作介绍
主屏图标
Web App Manifest内容示例以下:
{ "short_name": "主屏显示的应用名称", "name": "安装banner显示的应用名称", "icons": [ { "src": "launcher-icon-1x.png", "type": "image/png", "sizes": "48x48", "density": "1.0" }, { "src": "launcher-icon-2x.png", "type": "image/png", "sizes": "96x96", "density": "1.0" }, { "src": "launcher-icon-4x.png", "type": "image/png", "sizes": "192x192", "density": "1.0" } ], "start_url": "index.html?launcher=true" }
P.S.安装banner是指一个相似于获取权限的弹出面板,用户能够选择添加至主屏幕或取消,知足必定条件的话,Chrome会自动弹出安装banner,具体见Web App Install Banners
这样理想状况下咱们就拥有了主屏图标,支持Web App Manifest的环境会选用最合适的(最接近48dp的)图标
注意:index.html里的内容应该是首屏渲染须要的最小化内容,为了达到首屏当即加载的效果,能够把带loading和默认占位图的页面框架做为App Shell展现出来。另外,为了达到秒开可用的首屏性能,Web App首屏性能优化其它常规手段在PWA也是推荐使用的,好比数据直出。如开篇所说,PWA并无天生的(首屏)性能优点,Web App适用的常规优化手段仍然是必要的
闪屏(Splash)
从主屏图标进入,可定制的启动过程显示内容包括:标题,背景色和图像。新配置项以下:
// 背景色 "background_color": "#2196F3", // 主题色,包括工具栏 "theme_color": "#2196F3",
图像从icons中选取最接近128dp的图像做为闪屏,不支持动图
另外,还能够指定显示模式和页面方向:
// 全屏(隐藏浏览器的UI) "display": "standalone", // 显示浏览器外壳,像打开书签同样 "display": "browser", // 横屏 "orientation": "landscape"
P.S.关于闪屏的示例及更多信息请查看Adding a Splash Screen for Installed Web Apps in Chrome 47
特别注意:若是manifest.json文件有更新,这些改动不会自动生效,除非用户从新添加应用到主屏
系统通知
与Web App Manifest无关,依赖Push API。简单示例以下:
// service-worker.js self.addEventListener("push", function (event) { event.waitUntil( self.self.registration.showNotification("发布新文章啦", { body: "有新文章发布啦,点击查看。" }) ); });
这里很少作介绍(目前(2017/12/15)几乎能够认为这个特性不存在),由于规范定义了API,但没规定统一个push协议,因此各家浏览器的push机制不一样,好比Chrome的GCM在咱们这片天空下就不可用
关于Push API的更多信息,请查看【Service Worker】消息推送功能“全军覆没”
缓存-代理
缓存分为几部分:
首屏静态资源缓存(预缓存)
已访问资源缓存(运行时缓存)
动态内容缓存(运行时缓存)
缓存是纯数据操做(包括持久化),而service worker可以在后台运行,尤为适合处理这种与页面及交互无关的事情,因此service worker与Cache API,Push API成了搭档。但service worker自身也应该看作“加强”项,在不支持service worker的环境应该跳过缓存机制保证基本的页面体验,简单的特征检测方案以下:
if ('serviceWorker' in navigator) { navigator.serviceWorker .register('./service-worker.js') .then(function() { console.log('Service Worker Registered'); }); }
service worker在install事件处理器完成包括App Shell在内的首屏静态资源缓存:
// service-worker.js var cacheName = 'weatherPWA-step-6-1'; var filesToCache = [ // 入口URL '/', '/index.html', '/scripts/app.js', '/styles/inline.css', // App Shell须要的资源 '/images/ic_add_white_24px.svg', '/images/ic_refresh_white_24px.svg', // 内容展现可能用到的资源 '/images/clear.png', '/images/cloudy-scattered-showers.png', '/images/cloudy.png', '/images/fog.png', '/images/partly-cloudy.png', '/images/rain.png', '/images/scattered-showers.png', '/images/sleet.png', '/images/snow.png', '/images/thunderstorm.png', '/images/wind.png' ]; self.addEventListener('install', function(e) { console.log('[ServiceWorker] Install'); e.waitUntil( caches.open(cacheName).then(function(cache) { console.log('[ServiceWorker] Caching app shell'); //! 只要有一个失败就不接着作下一个了 return cache.addAll(filesToCache); }) ); });
固然,还须要对缓存作基本的版本控制:
// service-worker.js self.addEventListener('activate', function(e) { console.log('[ServiceWorker] Activate'); e.waitUntil( caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { // 觉得cacheName为cache key,若是存在旧的缓存,删除掉 if (key !== cacheName) { console.log('[ServiceWorker] Removing old cache', key); return caches.delete(key); } })); }) ); // 要求当即激活service worker,避免边界case return self.clients.claim(); });
P.S.边界case指的是某些状况下service worker没法马上恢复激活态,致使不走缓存。为了屏蔽这些边界case,推荐使用GoogleChromeLabs/sw-precache帮助处理缓存控制问题(包括过时,更新策略等等)
缓存有了,接下来实现代理部分,拦截请求,并把缓存内容做为响应:
// service-worker.js // 拦截请求 self.addEventListener('fetch', function(event) { console.log('[ServiceWorker] Fetch', e.request.url); // 自定义响应内容 e.respondWith( // 查找缓存,没有才请求 caches.match(e.request).then(function(response) { return response || fetch(e.request); }) ); });
到这里基本的缓存-代理机制就准备好了,咱们作了这些事情:
按资源列表预先缓存静态资源
拦截请求
把缓存内容做为响应给过去
有3个注意事项:
浏览器缓存可能会影响缓存更新,因此install事件处理器中的请求不会走缓存,而是直接进入网络
注销service worker不会清掉缓存,cache key不变的话,以后还会拿到旧的缓存内容
默认新注册的service worker在页面从新载入以后才会生效,除非作特殊处理
另外,咱们的简版实现还存在一些问题,例如:
缓存版本控制依赖一个静态的cache key,每次更新service-worker.js都要修改这个key
一旦cache key有变化,会抹掉全部缓存,从新请求一遍,对于静态资源有些浪费
缺乏运行时缓存,资源列表不够灵活,指望更强大的边访问边缓存
第1个问题没什么太好的办法,第2个问题能够经过细分资源类型来缓解,例如:
// Shorthand identifier mapped to specific versioned cache. var CURRENT_CACHES = { font: 'font-cache-v' + FONT_CACHE_VERSION, css: 'css-cache-v' + CSS_CACHE_VERSION, img: 'img-cache-v' + IMG_CACHE_VERSION };
经过更细粒度的版本控制,能在必定程度上下降强制更新缓存的成本,固然,缓存层下面还有HTTP Cache兜底,缓存更新成本不是很是关键
至于运行时缓存,实际上只须要再作最后一小步就行了:
没命中缓存的话,请求资源并缓存
具体以下:
// 查找缓存,没有才请求 caches.match(e.request).then(function(response) { return response || fetch(e.request).then(function(res) { return caches.open(dataCacheName).then(function(cache) { // 并缓存起来 cache.put(e.request.url, res.clone()); return res; ) }) })
另外,还能够根据资源类型及场景要求,针对性的选用合适的缓存策略,例如:
// service-worker.js self.addEventListener('fetch', function(e) { console.log('[Service Worker] Fetch', e.request.url); var dataUrl = 'https://cache.domain.com/fresh/'; // 策略1:有实时性要求的资源,请求优先,fetch then cache if (e.request.url.indexOf(dataUrl) > -1) { e.respondWith( caches.open(dataCacheName).then(function(cache) { return fetch(e.request).then(function(response){ cache.put(e.request.url, response.clone()); return response; }); }) ); } else { // 策略2:通常资源,缓存优先,cache falling back to fetch } });
P.S.更多缓存策略,见参考资料部分
三.Demo
官方Demo:Weather PWA,可能没法正常访问
搬运Demo(把官方Demo挪到github pages):https://ayqy.github.io/pwa/demo/weather-pwa/index.html
P.S.github pages很是适合用做试验田,稳定可靠的HTTPS,发布内容没有任何限制能够随便折腾,之后的博客Demo都会逐步迁移过去(以前一直放在本身的FTP,那可真蠢..)
weather-pwa
不太乐观的消息:事实上,故意精心准备了用户环境(官方正版Chrome + 官方Demo),在小米4上没有自动弹出安装banner(多是操做姿式等条件不知足,见上文),手动点击“添加至主屏幕”,toast添加成功,但主屏幕上啥也没有……这就是提不起兴趣手写Demo试玩的缘由(固然,主要缘由是懒;))
四.案例
阿里巴巴国际站
AliExpress
饿了么:奇怪,为何没有感觉到Cache的做用呢
注意,隐身模式可能会致使阿里巴巴国际站的service worker抛以下错误:
Uncaught (in promise) DOMException: Quota exceeded.
正常环境可正常体验
P.S.更多案例,请查看Case Studies | Web | Google | Developers
五.应用场景
简言之,PWA算是Web App的升级版,主要亮点是类native支持。以渐进加强的方式,不须要过高成本就能完成Web App到PWA的“升级”,让部分用户(支持PWA的环境)得到更快(缓存)更便捷(主屏图标)的类native体验(全屏)
那么具体应用场景分如下几种:
缓存能带来明显收益的Web App
指望具备离线能力,或类native体验,或者单纯只是想要个主屏图标的Web应用
指望蹭个技术热点/协助推进其发展的Web应用或浏览器供应商
无论应用场景,话说回来,正如zxx某篇关于缓存(仍是worker?)的文章所说,这么点儿成本就能让页面得到离线能力,真切看到缓存带来的收益,何乐而不为呢?
另外,Angular,React,Vue等主流框架都提供了PWA脚手架,具体请查看The Ultimate Guide to Progressive Web Applications
参考资料
The offline cookbook:缓存策略图解,好东西,待翻译,就着ServiceWorker Cookbook一块儿看
如何看待 Progressive Web Apps 的发展前景?
Your First Progressive Web App:官方教程
A Beginner’s Guide To Progressive Web Apps:比较全面的入门指南
改造你的网站,变身PWA:原文Retrofit Your Website as a Progressive Web App