CodingMeUp / AboutFE

知识归纳、内容都在issue里
74 stars 14 forks source link

41、Service Worker 离线、 PWA #42

Open CodingMeUp opened 4 years ago

CodingMeUp commented 4 years ago

Service Worker

Service Worker 是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 现在,它们已包括如离线缓存,转发请求和网络代理等功能。Service Worker实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求

在 Service Worker 出现前,存在能够在网络上为用户提供离线体验的另一个 API,称为 AppCache(html5 manifest缓存技术)。 AppCache API 存在的许多相关问题,在设计 Service Worker 时已予以避免。

基于Service Worker和cacheStorage缓存及离线开发,套路非常固定,无侵入,且语言纯正,直接复制粘贴就可以实现缓存和离线功能,纯前端,无需服务器配合。一个看上去很酷的功能只要复制粘贴就可以实现,绝对是成本极低的,小白中的小白也能上手。

由于成本几乎可以忽略不计,此时离线功能支持的投入产出比相当高了,经验告诉我这种技术以后流行和普及的可能性非常高

Service Worker 相关注意事项:

  1. 它是一种 JavaScript Worker,无法直接访问 DOM。 Service Worker 通过响应 [postMessage] (https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage) 接口发送的消息来与其控制的页面通信,页面可在必要时对 DOM 执行操作。 我们平常浏览器窗口中跑的页面运行的是主JavaScript线程,DOM和window全局变量都是可以访问的。而Service Worker是走的另外的线程,可以理解为在浏览器背后默默运行的一个线程,脱离浏览器窗体,因此,window以及DOM都是不能访问的,此时我们可以使用self访问全局上下文,可参见这篇短文:“了解JS中的全局对象window.self和全局作用域self”。 由于Service Worker走的是另外的线程,因此,就算这个线程翻江倒海也不会阻塞主JavaScript线程,也就是不会引起浏览器页面加载的卡顿之类。同时,由于Service Worker设计为完全异步,同步API(如XHR和localStorage)不能在Service Worker中使用。
  2. Service Worker 是一种可编程网络代理,让您能够控制页面所发送网络请求的处理方式。
  3. Service Worker 在不用时会被中止,并在下次有需要时重启,因此,您不能依赖 Service Worker onfetch 和 onmessage 处理程序中的全局状态。 如果存在您需要持续保存并在重启后加以重用的信息,Service Worker 可以访问 IndexedDB API
  4. Service Worker 广泛地利用了 promise,因此如果您不熟悉 promise,则应停下阅读此内容,看一看 Promise 简介。
  5. HTTPS必须,您可能会善意地使用这些功能,但中间人可会将其用于不良目的。 为避免这种情况,可仅在通过 HTTPS 提供的页面上注册 Service Worker,如此我们便知道浏览器接收的 Service Worker 在整个网络传输过程中都没有被篡改。

Service Worker 生命周期

Service Worker 的生命周期完全独立于网页。

要为网站安装服务工作线程,您需要先在页面的 JavaScript 中注册。 注册服务工作线程将会导致浏览器在后台启动服务工作线程安装步骤。

在安装过程中,您通常需要缓存某些静态资产。 如果所有文件均已成功缓存,那么 Service Worker 就安装完毕。 如果任何文件下载失败或缓存失败,那么安装步骤将会失败,Service Worker 就无法激活(也就是说, 不会安装)。 如果发生这种情况,不必担心,它下次会再试一次。 但这意味着,如果安装完成,您可以知道您已在缓存中获得那些静态资产。

安装之后,接下来就是激活步骤,这是管理旧缓存的绝佳机会,我们将在 Service Worker 的更新部分对此详加介绍。

激活之后,Service Worker 将会对其作用域内的所有页面实施控制,不过,首次注册该 Service Worker 的页面需要再次加载才会受其控制。 服务工作线程实施控制后,它将处于以下两种状态之一:服务工作线程终止以节省内存,或处理获取和消息事件,从页面发出网络请求或消息后将会出现后一种状态。

以下是 Service Worker 初始安装时的简化生命周期。

兼容性

注册 Service Worker

若要安装 Service Worker,您需要通过在页面中对其进行注册来启动安装。 这将告诉浏览器 Service Worker JavaScript 文件的位置

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

此代码用于检查 Service Worker API 是否可用,如果可用,则在页面加载后注册位于 /sw.js 的 Service Worker。

每次页面加载无误时,即可调用 register();浏览器将会判断服务工作线程是否已注册并做出相应的处理。

register() 方法的精妙之处在于服务工作线程文件的位置。 您会发现在本例中服务工作线程文件位于根网域。 这意味着服务工作线程的作用域将是整个来源。 换句话说,Service Worker 将接收此网域上所有事项的 fetch 事件。 如果我们在 /example/sw.js 处注册 Service Worker 文件,则 Service Worker 将只能看到网址以 /example/ 开头(即 /example/page1/、/example/page2/)的页面的 fetch 事件。

现在,您可以通过转至 chrome://inspect/#service-workers 并寻找您的网站来检查 Service Worker 是否已启用。

安装 Service Worker

在受控页面启动注册流程后,我们来看看处理 install 事件的 Service Worker 脚本。

最基本的例子是,您需要为安装事件定义回调,并决定想要缓存的文件。

self.addEventListener('install', function(event) {
  // Perform install steps 在 install 回调的内部,我们需要执行以下步骤:
- 打开缓存。
- 缓存文件。
- 确认所有需要的资产是否已缓存
});

var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

此处,我们以所需的缓存名称调用 caches.open(),之后再调用 cache.addAll() 并传入文件数组。 这是一个 promise 链(caches.open() 和 cache.addAll())。 event.waitUntil() 方法带有 promise 参数并使用它来判断安装所花费的时间,以及安装是否成功。

如果所有文件都成功缓存,则将安装 Service Worker。 如有任何文件无法下载,则安装步骤将失败。 这可让您依赖于所定义的所有资产,但也意味着需要对您决定在安装步骤缓存的文件列表格外留意。 定义一个过长的文件列表将会增加文件缓存失败的几率,从而导致服务工作线程未能安装。

这仅是一个示例,实际您可以在 install 事件中执行其他任务,或完全避免设置 install 事件侦听器。

缓存和返回请求

在安装 Service Worker 且用户转至其他页面或刷新当前页面后,Service Worker 将开始接收 fetch 事件。

其中,Cache直接和请求打交道,CacheStorage和Cache对象打交道,我们可以直接使用全局的caches属性访问CacheStorage,例如,虽然API上显示的是CacheStorage.open(),但我们实际使用的时候,直接caches.open()就可以了。

Cache和CacheStorage的出现让浏览器的缓存类型又多了一个:之前有memoryCache和diskCache,现在又多了个ServiceWorker cache。

下面提供了一个示例。

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);
      }
    )
  );
});

这里我们定义了 fetch 事件,并且在 event.respondWith() 中,我们传入来自 caches.match() 的一个 promise。 此方法检视该请求,并从服务工作线程所创建的任何缓存中查找缓存的结果。

如果发现匹配的响应,则返回缓存的值,否则,将调用 fetch 以发出网络请求,并将从网络检索到的任何数据作为结果返回。 这是一个简单的例子,它使用了在安装步骤中缓存的所有资产。

如果希望连续缓存新请求,可以通过处理 fetch 请求的响应并将其添加到缓存来实现,如下所示。

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

        // IMPORTANT:Clone the request. A request is a stream and
        // can only be consumed once. Since we are consuming this
        // once by cache and once by the browser for fetch, we need
        // to clone the response.
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).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;
          }
        );
      })
    );
});

执行的操作如下:

  1. 在 fetch 请求中添加对 .then() 的回调。
  2. 获得响应后,执行以下检查:
    • 确保响应有效。
    • 检查并确保响应的状态为 200。
    • 确保响应类型为 basic,亦即由自身发起的请求。 这意味着,对第三方资产的请求也不会添加到缓存。
  3. 如果通过检查,则克隆响应。 这样做的原因在于,该响应是数据流, 因此主体只能使用一次。 由于我们想要返回能被浏览器使用的响应,并将其传递到缓存以供使用,因此需要克隆一份副本。我们将一份发送给浏览器,另一份则保留在缓存。

更新 Service Worker

在某个时间点,您的 Service Worker 需要更新。 此时,您需要遵循以下步骤:

  1. 更新您的服务工作线程 JavaScript 文件。 用户导航至您的站点时,浏览器会尝试在后台重新下载定义 Service Worker 的脚本文件。 如果 Service Worker 文件与其当前所用文件存在字节差异,则将其视为新 Service Worker。
  2. 新 Service Worker 将会启动,且将会触发 install 事件。
  3. 此时,旧 Service Worker 仍控制着当前页面,因此新 Service Worker 将进入 waiting 状态。
  4. 当网站上当前打开的页面关闭时,旧 Service Worker 将会被终止,新 Service Worker 将会取得控制权。
  5. 新 Service Worker 取得控制权后,将会触发其 activate 事件。

出现在 activate 回调中的一个常见任务是缓存管理。 您希望在 activate 回调中执行此任务的原因在于,如果您在安装步骤中清除了任何旧缓存,则继续控制所有当前页面的任何旧 Service Worker 将突然无法从缓存中提供文件。

比如说我们有一个名为 'my-site-cache-v1' 的缓存,我们想要将该缓存拆分为一个页面缓存和一个博文缓存。 这就意味着在安装步骤中我们创建了两个缓存:'pages-cache-v1' 和 'blog-posts-cache-v1',且在激活步骤中我们要删除旧的 'my-site-cache-v1'。

以下代码将执行此操作,具体做法为:遍历 Service Worker 中的所有缓存,并删除未在缓存白名单中定义的任何缓存。

self.addEventListener('activate', function(event) {

  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];

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

Service Worker 场景

Service Worker除了可以缓存和离线开发,其可以应用的场景还有很多,举几个例子(参考自MDN):

CodingMeUp commented 4 years ago

SW和PWA技术的关系

PWA全称为“Progressive Web Apps”,渐进式网页应用。功效显著,收益明显

PWA的核心技术包括:

有此可见,Service Worker仅仅是PWA技术中的一部分,但是又独立于PWA。也就是虽然PWA技术面向移动端,但是并不影响我们在桌面端渐进使用Service Worker,考虑到自己厂子用户70%~80%都是Chrome内核,收益或许会比预期的要高。