什么是PWA
Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。
上面是Lavas给出的简单描述,PWA给我的个人感觉来说,就是将原生APP的体验搬到浏览器上,包括例如在桌面上生成icon,快速启动,可以离线使用,可以推送消息,总而言之,它需要具备原生APP的所有特点,并在此基础上更进一步。
PWA应用的技术
- Service Worker
- cacheStorage
- Push Notification(本应用并未涉及)
应用演示
这个应用的想法源自于Your First Progressive Web App,在学习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 worker
window.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 += '<p class="item"></p>'
}
return temp;
})();
let model = (function() {
let temp = '';
switch (type) {
case 'normal':
temp = `
<div class="card preload mg" id="${key}">
${ rows }
<p class="item" style="width: 4rem"></p>
</div>
`
break;
case 'title':
temp = `
<div class="card preload mg" id="${key}">
<p class="head"></p>
${ rows }
<p class="item" style="width: 4rem"></p>
</div>
`
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哦