https://codelabs.developers.google.com/codelabs/your-first-pwapp/#0
1.介绍
这里将使用PWA技术来构建一个天气web应用,这个app将会:
- 使用以及验证PWA的特性
- 使用API获取最新的天气数据
- 添加城市时,可以提供类似原生应用的交互
我们将会学到
- 怎么使用app shell来设计一个应用
- 怎么使app离线工作
- 怎么储存数据用于离线工作
我们需要什么
- 最新版本的chrome。其实用其他浏览器也可以,只不过我们想用chrome的devTools来体验一些新版本浏览器的特性
- 自己做一个web服务器,或者用web server for chrome(ps:这是一个方便快捷的静态文件服务器,访问chrome://apps/或者书签最左边进入应用,进入web server,选择一个目录,启动服务器即可)
- 下载示例代码
- 一个文本编辑器
- 基础的web知识
2.开始
下载解压以上的实例代码,然后打开静态文件服务器,以实例代码中的work为根目录,然后通过服务器访问里面的index.html(把chrome改为手机模式)。
访问可以看到有个圆形进度条在转。work目录中仅仅是一个骨架,后续会添加剩余的功能
3.app shell
app shell是html、css和js的最小集合,用于为用户提供WAP接口以及保证了良好的性能。它的第一次加载是非常快的而且马上进行缓存。任意时刻用户打开app,app都会从本地缓存中加载shell,这使app打开的速度非常快。
shell的结构将数据从核心的公共结构和UI中分离开来,所有的公共结构和UI都使用service worker进行本地缓存,PWA仅仅请求必须的数据。可以理解为shell就是app的架子(包括UI以及公共的结构),而数据则显示在这个架子上,数据经常会发生变化,所以必要的数据需要都次都去请求获取。用另一种说法解释就是shell就是应用商店中的原生应用,运行的时候再请求数据来显示。
service worker是一个浏览器运行在后台的脚本,用于提供各种特性
为什么要使用app shell结构
这可以使我们专注于速度,提供原生应用的用户体验:瞬间完成加载、实时更新,而且不需要应用商店
设计app shell
首先是把核心组件从设计中拆分出来,需要明白:
- 界面上什么需要马上显示?
- 其他关键的UI组件是什么?
- 什么资源是app shell所需的?如图片、脚本和样式等。
在这个天气app中,关键的组件如下:
- 头部组件:标题、添加和刷新按钮
- 天气预报版块的容器组件
- 天气预报版块的模板
- 一个用于添加城市的对话框
- 用于显示loading的指示器
4.实现app shell
有很多种方式可以初始化一个项目,我们推荐使用web starker kit,因为在这个例子我们希望尽可能的简单,所以已经提供好了必备的资源。
为shell创建html
index.html已经在work目录中了,而且样式也已经写好了
检查关键的JS代码
以上界面已经准备好了。在scripts/app.js中可以发现:
- app对象包含了一些应用关键的信息
- 四个监听器:头部组件的添加和刷新、添加城市的对话框的添加和取消
- app.updateForcecastCard用于添加或更新天气预报
- app.getForecast用于获取最新的天气预报信息
- app.updateForecasts用于更新所有的天气预报信息
- initialWeatherForecast用于mock数据,能快速测试界面
测试
以上JS和界面都准备好了,解除以下两端代码的注释(分别在html和js文件底部位置):
<!--<script src="scripts/app.js" async></script>--> // app.updateForecastCard(initialWeatherForecast);
重新运行,即可看到天气预报效果
5.快速初始化
PWA应该是快速启动而且马上可以使用,以上app可以快速打开,但是还不可用,因为没有数据,需要通过ajax来获取数据,但这额外的请求会导致初始的加载变慢,所以应该在app第一次加载的时候,服务端进行一次数据直出,来提高速度。
注入数据(数据直出)
服务端直接把天气数据注入到JS中,但是在生产环境,注入的天气数据要基于用于的IP地址。这里假设initialWeatherForecast就是服务器已经注入的数据,我们直接拿来用
区分是否是第一次运行
什么时候才需要展示缓存中可能已经过时的天气数据呢?
对于用户所添加的城市,应该本地保存到一个存储系统中,为了尽可能简单,这里使用localStorage,这对于生产环境不是非常好,因为它是阻塞的,对于某些设备可能非常慢。
首先,需要保存用户的选项,添加代码如下:
// TODO add saveSelectedCities function here // Save list of cities to localStorage. app.saveSelectedCities = function() { var selectedCities = JSON.stringify(app.selectedCities); localStorage.selectedCities = selectedCities; };
接着,添加初始化的代码,用来检查用户是否本地保存了一些城市(如果是则渲染出来),否则使用注入的数据:
// TODO add startup code here app.selectedCities = localStorage.selectedCities; if (app.selectedCities) { app.selectedCities = JSON.parse(app.selectedCities); app.selectedCities.forEach(function(city) { app.getForecast(city.key, city.label); }); } else { /* The user is using the app for the first time, or the user has not * saved any cities, so show the user some fake data. A real app in this * scenario could guess the user's location via IP lookup and then inject * that data into the page. */ app.updateForecastCard(initialWeatherForecast); app.selectedCities = [ {key: initialWeatherForecast.key, label: initialWeatherForecast.label} ]; app.saveSelectedCities(); }
保存城市信息
最后,需要修改添加城市butAddCity的监听器,来保存被选择的城市到localStorage中:
document.getElementById('butAddCity').addEventListener('click', function() { // Add the newly selected city var select = document.getElementById('selectCityToAdd'); var selected = select.options[select.selectedIndex]; var key = selected.value; var label = selected.textContent; if (!app.selectedCities) { app.selectedCities = []; } app.getForecast(key, label); app.selectedCities.push({key: key, label: label}); app.saveSelectedCities(); app.toggleAddDialog(false); });
6.使用service worker来预缓存app shell
PWA应该支持离线工作,而且对于断续的,缓慢的网络环境,也可以正常工作。实现这一点需要通过service worker来缓存app shell和data
注册sw
先进行判断,支持的话再进行sw的注册
if ('serviceWorker' in navigator) { navigator.serviceWorker .register('./service-worker.js') .then(function() { console.log('Service Worker Registered'); }); }
缓存站点的资源
当sw注册完成后的第一次访问页面,install事件就会被触发,在这个事件中对资源进行缓存,在sw.js内部执行如下代码:
var cacheName = 'weatherPWA-step-6-1'; var filesToCache = []; 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); }) ); });
以上根据一个名字,打开一个cache,每个cache相当于一个缓存的集合,两两之间不会互相影响。addAll将一系列资源添加到cache中,这是一个原子操作。
添加完以上代码后,刷新页面,在调试工具中可以看到当前域中有一个sw处于running状态(页面刷新前,这里是一片空白的):
接着添加一个activate事件监听:
self.addEventListener('activate', function(e) { console.log('[ServiceWorker] Activate'); });
再次刷新页面,以上的sw状态,变成
这是因为旧的sw仍然控制着当前页面,新的sw无法生效,就处于wating状态了(添加的activate回调也没执行)。这里旧的sw是指最开始页面刷新后,处于running状态的sw,里面只监听了一个install事件。后来我们修改了sw的代码,添加了一个activate监听,这就属于一个新的sw了。
为了使新的sw能够生效,即能够更新的sw。需要手动关闭页面,然后重新打开页面,或者点击上面的skipWaiting。但是对于调试环境下,为了更加方便,可以启用 update on reload 选项,这样每次刷新页面,sw都会被强制更新生效。启用这个选项后强制更新,控制台会报一个错误(这是可以忽略的):
更新完成的第一件事情就是。将旧的sw的缓存,或者更新后用不到的缓存移除掉,需要被移除的cache的名字保存在cacheName中
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); } })); }) ); return self.clients.claim(); });
claim函数处理一个边缘情况:(未完...)
其他
只要sw缓存了数据,下次离线访问的时候,请求会被sw拦截,sw可以返回对应的数据,包括当前的页面html等。