Service Worker学习与实践(一)——离线缓存

什么是Service Worker

Service Worker本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步APIjavascript

  • Service Worker的本质是一个Web Worker,它独立于JavaScript主线程,所以它不能直接访问DOM,也不能直接访问window对象,可是,Service Worker能够访问navigator对象,也能够经过消息传递的方式(postMessage)与JavaScript主线程进行通讯。
  • Service Worker是一个网络代理,它能够控制Web页面的全部网络请求。
  • Service Worker具备自身的生命周期,使用好Service Worker的关键是灵活控制其生命周期。

Service Worker的做用

  • 用于浏览器缓存
  • 实现离线Web APP
  • 消息推送

Service Worker兼容性

Service Worker是现代浏览器的一个高级特性,它依赖于fetch APICache StoragePromise等,其中,Cache提供了Request / Response对象对的存储机制,Cache Storage存储多个Cachecss

示例

在了解Service Worker的原理以前,先来看一段Service Worker的示例:html

self.importScripts('./serviceworker-cache-polyfill.js');

var urlsToCache = [
  '/',
  '/index.js',
  '/style.css',
  '/favicon.ico',
];

var CACHE_NAME = 'counterxing';

self.addEventListener('install', function(event) {
  self.skipWaiting();
  event.waitUntil(
    caches.open(CACHE_NAME)
    .then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});


self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['counterxing'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

复制代码

下面开始逐段逐段地分析,揭开Service Worker的神秘面纱:java

polyfill

首先看第一行:self.importScripts('./serviceworker-cache-polyfill.js');,这里引入了Cache API的一个polyfill,这个polyfill支持使得在较低版本的浏览器下也可使用Cache Storage API。想要实现Service Worker的功能,通常都须要搭配Cache API代理网络请求到缓存中。webpack

Service Worker线程中,使用importScripts引入polyfill脚本,目的是对低版本浏览器的兼容。git

Cache Resources List And Cache Name

以后,使用一个urlsToCache列表来声明须要缓存的静态资源,再使用一个变量CACHE_NAME来肯定当前缓存的Cache Storage Name,这里能够理解成Cache Storage是一个DB,而CACHE_NAME则是DB名:github

var urlsToCache = [
  '/',
  '/index.js',
  '/style.css',
  '/favicon.ico',
];

var CACHE_NAME = 'counterxing';
复制代码

Lifecycle

Service Worker独立于浏览器JavaScript主线程,有它本身独立的生命周期。web

若是须要在网站上安装Service Worker,则须要在JavaScript主线程中使用如下代码引入Service Workerjson

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    console.log('成功安装', registration.scope);
  }).catch(function(err) {
    console.log(err);
  });
}
复制代码

此处,必定要注意sw.js文件的路径,在个人示例中,处于当前域根目录下,这意味着,Service Worker和网站是同源的,能够为当前网站的全部请求作代理,若是Service Worker被注册到/imaging/sw.js下,那只能代理/imaging下的网络请求。浏览器

可使用Chrome控制台,查看当前页面的Service Worker状况:

安装完成后,Service Worker会经历如下生命周期:

  1. 下载(download
  2. 安装(install
  3. 激活(activate
  • 用户首次访问Service Worker控制的网站或页面时,Service Worker会马上被下载。以后至少每24小时它会被下载一次。它可能被更频繁地下载,不过每24小时必定会被下载一次,以免不良脚本长时间生效。

  • 在下载完成后,开始安装Service Worker,在安装阶段,一般须要缓存一些咱们预先声明的静态资源,在咱们的示例中,经过urlsToCache预先声明。

  • 在安装完成后,会开始进行激活,浏览器会尝试下载Service Worker脚本文件,下载成功后,会与前一次已缓存的Service Worker脚本文件作对比,若是与前一次的Service Worker脚本文件不一样,证实Service Worker已经更新,会触发activate事件。完成激活。

如图所示,为Service Worker大体的生命周期:

install

在安装完成后,尝试缓存一些静态资源:

self.addEventListener('install', function(event) {
  self.skipWaiting();
  event.waitUntil(
    caches.open(CACHE_NAME)
    .then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});
复制代码

首先,self.skipWaiting()执行,告知浏览器直接跳过等待阶段,淘汰过时的sw.jsService Worker脚本,直接开始尝试激活新的Service Worker

而后使用caches.open打开一个Cache,打开后,经过cache.addAll尝试缓存咱们预先声明的静态文件。

监听fetch,代理网络请求

页面的全部网络请求,都会经过Service Workerfetch事件触发,Service Worker经过caches.match尝试从Cache中查找缓存,缓存若是命中,则直接返回缓存中的response,不然,建立一个真实的网络请求。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});
复制代码

若是咱们须要在请求过程当中,再向Cache Storage中添加新的缓存,能够经过cache.put方法添加,看如下例子:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      // 缓存命中
      if (response) {
        return response;
      }

      // 注意,这里必须使用clone方法克隆这个请求
      // 缘由是response是一个Stream,为了让浏览器跟缓存都使用这个response
      // 必须克隆这个response,一份到浏览器,一份到缓存中缓存。
      // 只能被消费一次,想要再次消费,必须clone一次
      var fetchRequest = event.request.clone();

      return fetch(fetchRequest).then(
        function(response) {
          // 必须是有效请求,必须是同源响应,第三方的请求,由于不可控,最好不要缓存
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }

          // 消费过一次,又须要再克隆一次
          var responseToCache = response.clone();
          caches.open(CACHE_NAME)
            .then(function(cache) {
              cache.put(event.request, responseToCache);
            });
          return response;
        }
      );
    })
  );
});
复制代码

在项目中,必定要注意控制缓存,接口请求通常是不推荐缓存的。因此在我本身的项目中,并无在这里作动态的缓存方案。

activate

Service Worker总有须要更新的一天,随着版本迭代,某一天,咱们须要把新版本的功能发布上线,此时须要淘汰掉旧的缓存,旧的Service WorkerCache Storage如何淘汰呢?

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['counterxing'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
复制代码
  1. 首先有一个白名单,白名单中的Cache是不被淘汰的。
  2. 以后经过caches.keys()拿到全部的Cache Storage,把不在白名单中的Cache淘汰。
  3. 淘汰使用caches.delete()方法。它接收cacheName做为参数,删除该cacheName全部缓存。

sw-precache-webpack-plugin

sw-precache-webpack-plugin是一个webpack plugin,能够经过配置的方式在webpack打包时生成咱们想要的sw.jsService Worker脚本。

一个最简单的配置以下:

var path = require('path');
var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');

const PUBLIC_PATH = 'https://www.my-project-name.com/';  // webpack needs the trailing slash for output.publicPath

module.exports = {

  entry: {
    main: path.resolve(__dirname, 'src/index'),
  },

  output: {
    path: path.resolve(__dirname, 'src/bundles/'),
    filename: '[name]-[hash].js',
    publicPath: PUBLIC_PATH,
  },

  plugins: [
    new SWPrecacheWebpackPlugin(
      {
        cacheId: 'my-project-name',
        dontCacheBustUrlsMatching: /\.\w{8}\./,
        filename: 'service-worker.js',
        minify: true,
        navigateFallback: PUBLIC_PATH + 'index.html',
        staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
      }
    ),
  ],
}
复制代码

在执行webpack打包后,会生成一个名为service-worker.js文件,用于缓存webpack打包后的静态文件。

一个最简单的示例

Service Worker Cache VS Http Cache

对比起Http Header缓存,Service Worker配合Cache Storage也有本身的优点:

  1. 缓存与更新并存:每次更新版本,借助Service Worker能够立马使用缓存返回,但与此同时能够发起请求,校验是否有新版本更新。
  2. 无侵入式:hash值实在是太难看了。
  3. 不易被冲掉:Http缓存容易被冲掉,也容易过时,而Cache Storage则不容易被冲掉。也没有过时时间的说法。
  4. 离线:借助Service Worker能够实现离线访问应用。

可是缺点是,因为Service Worker依赖于fetch API、依赖于PromiseCache Storage等,兼容性不太好。

后话

本文只是简单总结了Service Worker的基本使用和使用Service Worker作客户端缓存的简单方式,然而,Service Worker的做用远不止于此,例如:借助Service Worker作离线应用、用于作网络应用的推送(可参考push-notifications)等。

甚至能够借助Service Worker,对接口进行缓存,在我所在的项目中,其实并不会作的这么复杂。不过作接口缓存的好处是支持离线访问,对离线状态下也能正常访问咱们的Web应用。

Cache StorageService Worker老是分不开的。Service Worker的最佳用法其实就是配合Cache Storage作离线缓存。借助于Service Worker,能够轻松实现对网络请求的控制,对于不一样的网络请求,采起不一样的策略。例如对于Cache的策略,其实也是存在多种状况。例如能够优先使用网络请求,在网络请求失败时再使用缓存、亦能够同时使用缓存和网络请求,一方面检查请求,一方面有检查缓存,而后看两个谁快,就用谁。

相关文章
相关标签/搜索