对于PWA
,在通过屡次被面试官进行灵魂拷问后😭,我对他产生了浓厚的兴趣,苦于前段时间笔者忙着面试没得时间,因而就耽搁到了如今😂。不过对于学习新技术而言,咱们老是须要去怀着一颗敬畏之心去研究的,一项技术的兴起老是有着它的意义所在,也必然表明了某种趋势。说了这么多,那PWA
究竟是什么呢?javascript
PWA(Progressive web apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式加强策略来建立跨平台 Web 应用程序。这些应用无处不在、功能丰富,使其具备与原生应用相同的用户体验优点css
MDN上的解释老是很官方的,从字面上来讲,咱们能够知道他是一种渐进式的Web
应用,那么何谓渐进式呢?其实就是表明着若是浏览器不支持,那么对原有应用不会产生影响,对于支持该项技术的浏览器,他会在原有基础上新增它的特性,让用户获得更好的体验。目前在Vue
、React
脚手架中已经集成了该项技术,一旦你拥有一个web app
项目,那么你的PWA
之旅就已经开始了。html
为何它会这么火?这就不得不提到它的三大特性了:java
对于一个网站来讲,怎么留住用户就成了咱们必须考虑的一个问题,而对于Web
应用而言,被用户记住的一个比较粗糙的方式莫过于书签了,可就用户体验层次来讲,这就没法与原生应用进行媲美了。对于一个比较大型的项目来讲,开发一个原生应用的成本无疑是巨大的。web
因而咱们怎么让一个Web
应用具有像原生App
同样的桌面添加直接可访问并具备打开网站的过分效果就成了一种迫切的开发须要,PWA
应势而生。面试
在
PWA
中有一个必须注意的点,它只支持在https
协议和localhost
即本地环境下进行使用,也就是你的应用须要被访问必须具有这个条件。数据库
其实对于这个功能而言,它的核心在于一个名叫manifest.json
的文件,一旦咱们的应用引入了该项配置,它就能被安装到桌面进行使用。编程
{
"name": "HackerWeb",//应用名称
"short_name": "HackerWeb",//短名称,用于在桌面显示
"start_url": ".",//入口url
"display": "standalone",//应用的展示模式,通常来讲这个模式体验最优
"background_color": "#fff",//应用的主题颜色,通常会改变你的上方菜单栏背景颜色
"description": "A simply readable Hacker News app.",//应用描述
"icons": [{//在不一样环境下展示的应用图标
"src": "images/touch/homescreen48.png",
"sizes": "144x144",
"type": "image/png"
}]
}
复制代码
具体配置的详情描述能够参照:Web App Manifestjson
配置好以后咱们只需使用link
标签进行引入就足够了api
<link rel="manifest" href="manifest.json">
复制代码
这样你的应用就已经具有了被安装到桌面的能力,是否是很简单😏。
这个描述功能的实现,笔者就开始要准备放大招了🐤,它的一个核心概念能够用一张图来描述:
其实这项技术的实现就须要借助咱们的ServiceWorker
以及这一个Cache Storage
来进行配合实现了。
功能的实现思路就在于ServiceWorker
能够拦截全部请求,并能够操做Cache Storage
进行存取操做,若是用户断网,咱们就能够选择从缓存中读取须要的数据,这样咱们就能实现离线缓存功能了🤒。
具体什么是webWoker
,本文就再也不赘述了,详细概念能够参见阮一峰老师这篇博客,Web Worker 使用教程
想要使用它,咱们通常会在用户首次访问网站的时候进行注册。为了避免影响页面正常的解析和页面资源的下载,咱们会选择在onload
事件触发时进行ServiceWorker
的注册,它的注册很简单,只须要调用一个Api
便可:
window.onload = function() {
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("./sw.js")
.then(registration => {
console.log(registration);
})
.catch(err => {
console.log(err);
});
}
};
复制代码
咱们首先会判断该浏览器是否支持ServiceWorker
,若是支持就进行注册,不支持就直接跳过,不会影响页面。这个注册方法返回的是一个Promise
对象,咱们能够在then
方法中获取到registration
,这个对象包含了一些注册成功后的信息,若是失败,咱们能够在catch
方法中进行捕获。
注册完咱们的sw.js
(文件名自定义)后,咱们就能够在sw.js
文件中来研究它的三个核心生命周期函数了。
service worker
注册成功的时候触发,主要用于缓存资源service worker
激活的时候触发,主要用于删除旧的资源通常在这个阶段咱们主要会将须要离线缓存的一些页面、资源等存入缓存中,以便在无网络的状况下能够继续访问网站。
self.addEventListener("install", async e => {
cacheData(); //调用缓存方法
await self.skipWaiting(); //跳过等待
// e.waitUtil(self.skipWaiting()); //另外一种跳过等待方式
});
复制代码
首先我会调用相应的缓存资源方法,而后后面的self.skipWating
方法主要就是用于若是你的sw.js
也就是被注册的文件发生改变就会从新触发install
生命周期函数,可是却不会当即触发activite
周期,它会等待上一个sw.js
销毁后才会激活下一个,这个时候咱们新注册的sw.js
并无被激活,因此为了可以让新注册的sw.js
能马上生效,咱们能够加上这么一句进行跳过等待。
这个地方为何会有这么两种写法呢?实际上是由于
self.skipWating
返回的是一个Promise
,是异步的,为了保证当前周期函数执行完再进入下一个因此咱们须要等待它执行完成,这里可使用async await
来实现,也可使用内置的一个工具方法waitUtil
来实现相应功能。
下面咱们来解析一下代码中cacheData
方法:
//缓存方法
const CHACH_NAME = "cache_v2";
async function cacheData() {
const cache = await caches.open(CHACH_NAME); //打开一个数据库
const cacheList = [
"/",
"/index.html",
"/images/logo.png",
"/manifest.json",
"/index.css",
"/setting.js"
]; //须要缓存的清单
await cache.addAll(cacheList); //缓存起来
}
复制代码
其实在这里就用上了咱们另外一个须要研究的知识点cache storage
了。他其实有点相似于一个数据库,通常想要使用数据库,咱们就须要先打开一个数据库,每一个数据库都有一个本身的名字,知足了这些条件,咱们就能往cache storage
中存入数据了。
caches.open(cacheName).then(function(cache) {})
: 用于打开缓存,返回一个匹配cacheName的cache对象的promise,相似于链接数据库caches.keys()
返回一个promise对象,包括全部的缓存的key(数据库名)caches.delete(key)
根据key删除对应的缓存(数据库)cache.put(req, res)
把请求当成key,而且把对应的响应存储起来cache.add(url)
根据url发起请求,而且把响应结果存储起来cache.addAll(urls)
抓取一个url数组,而且把结果都存储起来cache.match(req)
: 获取req对应的response咱们须要先列出咱们须要进行缓存的清单,也就是代码中的cacheList
,调用cache storage
中的addAll
方法就能将须要缓存的资源存入cache storage
中了😀。
在这个阶段中,咱们通常会作的事情无非就一件事,把旧的资源或cache storage
删除掉。
但因为serviceWoker
在用户浏览器中安装激活后咱们并不能立马就生效,通常会须要用户在刷新页面后的第二次访问才能生效,因此咱们会在activate
阶段中调用一个API,让咱们可以在第一访问就能生效,具体代码以下:
const CHACH_NAME = "cache_v2";//在全局定义了当前数据库名
self.addEventListener("activate", async e => {
/**查出数据库全部库名,清除旧版本库 */
const keys = await caches.keys();
keys.forEach(key => {
//若是该数据库名不是当前定义的名字就进行删除
if (key !== CHACH_NAME) {
caches.delete(key);
}
});
/**用于马上获取页面控制权,确保用户第一次打开浏览器就是立马生效*/
await self.clients.claim();
});
复制代码
由于self.clients.claim()
返回的也是一个Promise
对象,因此咱们也须要等待其执行完成。
这个阶段能够说就是比较核心的生命周期函数了,由于前面两个主要用于一些初始化的操做,而fetch
阶段就真正实现离线缓存的中心枢纽,它会拦截全部页面请求,由于这一特性,咱们就能在无网络的状况下将用户须要请求的资源从缓存中读取出来返回给用户。
通常对于处理用户请求,咱们会有多种策略,下面笔者就讲两种经常使用的:
顾名思义,就是先去网络上请求,若是请求不到,再去缓存中读取,具体代码以下:
self.addEventListener("fetch", async e => {
const req = e.request;//拿到请求头
await e.respondWith(networkFirst(req));//将用户请求的资源响应给浏览器
});
//网络优先
async function networkFirst(req) {
/**使用try.catch进行异常捕获*/
try {
const res = await fetch(req);
return res;
} catch (error) {
const cache = await caches.open(CHACH_NAME); //打开一个数据库
return await cache.match(req);//读取缓存
}
}
复制代码
首先会使用Fetch向对应网络地址发起请求,若是请求不到资源就会抛出异常,就能被try catch
捕获,而后进入catch
中进行缓存读取。
先读取缓存中数据,若是没有再发起网络请求。
//缓存优先
async function cachekFirst(req) {
const cache = await caches.open(CHACH_NAME); //打开一个数据库
let res = await cache.match(req);//读取缓存
if (res) {
return res;
} else {
res = await fetch(req);
return res;
}
}
复制代码
具体代码含义就很少加赘述了,能看到这一步应该对你没什么问题了吧😜。
拿到对应资源以后,咱们就只须要调用e.respondWith
方法就能把返回值响应给浏览器进行渲染了,至此咱们已经完成了一大步,最后就是怎么进行系统通知了。
这个处理部分就不能放在sw.js
文件中了,由于咱们须要用到window
中的Notification
函数。
//先获取通知权限
if (Notification.permission == "default") {
Notification.requestPermission();
}
if (!navigator.onLine) {
new Notification("提示", { body: "您已断线,如今访问的是缓存内容" });
}
复制代码
对于这种系统级别的api
,第一步天然就是获取用户权限,而后才能进行下一步操做。在这里笔者就只写了一个通知用户已经离线的功能。
笔者的文件目录:
在index.html
文件中只须要用link
标签引入manifest.json
和setting.js
,setting.js
中内容以下:
window.onload = function() {
if (this.navigator.serviceWorker) {
this.navigator.serviceWorker
.register("./sw.js")
.then(registration => {
console.log(registration);
})
.catch(err => {
console.log(err);
});
}
};
/** * 判断用户是否联网,并给与通知提示 */
//先获取通知权限
if (Notification.permission == "default") {
Notification.requestPermission();
}
if (!navigator.onLine) {
new Notification("提示", { body: "您已断线,如今访问的是缓存内容" });
}
复制代码
洋洋洒洒也写了3k多字,但愿可以对你们有所帮助,同时也欢迎你们对表述不正确的地方加以指正🧐。