什么是PWA
Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。
上面是Lavas给出的简单描述,PWA给我的个人感觉来说,就是将原生APP的体验搬到浏览器上,包括例如在桌面上生成icon,快速启动,可以离线使用,可以推送消息,总而言之,它需要具备原生APP的所有特点,并在此基础上更进一步。
PWA应用的技术
- Service Worker
- cacheStorage
- Push Notification(本应用并未涉及)
应用演示
这个应用的想法源自于,在学习PWA的时候看到这个demo,不过里面的代码基本看不懂。。。所以就用了它的UI设计和ICON,自己开始慢慢摸索。
(因为没有对PC样式进行适配,所以请在手机端或chrome手机调试模式打开,chrome点击"添加到主屏幕"即可添加桌面ICON)
项目结构
- images(存放图片)
- fontSet.js(根据不同手机设置全局字体)
- index.html(主页面)
- main.js(主程序)
- manifest.json(控制桌面启动程序(icon)的添加)
- reset.css(清空默认样式)
- skeleton(骨架屏,用于加载时过渡)
- style.css(主样式)
- sw.js(service worker进程)
缓存App Shell
首先,我们需要在主进程注册一个service worker
// 注册service workerwindow.addEventListener('DOMContentLoaded', function() { SW.register();})const SW = { // 注册 register() { // 检测serviceWorker是否可用 if('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js') .then(function() { console.log('Service Worker Registered'); }) .catch(function() { console.log('Service Worker failed'); }) } }}复制代码
如果注册成功了,service worker就正式开始工作了,service worker的生命周期可以简单描述为
serviceWorker(第一次安装或者发生变化) -> install -> activite
所以此时就进入了第一步“install“,在install过程中缓存我们离线时需要的文件,这里包括像页面本身,页面的样式,还有主程序等,但是需要注意的是,千万不能把sw.js也缓存进去,不然你的应用就永远更新不了了
const CACHENAME = 'weather-' + 'v4';const PATH = '/pwaTest';const fileToCache = [ PATH + '/', PATH + '/index.html', PATH + '/main.js', PATH + '/fontSet.js', PATH + '/skeleton.js', PATH + '/reset.css', PATH + '/style.css', PATH + '/images/icons/delete.svg', PATH + '/images/icons/plus.svg', PATH + '/images/partly-cloudy.png', PATH + '/images/wind.png', PATH + '/images/cloudy_s_sunny.png', PATH + '/images/cloudy.png', PATH + '/images/clear.png', PATH + '/images/rain.png', PATH + '/images/fog.png', PATH + '/images/icons/icon-32x32.png', PATH + '/images/icons/icon-128x128.png', PATH + '/images/icons/icon-144x144.png', PATH + '/images/icons/icon-152x152.png', PATH + '/images/icons/icon-192x192.png', PATH + '/images/icons/icon-256x256.png'];self.addEventListener('install', e => { console.log('Service Worker Install'); e.waitUntil( caches.open(CACHENAME).then(function (cache) { self.skipWaiting(); console.log('Service Worker Caching'); return cache.addAll(fileToCache); }) )})复制代码
注:e.waitUntil()是等待一个Promise对象执行完毕后。
当install完毕后,进入activate进程,我们需要清理掉旧的缓存,不然浏览器还会使用旧缓存,并且旧缓存也占用着空间。
self.addEventListener('activate', function (event) { event.waitUntil( // 遍历 caches 里所有缓存的 keys 值 caches.keys().then(function (cacheNames) { return Promise.all( cacheNames.map(function (NAME) { if (NAME != CACHENAME) { // 删除掉除了当前版本之外的缓存文件 return caches.delete(NAME); } }) ); }) );});复制代码
fetch
最开始的时候我并不知道fetch的作用是什么,只是跟着示例代码敲,程序就能正常运行,我原以为service worker是类似与vue组件间的emit和on一样,通过message来传递数据。
但service worker并不是,简而言之,service worker是通过监听fetch事件来拦截所有的请求,并对其进行处理,这些请求包括服务器对服务器本地文件的请求(index.html,style.css,main.js),也包括了对外部接口的调用(GET,POST请求)
self.addEventListener("fetch", function(e) { // e是所有的请求,没调用一次请求,都会被fetch监听到 e.respondWith(caches.match(e.request).then(function(response) { // 在caches中寻找response,如果有就返回response,如果没有,就继续fetch(即不在本地查找,调用接口去查找) return response || fetch(e.request); }));});复制代码
至此,这个应用的初步框架已经搭建起来了。
离线功能
我们知道,PWA的一大特点就是可以离线使用,所以我们需要对我们的代码进行一些处理。
self.addEventListener('fetch', e => { e.respondWith( caches.match(e.request).then(function (res) { if (res) { if (e.request.url.indexOf(self.location.host) !== -1) { // 同源 return res; } else { // 离线状态 if (!navigator.onLine) { return res; } else { return fetch(e.request).then((response) => { let responeClone = response.clone(); let responeClone_2 = response.clone(); responeClone_2.json().then(data => { caches.open(CACHENAME).then(cache => { cache.put(e.request, responeClone); }) }).catch(e => { console.log(e); }) return response; }) } } } // 远程js文件 if (e.request.url.indexOf('https://pv.sohu.com/cityjson?ie=utf-8') !== -1) { return fetch(e.request); } return fetch(e.request).then((response) => { let responeClone = response.clone(); let responeClone_2 = response.clone(); responeClone_2.json().then(data => { caches.open(CACHENAME).then(cache => { cache.put(e.request, responeClone); }) }).catch(e => { }) return response; }).catch(e => { }) }) )})复制代码
大体思路如下:
- 无论在线/离线,App shell的部分(即同源)总是可以离线获取的,所以直接return res即可
- 在线时,对于天气的情况,直接调用远程接口(不使用本地缓存),这样做的原因,是因为天气需要实时更新,每次访问时都应该是最新的天气情况,如果调用一次以后就直接去调用缓存的数据,那天气的情况就永远停留在第一次了
- 离线时,直接或者已经缓存好的天气情况即可
骨架屏
当用户网络情况不佳时,页面信息的加载需要一些时间,但是如果直接留给用户一个大白屏,用户不知道应用是否还是正常工作,所以需要一个过渡,来缓解用户的焦躁,那我们就需要用到骨架屏了
skeleton.js
const Skeleton = { Render(key, type, row) { let rows = (function() { let temp = ''; for (let i = 0; i < row; i ++) { temp += '' } return temp; })(); let model = (function() { let temp = ''; switch (type) { case 'normal': temp = `${ rows }` break; case 'title': temp = `${ rows }` break; default: break; } return document.createRange().createContextualFragment(temp); })(); return model; }}export default Skeleton复制代码
main.js
import skeleton from './skeleton.js' // 新建一个新的城市天气实例 buildNewCity(city) { if (navigator.onLine) { // 骨架屏先行渲染 let preModel = (function() { return skeleton.Render(city, 'title', 3); })(); let container = document.getElementById('container'); container.appendChild(preModel); } this.getInfoNow(city); } // 在线时才需要骨架屏 if (navigator.onLine) { let container = document.getElementById(this.name); setTimeout(() => { container.classList.remove('preload'); container.innerHTML = ""; container.appendChild(model); // 为删除键绑定事件 document.getElementById('delete_' + this.name).addEventListener('click', function() { WEATHERINFO.deleteCity(_this.name); }) }, 200); } else { let card = document.createElement('div'); card.classList = ['card ' + 'mg']; card.id = _this.name; card.appendChild(model); let container = document.getElementById('container'); container.appendChild(card); // 为删除键绑定事件 document.getElementById('delete_' + this.name).addEventListener('click', function() { WEATHERINFO.deleteCity(_this.name); }) }复制代码
当用户添加城市时,先将骨架屏放到页面上,再进行fetch操作,当fetch完成后,再将fetch到的数据给对应的div中。
chrome slow 3G下的测试
这就是这个小应用的几个技术要点,可以观看演示,
如果文章对你有用的话,可以点个star哦