service worker项目实战一:离线缓存实现

前言

学习本教程以前,须要对 nodejsnpm 至少有基础的了解,service worker涉及到缓存,因此对http缓存也应该有基础的了解。javascript

关于 service worker 更多的基础知识能够参考这儿,本文着重讲解离线缓存实现方案。css

本文github地址,若是对service worker有必定的了解,能够直接看具体实现。html

知识点归纳

  • service worker简介
  • 基于nodejs实现一个简单的静态资源服务器
  • 基本的离线缓存实现
  • 离线缓存的更新(重点)
  • 离线缓存和在线服务的结合

service worker简介

service worker翻译成中文叫作 服务工做线程,它是一个注册在指定源和路径下的事件驱动worker, service worker控制的页面或者网站可以拦截并修改访问和资源请求,细粒度地缓存资源,因此用service worker也可以在网络不可用的状况下实现离线缓存。java

service worker独立于主线程以外的worker上下文,所以它不能访问DOM,因此不会形成阻塞,它设计为彻底异步,同步API(如XHRlocalStorage)不能在service worker中使用。node

service worker只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会很是危险。在Firefox浏览器的用户隐私模式,service worker不可用,另外本地环境localhost也能够用service workerwebpack

项目目录结构

├── README.md
├── package.json
├── public
│   └── favicon.ico
├── server.js
├── src
│   ├── index.css
│   ├── index.html
│   └── index.js
├── sw
│   └── service-worker.js
└── yarn.lock
复制代码

新建文件夹,命名为service-worker。定位到该目录,执行npm init一路回车便可。 安装 serve-faviconnodemonexpress:nginx

npm install serve-favicon express nodemon --save-dev
或者:
yarn add serve-favicon express nodemon --dev
复制代码

而后在项目根目录下面建立sw src public 目录,并建立对应的文件。git

基于nodejs实现一个简单的静态资源服务器

实际项目中咱们应该使用nginx等来做为静态资源服务器,本示例中简单使用nodejs来实现。github

server.jsweb

const path = require("path");
const express = require("express");
const favicon = require('serve-favicon');
const app = express();
app.get("*", (req, res, next) => {
  console.log(req.url);
  next();
})
app.use(favicon(path.join(__dirname, "public", "favicon.ico")));
const options = {
  setHeaders (res, filePath, stat) {
    res.set({
      "cache-control": "no-cache"
    })
  }
}
app.use(express.static("src", options));
app.use(express.static("sw", {
  maxAge: 0
}))
app.listen("9000", () => {
  console.log("server start at localhost:9000");
})
复制代码

在server.js文件中,把src目录下的静态资源缓存类型设置为协商缓存,客户端每次获取资源都会向服务器验证文件有效性来确认是否使用本地缓存;sw下面的server-worker.js则设置为永久性缓存为0,也就是不缓存,客户端每次都会向服务器获取完整的资源,server-worker.js必定不能缓存

在package.json中添加启动命令

"start": "nodemon server.js"
复制代码

而后启动服务器

基本的离线缓存实现

先随便写一点内容到index.html和index.css中。

src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>service-worker</title>
  <link rel="stylesheet" href="./index.css">
</head>
<body>
  <div class="box">
    <div>hello service worker v1 </div>
  </div>
  <script src="./index.js"></script>
</body>
</html>
复制代码

src/index.css

.box {
  text-align: center;
  color: red;
}
复制代码

而后在index.js中实现service worker的注册逻辑,主流浏览器对service worker的支持度能够在 caniuse 中查看。

src/index.js

function serviceRegister () {
  const { serviceWorker } = navigator;
  if (!serviceWorker) {
    console.warn("your browser not support serviceWorker");
    return;
  }
  window.addEventListener("load", async () => {
    let sw = null;
    const regisration = await serviceWorker.register("./service-worker.js");
    sw = regisration.installing || regisration.waiting || regisration.active;
    sw && sw.addEventListener("statechange", (e) => {
      const { state } = e.target;
      console.log(state);
    });
  });
}
serviceRegister();
复制代码

首先从navigator中获取serviceWorker,而后进行支持度检测。当浏览器资源加载完成以后,调用serviceWorker.register(url, {scope: "xxxx"})注册server worker,url为service workder内容的js文件路径,scope为service worker的控制域,默认值是基于当前的location也就是 /,能够自定义修改。

serviceWorker.register函数返回一个regisration,由于worker是异步注册的,调用register以后不知道处于注册以后的哪一个阶段,因此只能用regisration中的installing、waiting、active去获取注册实例,而后给实例添加statechange事件监听状态改变。

那当我注册成功时,怎样查看我注册的sw呢?直接打开 chrome://inspect/#service-workers 就能够查看,在当前浏览器中,正在注册的 SW。另外,还有一个 chrome://serviceworker-internals,用来查看当前浏览器中,全部注册好的sw。另外,在当前页面下打开chrome控制台,切换到application选项,下面有个Service Workers二级选项,能够查看当前的sw状态,下面的Application Cache能够查看当前缓存的版本号。

浏览器打开控制台,而后打开localhost:9000,发能够看到log输出的sw的周期状态改变

installed
activating
activated
复制代码

可见第一次注册sw时候,sw会走完installed、activating、activated三个周期,在控制台切换到application/ Service Workers能够看到当前的sw状态以下图:

此时service-worker.js中什么内容都没有,咱们往里面添加一部份内容:

service-worker.js

const _this = this;
const version = "v1";
const cacheFile = [
  "/",
  "/index.html",
  "index.css",
  "index.js"
]
this.addEventListener("install", (event) => { 
  event.waitUntil(
    caches
    .open(version)
    .then((cache) => {
      return cache.addAll(cacheFile)
    })
  )
})
this.addEventListener("fetch", async (event) => {
  const { request } = event;
  event.respondWith(
    caches
      .match(request.clone())
      .catch(() => {
        return fetch(request.clone()).catch()
      })
  );
});
this.addEventListener("activate", (event) => {
  const wihleList = [version];
  event.waitUntil(
    caches
      .keys()
      .then((keyList) => {
        return Promise.all(
          keyList.map((key) => {
            if (!wihleList.includes(key)) {
              return caches.delete(key);
            }
          })
        )
      })
  )
});
复制代码

在server-worker线程里面,给当前worker添加install、fetch、activate事件。在install事件回掉函数中添加缓存版本号和具体的缓存文件。fetch事件拦截客户端请求,咱们首先从缓存中读取内容,若是缓存中找不到则继续向服务器请求。activate中对非当前cache version的缓存进行清理。

service-worker修改完成以后,刷新浏览器,切换到控制台能够看到当前sw状态如图:

嗯,大概的意思就是,老的sw还在服役中,页面的控制权还在老的sw上,新的sw已经安装完成,可是处于未激活状态,没有取得页面的控制权利。关于sw的更新后面会继续讨论,咱们手动点击skipWaiting,让新的sw当即激活,而后取得页面控制权。

注意:在开发调试过程当中,咱们常常须要在控制台对sw进行 skipWaiting、unRegister、delete cache等操做,可是你不要指望用户会这么作,因此在进行线上版本发布时候,必定要慎重,缓存搞的错乱了,用户加载的内容出错了,但是严重故障...

手动点击skipWaiting后,看到新的sw已经生效了。刷新浏览器,在network选项中能够看到缓存内容的请求已经被拦截了,从sw缓存中获取了。在chrome控制台中,把network状态改为offline,再次刷新浏览器,嗯,虽然没网了,可是仍是能够从本地缓存读取内容。

至此service-worker离线缓存基本原理算是搞清楚了,基本功能也实现了,可是若是要觉得这就能够用到生产环境中去,那绝对出大事,由于使用了sw应用的更新但是颇有玄妙的。

离线缓存的更新

加入了service-worker的应用中,更新分为sw更新和页面资源更新。

service-worker更新

接着上面的例子,每次service-worker.js更新后,客户端会从新安装新的sw,而后等待老的sw控制的页面都关闭或者整个浏览器都关闭,新的sw才会激活。在sw进程中,提供了skipWaiting 方法,能够跳过新的sw等待状态,直接激活,取得做用域下面的页面控制权。

src/service-worker.js

...
const version = "v2";
...
this.addEventListener("install", (event) => { 
  this.skipWaiting();
  ...
})
复制代码

添加skipWaiting(),同时把缓存 version 改成v2,把浏览器控制台network改成online,再次刷新浏览器。查看控制台的application,sw已经没有了skipWaiting状态,新的sw安装完成以后当即被激活了。使用skipWaiting以后也会产生一系列问题:

  • 浏览器在后台主动更新的sw,同时sw会缓存新的文件,下一次用户访问url时候,直接获取新的缓存。若是更新sw是由用户访问url触发的,因为sw注册和安装更新是独立线程异步的,则此时咱们的url打开时候,仍是由老旧的sw控制,因此获取的也是老旧的资源

修改src/index.html

<div>hello service worker v3 </div>
复制代码

修改sw/service-worker.js

const version = "v3";
复制代码

刷新浏览器,新的sw已经激活了,但是页面上仍是hello service worker v1,再次刷新才能是hello service worker v3。

解决方案:注册service worker时候,添加controllerchange事件,当监听到新的sw取得页面控制权时候,主动刷新页面或者以消息形式通知用户刷新浏览器

很明显这种方法虽然可行,可是直接被否决,主动刷新页面太暴力,通知用户刷新浏览器结果不可控。

  • 老的sw和新的sw可能不太同样,替换以后容易引发不少未知错误,这种错误不可控且很难复现

  • sw没有更新,可是资源更新了,用户访问url仍是走的老的sw缓存

修改src/index.html

<div>hello service worker v4 </div>
复制代码

刷新浏览器,页面内容仍是hello service worker v3。

离线缓存和在线服务的结合

鉴于离线缓存更新带来的各类问题,我思考许久,同时也看了service worker在其余著名企业的产品上的应用,最终思考出了一个方案:在sw控制的页面中,优先使用在线资源,sw充当本地和线上服务器的代理服务器角色;当在线资源获取出错(服务器宕机,网络不可用等状况),则使用sw本地缓存

html文件优先从线上获取,获取以后设置缓存,以便下一次获取html出错时候再从本地缓存获取的是上一次获取的文件。总之,当可以获取到服务器资源时候,咱们要保证html一直是最新的,由于html里面的各类js,css资源等,都会添加版本号,因此这些js、css的资源请求也会是正确的对应的版本号,除了html文件以外的资源,咱们优先从本地获取,本地获取不到再从线上获取而且缓存请求,以便下一次使用(实际项目中应该用请求uri和本身的cachefile也就是本身的缓存文件列表进行比较判断是否该缓存,本地cache storage空间优先,不可能缓存全部的非html请求)。

sw/service-worker.js

function setCache (req, res) {
  caches
    .open(version)
    .then((cache) => {
      cache.put(req, res);
    })
}
this.addEventListener("fetch", async (event) => {
  const { request } = event;
  if (request.headers.get("Accept").indexOf("text/html") !== -1) {
    event.respondWith(
      fetch(request.clone())
      .then((response) => {
        if (response) {
          setCache(request.clone(), response.clone())
          return response.clone();
        }
        return caches.match(request.clone());
      }).catch((e) => {
        return caches.match(request.clone());
      })
    )
    return;
  }
  event.respondWith(
    caches
      .match(request.clone())
      .then((response) => {
        if (response) {
          return response;
        }
        return (
          fetch(request.clone())
          .then((fetchResponse) => {
            // 对于非html资源,实际项目中不该该缓存全部的请求,毕竟本地cache storage有限
            // 应该对请求的资源和cacheFile进行对比,若是匹配则缓存,若是不匹配,
            // 则此处fetch仅仅充当服务请求中转的做用
            setCache(request.clone(), fetchResponse.clone());
            return fetchResponse.clone();
          })
        );
      }).catch((e) => {
        console.log(e);
      })
  );
});
复制代码

修改service-worker.js以后,咱们在控制台把sw给unregister了,而且把cache storage里面的缓存清理掉,而且把html中的内容改成hello service worker v1,再把service-worker.js中的verson改成v1,而后刷新页面,至关于用户第一次访问url进行sw注册。

以后就能够进行各类操做了,好比html内容修改、sw里面的缓存version修改或者sw其余内容修改、或者断网操做,或者本身清除cache storage请求等,每次操做以后刷新浏览器,看看效果而且好好思考结果和为何吧!

注意:

本示例的index.js index.css没有添加版本号,若是要修改里面的内容,修改以后得加个版本号,要否则获取的是缓存中的老旧资源

在开发调试过程当中,咱们常常须要在控制台对sw进行 skipWaiting、unRegister、delete cache等操做,要在多个场景下去观察和思考sw的状态和改变,而且根据本身的业务设计出合理的缓存方案

基本的离线缓存方案实现了,可是仅仅仍是demo阶段,实际应用中,好比咱们有三方cdn,并且本身的项目通过诸如webpack等工具打包以后各类资源都带版本号,每次都手动修改service-worker.js那是不现实的,并且很是容易出错。serviceWorker结合webpack等工具应用到生产环境上,还有须要作更多的事情。

下一篇将讲述 serviceWorker 结合 webpack 自动生成离线缓存应用。

本文github地址,若是有帮助欢迎star,若是您能提出宝贵意见建议或者issue那就更好了。

相关文章
相关标签/搜索