PWA-让你的web应用变得高大上

前言

对于PWA,在通过屡次被面试官进行灵魂拷问后😭,我对他产生了浓厚的兴趣,苦于前段时间笔者忙着面试没得时间,因而就耽搁到了如今😂。不过对于学习新技术而言,咱们老是须要去怀着一颗敬畏之心去研究的,一项技术的兴起老是有着它的意义所在,也必然表明了某种趋势。说了这么多,那PWA究竟是什么呢?javascript

什么是PWA

PWA(Progressive web apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式加强策略来建立跨平台 Web 应用程序。这些应用无处不在、功能丰富,使其具备与原生应用相同的用户体验优点css

MDN上的解释老是很官方的,从字面上来讲,咱们能够知道他是一种渐进式的Web应用,那么何谓渐进式呢?其实就是表明着若是浏览器不支持,那么对原有应用不会产生影响,对于支持该项技术的浏览器,他会在原有基础上新增它的特性,让用户获得更好的体验。目前在VueReact脚手架中已经集成了该项技术,一旦你拥有一个web app项目,那么你的PWA之旅就已经开始了。html

为何它会这么火?这就不得不提到它的三大特性了:java

  • 可添加到桌面
  • 离线访问
  • 后台通知

对于一个网站来讲,怎么留住用户就成了咱们必须考虑的一个问题,而对于Web应用而言,被用户记住的一个比较粗糙的方式莫过于书签了,可就用户体验层次来讲,这就没法与原生应用进行媲美了。对于一个比较大型的项目来讲,开发一个原生应用的成本无疑是巨大的。web

因而咱们怎么让一个Web应用具有像原生App同样的桌面添加直接可访问并具备打开网站的过分效果就成了一种迫切的开发须要,PWA应势而生。面试

三大特性实现详解

PWA中有一个必须注意的点,它只支持在https协议和localhost即本地环境下进行使用,也就是你的应用须要被访问必须具有这个条件。数据库

桌面添加

其实对于这个功能而言,它的核心在于一个名叫manifest.json的文件,一旦咱们的应用引入了该项配置,它就能被安装到桌面进行使用。编程

manifest配置

{
    "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进行存取操做,若是用户断网,咱们就能够选择从缓存中读取须要的数据,这样咱们就能实现离线缓存功能了🤒。

ServiceWorker详解

  • service worker容许web应用在网络环境比较差或者是离线的环境下依旧可使用
  • service worker能够极大的提高web app的用户体验
  • service worker是一个独立的 worker 线程,独立于当前网页进程,是一种特殊的web worker
  • Web Worker 是临时的,每次作的事情的结果还不能被持久存下来,若是下次有一样的复杂操做,还得费时间的从新来一遍
  • 一旦被 install,就永远存在,除非被手动 unregister
  • 用到的时候能够直接唤醒,不用的时候自动睡眠
  • 可编程拦截代理请求和返回,缓存文件,缓存的文件能够被网页进程取到(包括网络离线状态)
  • 离线内容开发者可控
  • 必须在 HTTPS 环境下才能工做
  • 异步实现,内部大都是经过 Promise 实现

具体什么是webWoker,本文就再也不赘述了,详细概念能够参见阮一峰老师这篇博客,Web Worker 使用教程

注册ServiceWorker

想要使用它,咱们通常会在用户首次访问网站的时候进行注册。为了避免影响页面正常的解析和页面资源的下载,咱们会选择在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方法中进行捕获。

serviceWorker的生命周期

注册完咱们的sw.js(文件名自定义)后,咱们就能够在sw.js文件中来研究它的三个核心生命周期函数了。

  • install - 会在service worker注册成功的时候触发,主要用于缓存资源
  • activate - 会在service worker激活的时候触发,主要用于删除旧的资源
  • fetch - 拦截页面全部请求,当有拦截到请求就会触发(核心),主要用于操做缓存或者读取网络资源

install阶段

通常在这个阶段咱们主要会将须要离线缓存的一些页面、资源等存入缓存中,以便在无网络的状况下能够继续访问网站。

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中存入数据了。

cache storage

  • caches api 相似于数据库的操做:
    • caches.open(cacheName).then(function(cache) {}): 用于打开缓存,返回一个匹配cacheName的cache对象的promise,相似于链接数据库
    • caches.keys() 返回一个promise对象,包括全部的缓存的key(数据库名)
    • caches.delete(key) 根据key删除对应的缓存(数据库)
  • cache对象经常使用方法(单条数据的操做)
    • cache.put(req, res) 把请求当成key,而且把对应的响应存储起来
    • cache.add(url) 根据url发起请求,而且把响应结果存储起来
    • cache.addAll(urls) 抓取一个url数组,而且把结果都存储起来
    • cache.match(req) : 获取req对应的response

咱们须要先列出咱们须要进行缓存的清单,也就是代码中的cacheList,调用cache storage中的addAll方法就能将须要缓存的资源存入cache storage中了😀。

activate阶段

在这个阶段中,咱们通常会作的事情无非就一件事,把旧的资源或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阶段

这个阶段能够说就是比较核心的生命周期函数了,由于前面两个主要用于一些初始化的操做,而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方法就能把返回值响应给浏览器进行渲染了,至此咱们已经完成了一大步,最后就是怎么进行系统通知了。

Notification

这个处理部分就不能放在sw.js文件中了,由于咱们须要用到window中的Notification函数。

//先获取通知权限
if (Notification.permission == "default") {
  Notification.requestPermission();
}
if (!navigator.onLine) {
  new Notification("提示", { body: "您已断线,如今访问的是缓存内容" });
}
复制代码

对于这种系统级别的api,第一步天然就是获取用户权限,而后才能进行下一步操做。在这里笔者就只写了一个通知用户已经离线的功能。

最后

笔者的文件目录:

  • images
    • logo.png
  • index.css
  • index.html
  • manifest.json
  • sw.js
  • setting.js
  • server.js

index.html文件中只须要用link标签引入manifest.jsonsetting.jssetting.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多字,但愿可以对你们有所帮助,同时也欢迎你们对表述不正确的地方加以指正🧐。

相关文章
相关标签/搜索