使用Service Worker功能接口,增强你的网站缓存和离线功能

尝尝鲜理解Service Worker功能

这里有一些实例网页: Google开发者文档, 宝可梦图鉴。按下面的步骤来把玩一下吧:
打开你的浏览器(拒绝IE和多年未更新的古董!),访问这些网页,确认网页加载完成后关闭它们。
断开你的网络连接(拔网线)。
重新在浏览器里打开这些页面。
震惊!!我们仍然可以正常的访问并浏览页面的内容。(这就是Service Worker的强大)
打开其他网站如google.com再试试,果不其然我们看到了熟悉的小恐龙。 

追史溯源

在WHATWG的 HTML 标准中提到关于离线应用,html 标签有一个manifest 属性,并且有相应的.appcache 格式文件用于为离线应用缓存资源。
这仍然是一个可用的浏览器特性,但不在被推荐了。因为有了Service Worker。

基本概念

Service Worker是一种事件驱动的特殊的web worker,注册于指定的源和路径。它使用一个特定格式的JavaScript文件用于与它的网页关联,监听或拦截来自这个页面的访问和资源请求甚至可以修改它们。同时它可以缓存页面的资源来使得网页在一些特殊的情况下仍然可以按预期的行为运作(最常见的就是网络不可用的状况了)。
换而言之,Service Worker使得启用它的应用能够控制网络请求,缓存请求内容来优化性能,并且拥有离线访问已缓存的内容的能力,就像一个本地应用一样工作。
Service Worker 以来于两个核心API来使得网页应用离线工作: Fetch (从网络获取资源的标准方式) and Cache (为应用数据提供持久’化的存储)。注意这里的cache是应用缓存 (参见DevTools -> Application -> Cache),需要区分于浏览器层面的缓存,应用缓存的失效和过期需要由service worker来管理。
总之,它就像是一个“HTTP请求的本地代理服务器”+“响应和资源的缓存管理器”。

Service Worker存在的目的

作为PWA (Progressive Web Application, 渐进式网络应用)的核心。
提供更好的网页性能。
提供优秀的离线和弱网环境的使用体验。
(为网络基础设施落后地区的人民提供可用的网络体验也是重要的政治正确)
使用Service Worker的必要条件
浏览器支持使用最新的Chrome, Firefox, Edge, Safari等。
在这里测试你的浏览器是否支持。
必须使用HTTPS
基于Service Worker的核心能力,能够劫持网络连接,伪造和过滤响应结果,它在网络层面过于强大也赋予了使用者做坏事的能力。为了防止中间人威胁到启用了Service Worker的页面的安全,它被设计成只能使用于HTTPS站点。

生命周期

Service Worker的初次运行生命周期流程如下图所示,从未注册开始主要分为Installed, Activated, IDLE 等阶段。

不同生命周期阶段转移的细节:

安装注册

在Service Worker被安装之前,首先需要在HTML页面内的脚本里注册server worker的脚本,即前面提到过的特定格式的JavaScript见(例如: sw.js)。注册这一步是在运行网页自身的脚本时告知浏览器:
这个页面是使用了Service Worker的,请加载指定路径的Service Worker的脚本并将它安装到当前域。
这一步本身不属于Service Worker的生命周期,因为它是需要在页面脚本里完成的,这和WebSocket比较类似。如下面的脚本(注意:它必须是HTML页面中script标签的一部分,而不是sw.js中的):

if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register('/sw.js').then(function(registration) {
   // Registration was successful
   console.log(':) Success. ', registration.scope);
 }, function(err) {
   // registration failed :(
   console.log(':( Failed. ', err);
 });
}

上面的代码为当前页面所在的域注册了sw.js,我们还可以使用scope为它的指定更为细分的范围。也就是说,只有发送往当前域中来自scope中指定子路径的请求才会被Service Worker拦截,其他路径的请求将不受影响。如下图代码,只有向”https://domain/app/“及其子路径作为url的请求才会被service-worker.js里绑定的事件响应。

navigator.serviceWorker.register('/service-worker.js', {
  scope: '/app/'
});

若scope未被指定,那么默认的scope是获取service worker脚本所在的同级域,及其响应的子域。

生命周期事件

Service Worker中的主要生命周期阶段的变化,是通过事件来通知脚本的,所以Serice Worker的脚本主要需要做的是为不同生命周期事件绑定好对应的处理器。核心的生命周期事件处理器如图示: 

下载事件 (Dowload)

下载事件未在上图中标出,它发生在脚本注册阶段被触发且由浏览器处理: 1) 从指定路径下载service worker的脚本; 2) 完成脚本的解析,如果不符合service worker脚本的规范则不会触发安装事件; 3) 正确的脚本被安装到浏览器的Service Worker列表,被触发安装事件; 下载失败,解析失败或者初始化错误引发的错误也可以在浏览器的开发者工具 -> 应用 -> Service Worker列表看到:

安装事件 (Install)

在下载事件被浏览器响应完成后,安装事件是Service Worker生命周期中第一个被触发的,所以它非常重要。我们通常使用它来完成各种初始化任务,主要是资源的预加载、缓存以及持久化一些长期有效的状态。
这里主要被使用的是Cache API,监听到安装事件就可以预先拉取一些资源,并在应用缓存中存储它们并用给定的名字标识,参见下面的代码示例。在下文中有关于API的更详细的内容。 

const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [  // The list of resources to cache
  '/',
  '/styles/main.css',
  '/script/main.js'
];
​
self.addEventListener('install', event => {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

鉴于Service Worker本质上类似于Web Worker,它的资源加载完全独立于页面进程,所以它完全不会影响首屏渲染的性能。当然了,我们也完全不能预期在首次加载中就能受到应用缓存带来的优化。


激活事件 (activate)

当Service Worker已经就绪,可以控制页面请求并处理功能事件如fetch,push和sync,激活事件就会被触发,我们监听激活事件的处理器就可以响应。 

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

激活事件标志着从这之后的请求将被Service Worker接管,所以通常用于区分Service Worker接管前后的分隔。 但这不意味着当前这次有效完成".register()"的页面会受到Service Worker 管理,因为我们无法预测页面资源先获取完成还是激活事件先响应。

请求事件 (fetch)

fetch事件被触发于页面对网络发起任意请求,当然我们前面已经提过:

1)必须在激活事件触发后service worker接管网络请求;

2)请求的目标url必须位于注册的资源范围(scope)内。
来看一下这个 demo, 这里Service Worker劫持了"fetch"请求,但只在激活后才会生效:
当你第一次打开这个demo页面,你应该在等待后看到一张狗的图片,因为这个页面的标签指向的就是一张dog.svg。对这个图片的请求在完成Service Worker的注册之前就已经发出去了,响应的自然是狗的图片。
然后刷新页面,这一次我们看到的却是猫的剪影图片了,之后反复刷新都会是猫的图片。因为Service Worker已经完成了注册,开始接管对图片资源的HTTP请求,并将我们的请求换成它准备好的猫图了。
我们来看看SW的脚本里都做了什么: 

// Caching the cat image when installing
self.addEventListener('install', event => {
  console.log('V2 installing…');
​
  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/cat.svg'))
  );
});
​
// Response the cat image when requesting dog.
self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);  
  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname.endsWith('/dog.svg')) {
    event.respondWith(caches.match('cat.svg'));
  }
});

在安装事件中,service worker请求了猫图cat.svg并加入缓存,这是页面进程完全不知道的。
而在fetch事件中,我们劫持了网络请求,当资源目标为狗图时,service worker没有乖乖地转发请求,而是从缓存列表取出猫图,回复给了页面。同样,我们的页面并不知道这个请求根本没有通过网络,而是被service worker返回的资源应付了。就会有这张猫图在页面的资源列表里被标注成了请求的dog.svg的奇妙效果:

在上面提到的Service Worker的开发这界面,我们可以通过点击"unregiter"来取消当前service worker脚本的注册,这样在下一次打开时虽然service worker会被再次注册,至少在那一次的页面我们见到的将还是狗图了。


APIs

Service Worker依赖于包括cache和fetch在内的许多相关的浏览器API来使得网页应用更加功能丰富,越来越接近原生应用的体验。(注:这里的cache和fetch是指浏览器接口,与上文的Service Worker的生命周期事件注意区分)


Cache & Fetch API

作为一个网页应用,除了前端无法控制的服务接口,最重要的无非是获取和存储资源了,fetch 和 cache 两个接口作为现代浏览器的重要接口,Service Worker的fetch事件的响应几乎可以说是重度依赖于他们。如果浏览器版本捎老一些,可能还需要使用腻子脚本(polyfill)来为提供这两个接口。
通过它们,Service Worker可以在截取页面的请求后,修改请求的参数内容,修改请求路径(注意跨域),延迟响应,修改响应内容,使用缓存内容伪造响应内容,甚至只用缓存构造一个完全不再依赖于网络的页面等等。


cache 

添加资源到缓存对象中:

self.addEventListener('install', function(event) {
  // In install event, cache the resources first
  event.waitUntil(
    caches.open('my-cache-identifier')   // Open/create a cache with identifier
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll([    
          '/',
          '/styles/main.css',
          '/script/main.js'
        ]); // Cache the major HTML, CSS, JS file
      })
  );
});

需要注意,一个缓存对象可以添加多个资源路径->响应结果。

fetch

Fetch API 是浏览器新的API标准,在Service Worker中通常被用于在 FetchEvent 中转发请求。
当网页正在发送HTTP请求,触发了Service Worker的fetch事件,我们就能在劫持HTTP包中的内容,按照需要进行读取或者修改,然后再继续发送或者转发给其他目标。
通常有以下操作流程或其中一部分:
检查请求目标是否已被缓存,如果已存在直接回复缓存内容;
拆包分析请求内容,筛选请求,阻止某些请求被发送,或者修改其内容;
使用 fetch API 发送请求,或者转发请求;
当得到响应,拆包检查响应状态,类型或者其他的HTTP头,按照需要筛选或者修改回复体的内容;
按需缓存响应结果;
流程代码示例: 

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }
​
        return fetch(event.request).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
​
            // IMPORTANT: Clone the response. A response is a stream
            // and because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            var responseToCache = response.clone();
​
            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });
​
            return response;
          }
        );
      })
    );
});

存储 & 通信API
Fetch 是通信用的API , cache是存储用的。但他们通常适用于“代理网页的网络请求”和 “缓存网页的资源”。
如果Service Worker还有其他的通信需求(如直接和页面通信,或者其他的Web Worker通信),或者存储不同需求的数据(如页面层数据,浏览器层数据,更大量的数据等),还有以下常用的API。
进程间通信
上文多次提及Web Worker, Service Worker与常规的Web Worker都是独立于渲染上下文的独立线程,所以它们都是无法直接操作DOM或者window对象的。如果我们有和其他Worker或者页面交互的需求,可以使用 postMessage API 和 message事件来进行进程/线程间通信。
在页面的主线程创建消息频道 MessageChannel,使用 postMessage 向频道上发送消息并且监听上面来的 message 事件:

function sendMessage(message) {
  // This wraps the message posting/response in a promise, which will resolve if the response doesn't
  // contain an error, and reject with the error if it does. If you'd prefer, it's possible to call
  // controller.postMessage() and set up the onmessage handler independently of a promise, but this is
  // a convenient wrapper.
  return new Promise(function(resolve, reject) {
    var messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = function(event) {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };
​
    // This sends the message data as well as transferring messageChannel.port2 to the service worker.
    // The service worker can then use the transferred port to reply via postMessage(), which
    // will in turn trigger the onmessage handler on messageChannel.port1.
    // See https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
    navigator.serviceWorker.controller.postMessage(message,
      [messageChannel.port2]);
  });
}

在Service Worker中,使用Client对象上的Client.postMessage来发送消息, 并且监听messageService Worker自己的消息事件:

// Consume the message from host thread (or other Workers)
addEventListener('message', (event) => {
    console.log(`The client sent me a message: ${event.data}`);
});
​
{
  // Send message to host thread (or other Workers)
  clients.matchAll(event.clientId).postMessage({
    msg: "Hey I just got a fetch from you!",
  });
}

数据存储

Service Worker中也能使用各种新旧浏览器标准下的Web存储API来持久化数据,我们可以依照不同的需要来选择:

注意Service Worker的官方标准提到它是完全基于Promise的异步非阻塞 (it's designed to be fully asynchronous),同步的XHR请求和 localStorage (LocalStorage请求都是完全同步的) 在Service Worker不可以被使用。
对于真正需要被长期持久化且在浏览器重启之后也需要被复用的内容,使用IndexedDB是更加建议的方案,甚至可以在这之上做类似基于数据库的数据同步。

更多 Web 应用的API

Service Worker作为PWA的核心概念之一,它也是将web应用变得更接近原生应用的出发点。在它之上,更多浏览器特性提供了类似原生应用的支持。

使用场景

Server Worker在PWA之外也有诸多应用,基于它对HTTP请求和响应的强大管理能力,它可以作为多种依赖网络的应用的核心流程管理器。

cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
  return new Request(urlToPrefetch, { mode: 'no-cors' });
})).then(function() {
  console.log('All resources have been fetched and cached.');
});

​30X的HTTP状态码尚不支持离线请求重定向, 这是一个已知的issue。建议在官方支持离线重定向前,根据你的使用场景寻找其他方案,
在使用Service Worker代理HTTP的响应体时,务必记住clone response,而不要直接消费掉响应体。 原因是HTTP response是一个 流, 它的内容只能被消费一次。 只要我们仍然希望既能让浏览器正确的获得响应体中的内容,又能是它被缓存或者在Service Worker作内容检查,请不要忘记复制一个响应体。

  1. 全静态站点
    如果一个网站只包含静态数据而无需服务, 我们可以缓存所有的html页面,css样式,脚本和图片等资源,来使得这个页面在初次打开后可以被完全地离线访问。
  2. 预加载
    为了优化首屏渲染,页面上非必要的资源通常被延迟加载直到它们被需要。这类资源使用Server Worker来加载既可以使得在需要被加载时有良好的体验,有不会影响到首屏性能。
  3. 应变响应
    有时候HTTP请求可能会因为不确定因素失败(如服务器离线,网络中断等),此时为用户提供一个应变的响应比如展示上一次成功加载的资源/数据。(例如:实时数据监测)
    Service worker可以帮助验证请求是否成功,如果失败就提供应变策略的回复。 
  4. 仿造响应
    仿造响应是非常有用。它可以帮助我们隔离部分特定的请求来使用给定的回复,或者我们可以用它来测试一些尚不可用,或者不能稳定重现问题的资源或者REST API.
  5. 窗口缓存
    Service Worker来承担缓存数据的责任,页面可以直接使用window.cache来访问缓存。
    通过窗口缓存作为媒介可以间接实现service worker向页面的数据传递,也可以将Service Worker用作缓存的生产者而页面作为消费者。 
  6. 在Service Worker中使用fetch API来转发请求,请求中默认不会包含cookie等中的用户认证信息。
    如果需要为转发请求附带认证信息, 在fetch请求中添加'credentials'的参数:
    fetch(url, {
      credentials: 'include'
    })
    

    跨域资源默认是不支持缓存的,需要额外参数。

  7. 如果目标资源支持CORS,在构建请求需要附带参数 {mode: 'cors'} 。
  8. 如果目标资源不支持CORS或者不确定, 我们可以使用 non-cors模式,
    但这会导致"不透明"的响应, 意味着Service Worker不能判断响应中的状态,不透明的结果被缓存后仍被页面消费成non-cors的响应。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1024小神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值