资讯专栏INFORMATION COLUMN

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

xiaochao / 1797人阅读

摘要:的本质是一个,它独立于主线程,因此它不能直接访问,也不能直接访问对象,但是,可以访问对象,也可以通过消息传递的方式与主线程进行通信。的最佳用法其实就是配合做离线缓存。

什么是Service Worker

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

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存储多个Cache

示例

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

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的神秘面纱:

polyfill

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

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

Cache Resources List And Cache Name

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

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

var CACHE_NAME = "counterxing";
Lifecycle

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

如果需要在网站上安装Service Worker,则需要在JavaScript主线程中使用以下代码引入Service Worker

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会经历以下生命周期:

下载(download

安装(install

激活(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);
          }
        })
      );
    })
  );
});

首先有一个白名单,白名单中的Cache是不被淘汰的。

之后通过caches.keys()拿到所有的Cache Storage,把不在白名单中的Cache淘汰。

淘汰使用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也有自己的优势:

缓存与更新并存:每次更新版本,借助Service Worker可以立马使用缓存返回,但与此同时可以发起请求,校验是否有新版本更新。

无侵入式:hash值实在是太难看了。

不易被冲掉:Http缓存容易被冲掉,也容易过期,而Cache Storage则不容易被冲掉。也没有过期时间的说法。

离线:借助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的策略,其实也是存在多种情况。例如可以优先使用网络请求,在网络请求失败时再使用缓存、亦可以同时使用缓存和网络请求,一方面检查请求,一方面有检查缓存,然后看两个谁快,就用谁。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97897.html

相关文章

  • 【PWA学习实践】(3) 让你的WebApp离线可用

    摘要:学习与实践系列文章已整理至学习手册,文字内容已同步至。本文是学习与实践系列的第三篇文章。引言其中一个令人着迷的能力就是离线可用。但是,如果你注意到文章开头的图片就会发现,离线时我们不仅可以访问,还可以使用搜索功能。 《PWA学习与实践》系列文章已整理至gitbook - PWA学习手册,文字内容已同步至learning-pwa-ebook。转载请注明作者与出处。 本文是《PWA学习与实...

    since1986 评论0 收藏0
  • Service Worker学习实践(二)——PWA简介

    摘要:简称,是提升的体验的一种新方法,能给用户原生应用的体验。当网站以这种方式启动时它具有唯一的图标和名称,以便用户将其与其他网站区分开来。表示启动时的方向,横屏竖屏等,具体参数值可参考文档。下一篇文章中,主要讲述在实践中的重要能力。 这周,Chrome 70正式版本发布,Progressive Web Apps(PWA)已经正式支持到Windows 10平台,然而,早在前几个版本之前,Ch...

    KavenFan 评论0 收藏0
  • 【PWA学习实践】(5)在Web中进行服务端消息推送

    摘要:本文是学习与实践系列的第五篇文章。实际上,消息推送与提醒是两个功能和。在这一篇里,我们先来学习如何使用进行消息推送。而当服务端要推送消息时,会使用私钥对发送的数据进行数字签名,并根据数字签名生成一个叫请求头。 《PWA学习与实践》系列文章已整理至gitbook - PWA学习手册,文字内容已同步至learning-pwa-ebook。转载请注明作者与出处。 本文是《PWA学习与实践》系...

    suemi 评论0 收藏0
  • [译]介绍下渐进式 Web App(离线) - Part 1

    摘要:基本上是使用现代技术构建的网站但是体验上却像一个移动,在年,谷歌工程师和创造了。此后谷歌就一直致力于让能给用户像原生一般的体验。检查谷歌浏览器的和现在重载你的并且打开,到选项去查看面板,确保这个选项是勾选的。 Web开发多年来有了显著的发展。它允许开发人员部署网站或Web应用程序并在数分钟内为全球数百万人服务。只需一个浏览器,用户可以输入URL就可以访问Web应用程序了。随着 Prog...

    Wildcard 评论0 收藏0
  • [译]介绍下渐进式 Web App(离线) - Part 1

    摘要:基本上是使用现代技术构建的网站但是体验上却像一个移动,在年,谷歌工程师和创造了。此后谷歌就一直致力于让能给用户像原生一般的体验。检查谷歌浏览器的和现在重载你的并且打开,到选项去查看面板,确保这个选项是勾选的。 Web开发多年来有了显著的发展。它允许开发人员部署网站或Web应用程序并在数分钟内为全球数百万人服务。只需一个浏览器,用户可以输入URL就可以访问Web应用程序了。随着 Prog...

    gaara 评论0 收藏0

发表评论

0条评论

xiaochao

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<