我喜欢移动app,并且也是那些坚持使用Web技术构建移动应用程序的人之一。javascript
通过技术的不断迭代(可能还有一些其它的东西),移动体验设计越来越平易近人,给予用户更好的体验。css
而今天,咱们就要介绍一个新技术--渐进式 web 应用程序。在理解这个概念并本身尝试了一下以后,我以为没有必要再作 hybrid 应用了。html
咱们准备作这样的一个demo:java
渐进式 Web 应用是典型的旨在提升用户离线体验的 Web 应用。它解决了这样的问题:怎么才能不显示相似下面的离线错误?jquery
事实上,PWA 不只解决了离线错误,还在恢复链接的时候将用户与内容链接起来。移动设备是渐进式 web 应用的主要使用场景。让我来告诉你为何?web
如上,用户对待两种场景的处理方式是不同的。移动端用户不必定有很好的网络链接,有的甚至没有。在这样的场景下,开发商须要作的就是保持用户对产品的好感,在其网络恢复时与其互动。若是信号不好,开发商须要经过一些手段保持用户的耐心,不至于在请求过程当中用户直接关闭 web 应用。chrome
当咱们开始构建 PWA 应用时,你就能理解上面的场景了。shell
PWA 背后的原理是 service workers。若是想让用户在离线场景下依然保持打开 web 页面,你须要在用户打开 web 应用而且有网络链接时作一些“后台任务”,这个“后台任务”会搜集 web 页面最近一次运行须要的一些资源,以备离线时使用。api
这就好像每一年秋收储备粮食,以备冬天不时之需同样,不断循环。promise
PWA 中的 service worker,能够类比成春天的播种的农民。下面是 MDN 对 service workers 的描述:
Service worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你能够彻底控制应用在特定情形(最多见的情形是网络不可用)下的表现。
简而言之,service worker 就是一些在后台运行逻辑的 worker。它没有权限操做 DOM,可是能够调用其它的 API (例如 IndexDB 以及 Fetch API)。
开始以前请牢记:
demo/sw.js
只能相对于 demo
起做用,demo/first/sw.js
相对于 first
。若是你能利用 service workers 存储离线使用所需的文件,那你就没有必要开发移动 app 了。若是你的 web 应用对移动用户进行了优化,而且几乎不须要调用移动端的硬件功能,那么你应该尝试一下 PWA。
我花了一些时间看飞行模式下一些移动 app 的表现。我将它们分红三类:
例子: Coinbase
Coinbase 就是一直停留在 loading 的这个页面。它甚至让我怀疑这样的 app 为啥要存在,由于这个页面简直跟 web 展现如出一辙。Coinbase 不是财经类 app,无需实时展现信息,所以,PWA 可能只适用应用于其 App Shell。
App Shell 是指不包含动态内容的一部分应用程序。例如导航菜单、侧边栏、背景、logo 等等。
例子:Uber
Uber 给用户展现了一些信息(经过 App Shell 以及地图),而且告知用户不能操做是因为他网络中断了。Uber是一个很高频的 app,这样的交互展现对于他们的应用场景颇有意义。
例子: Medium
Medium在离线状态下展现缓存的数据,一些离线展现在这个分类里面的 app(例如,Instagram)还会提示用户离线了,因此,就不要对这个分类里面的 app 指望再搞了。
个人想法是,若是 PWA(或者 service workers)技术成熟而且被大规模应用的话,为何不节省掉:
呢?
当咱们接下来谈到 Web Manifest 时,你就意识到只要给你的 web 应用新增一个桌面 icon,web 应用就能够经过点击这个 icon 实现启动了。
一些公司已经在 PWA 方面作的比较好了,你能够在这个网址上面找到这些公司:pwa.rocks
咱们已经介绍了足够多的理论知识了。这是一个手把手的教程,来吧,让咱们动起手来。首先,按照下面的结构来建立一个新的项目:
|--pwa-demo |----css |----fonts |----images |----js |----index.html |----service-worker.js
下载 Materialize 这个 UI 库,用里面 CSS
、Fonts
、js 文件
分别替换项目里面的文件夹。
打开 index.html
文件,引入一些资源:
<!-- ./index.html --> <!DOCTYPE html> <html> <head> <!--Import Google Icon Font--> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <!--Import materialize.css--> <link type="text/css" rel="stylesheet" href="css/materialize.min.css" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="css/app.css"> <!--Let browser know website is optimized for mobile--> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> </head> <body> Body coming soon <!-- Scripts --> <script type="text/javascript" src="js/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="js/materialize.min.js"></script> <script type="text/javascript" src="js/app.js"></script> </body> </html>
咱们已经引入了下载好的文件,还须要本身在相应的目录建立一下 app.css
以及 app.js
这两个文件。
越早在浏览器注册,Service Worker 就能越早的开始工做。最佳的作法是在应用的入口。在这个项目中,咱们能够在 app.js
注册一个新的 worker:
(function(){ if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js') .then(function() { console.log('Service Worker Registered'); }); } })()
在作其余操做以前,咱们首先须要检测一下浏览器对于 Service Worker 的兼容性。若是支持,那咱们就能够利用 register
这个方法来注册这个 worker,这个方法告知了 service worker 文件的路径。注册函数返回一个 promise ,你能够在这个 promise 里面判断注册是否成功。
在开始构建 PWA 以前,你须要理解 Service Worker 的生命周期:
这一阶段主要是让 worker 在浏览器给定的做用域挂载。因为这是生命周期的第一步,最好在这一步缓存各类资源:
// ./service-worker.js var cacheName = 'PWADemo-v1'; var filesToCache = [ '/index.html', '/css/app.css', '/js/app.js', /* ...and other assets (jQuery, Materialize, fonts, etc) */ ]; 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); }) ); });
caches.open
和 cache.addAll
都是异步操做.service worker 在这些操做完成以前可能会中断,e.waitUntil
用来等待 promise 的状态变成 resolved 或者 rejected。addAll
来新增缓存。当 worker 挂载完成,其效果并不会当即展现出来,除非前一个 service worker 销毁而且该 web 应用被从新访问。假设咱们挂载了另外一个不一样 cacheName 的 service worker:
// ./service-worker.js var cacheName = 'PWADemo-v2'; var filesToCache = [ //... ]; self.addEventListener('install', function(e) { console.log('[ServiceWorker] Install'); //... });
当这个新的 service worker 建立以后,新的缓存 PWADemo-v2
也被建立,这时候 PWADemo-v1
仍然存在。当触发 Activate 时,咱们能够删除 PWADemo-v1
,使其“让位”于 PWADemo-v2
:
// ./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) { if (key !== cacheName) { console.log('[ServiceWorker] Removing old cache', key); return caches.delete(key); } })); }) ); });
咱们检查全部的 cache 名称,若是发现不是正在使用的 cache,那么将其直接删除。
Fetch 不是一个必需的生命周期,但它提供了拦截请求资源的方法。当发送请求时,首先会触发这样的事件:
// ./service-worker.js self.addEventListener('fetch', function(e) { console.log('[ServiceWorker] Fetch', e.request.url); e.respondWith( caches.match(e.request).then(function(response) { return response || fetch(e.request); }) ); });
若是资源已经被缓存了,咱们返回浏览器缓存的版本。若是没有,那么咱们调用 fetch api 去发送 HTTP 请求该资源。
因为 service workers 的工做方式,特别是进行缓存时,不是很容易进行 debugger 调试。幸运的是,chrome 的 dev tools 提供了助力。跟着下面的步骤,调试咱们刚注册的 service worker:
你已经了解了必备的知识点,PWA 的概念对你来讲已经不陌生了。接下来,咱们将要讨论 PWA 的缓存策略。咱们将了解如何使用 IndexDB 来保存数据而不是 localStorage。