Service Workers

本文试图解答几个小问题

先来看看业界如何评说:

Service Workers 让 Web 应用感觉更像是原生应用 by David Iffland, 王沛。

The ServiceWorker: The network layer is yours to own by Jake Archibald.

Service worker is a game-changing API that will allow for the development of new types of web applications by Alex Mackey.

Service Workers 是什么?

简单的说 Service Workers 就是一个生命周期短暂的、事件驱动的后台线程,它处理来自系统和被其控制的页面的事件。WebApp与Native App再战一轮?

At their simplest, Service Workers are scripts that act as client-side proxies for web pages. ServiceWorkers and Firefox

A service worker is a method that enables applications to take advantage of persistent background processing, including hooks to enable bootstrapping of web applications while offline. Service Workers – W3C Working Draft 25 June 2015

Basic registration 示例:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('service-worker.js', {scope: './'}).then(function(registration) {
    document.querySelector('#status').textContent = 'succeeded';
  }).catch(function(error) {
    document.querySelector('#status').textContent = error;
  });
} else {
  // The current browser doesn't support service workers.
}

Service Workers 和 Web Workers 有关系吗?

A service worker is a type of web worker. The core of this system is an event-driven Web Worker, which responds to events dispatched from documents and other sources. A system for managing installation, versions, and upgrades is provided. Service Workers – W3C Working Draft 25 June 2015

The service worker is like a shared worker, but whereas pages control a shared worker, a service worker controls pages. Service Worker - first draft published

Web Worker 可以在后台运行,但是它依赖于页面,不能在页面不存在的时候运行。WebApp与Native App再战一轮?

Dedicated Worker 示例:

<!-- page.html -->
<!DOCTYPE HTML>
<html>
 <head>
  <title>Worker example: One-core computation</title>
 </head>
 <body>
  <p>The highest prime number discovered so far is: <output id="result"></output></p>
  <script>
   var worker = new Worker('worker.js');
   worker.onmessage = function (event) {
     document.getElementById('result').textContent = event.data;
   };
  </script>
 </body>
</html>

/* worker.js */
var n = 1;
search: while (true) {
  n += 1;
  for (var i = 2; i <= Math.sqrt(n); i += 1)
    if (n % i == 0)
     continue search;
  // found a prime!
  postMessage(n);
}

Shared Worker 示例:

<!-- test.html -->
<!DOCTYPE HTML>
<title>Shared workers: demo 1</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.onmessage = function(e) { // note: not worker.onmessage!
    log.textContent += '\n' + e.data;
  }
</script>

/* test.js */
onconnect = function(e) {
  var port = e.ports[0];
  port.postMessage('Hello World!');
}

Service Workers 能做啥?

现在 service worker 的最佳使用场景是提供离线能力。开发人员可以注册一个 service worker 作为网络代理提供网络拦截。即使没有可用的网络时,这个代理也能够对缓存的数据和资源或者是已经生成的内容作出响应。Service Workers 让 Web 应用感觉更像是原生应用

目前可以通过 Service Worker 实现的功能包括:替代坑坑洼洼的 Application Cache 的可编程离线缓存,Push Notification(消息推送),Background Sync(后台同步)。Service Worker 能成为诸多需要跨越页面处理能力的入口。比如如果你怀念 Web Intents 的话,Service Worker 也许也能成为它复活的平台:通过 Service Worker 注册某个 intent 事件,在事件到来时 worker 被启动,针对不同的 intent worker 可以选择打开不同的页面或重新聚焦某个已经打开的页面。WebApp与Native App再战一轮?

There are three main situations where I think service worker is particularly helpful: (1) Offline applications; (2) Performance; (3) Notification/mobile sites (in future). Get to know service worker and how to use it

Pre-fetching Resources During Registration 示例:

/* prefetch/index.html */
function showFilesList() {
  document.querySelector('#files').style.display = 'block';
}

// Helper function which returns a promise which resolves once the service worker registration
// is past the "installing" state.
function waitUntilInstalled(registration) {
  return new Promise(function(resolve, reject) {
    if (registration.installing) {
      // If the current registration represents the "installing" service worker, then wait
      // until the installation step (during which the resources are pre-fetched) completes
      // to display the file list.
      registration.installing.addEventListener('statechange', function(e) {
        if (e.target.state == 'installed') {
          resolve();
        } else if(e.target.state == 'redundant') {
          reject();
        }
      });
    } else {
      // Otherwise, if this isn't the "installing" service worker, then installation must have been
      // completed during a previous visit to this page, and the resources are already pre-fetched.
      // So we can show the list of files right away.
      resolve();
    }
  });
}

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('./service-worker.js', {scope: './'})
    .then(waitUntilInstalled)
    .then(showFilesList)
    .catch(function(error) {
      // Something went wrong during registration. The service-worker.js file
      // might be unavailable or contain a syntax error.
      document.querySelector('#status').textContent = error;
    });
} else {
  // The current browser doesn't support service workers.
}

/* service-worker.js */
// This polyfill provides Cache.add(), Cache.addAll(), and CacheStorage.match(),
// which are not implemented in Chrome 40.
importScripts('../serviceworker-cache-polyfill.js');

var CACHE_VERSION = 1;
var CURRENT_CACHES = {
  prefetch: 'prefetch-cache-v' + CACHE_VERSION
};

self.addEventListener('install', function(event) {
  var urlsToPrefetch = [
    './static/pre_fetched.txt',
    './static/pre_fetched.html',
    // This is an image that will be used in pre_fetched.html
    'https://www.chromium.org/_/rsrc/1302286216006/config/customLogo.gif'
  ];

  event.waitUntil(
    caches.open(CURRENT_CACHES['prefetch']).then(function(cache) {
      return cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
        return new Request(urlToPrefetch, {mode: 'no-cors'});
      })).then(function() {
        console.log('All resources have been fetched and cached.');
      });
    }).catch(function(error) {
      // This catch() will handle any exceptions from the caches.open()/cache.addAll() steps.
      console.error('Pre-fetching failed:', error);
    })
  );
});

self.addEventListener('activate', function(event) {
  // Delete all caches that aren't named in CURRENT_CACHES.
  // While there is only one cache in this example, the same logic will handle the case where
  // there are multiple versioned caches.
  var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) {
    return CURRENT_CACHES[key];
  });

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (expectedCacheNames.indexOf(cacheName) == -1) {
            // If this cache name isn't present in the array of "expected" cache names, then delete it.
            console.log('Deleting out of date cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // caches.match() will look for a cache entry in all of the caches available to the service worker.
    // It's an alternative to first opening a specific named cache and then matching on that.
    caches.match(event.request).then(function(response) {
      if (response) {
        console.log('Found response in cache:', response);

        return response;
      }

      // event.request will always have the proper mode set ('cors, 'no-cors', etc.) so we don't
      // have to hardcode 'no-cors' like we do when fetch()ing in the install handler.
      return fetch(event.request).then(function(response) {
        console.log('Response from network is:', response);

        return response;
      }).catch(function(error) {
        // This catch() will handle exceptions thrown from the fetch() operation.
        // Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
        // It will return a normal response object that has the appropriate error code set.
        console.error('Fetching failed:', error);

        throw error;
      });
    })
  );
});

Service Workers 真能取代 HTML5 AppCache?

和现有的 HTML5 数据缓存功能有很大的不同,service worker 的离线能力是可编程的。Russell 称它是一个:“让你做出选择去做哪些事的、可编程的、浏览器内置的代理”。由于 service worker 运行于后台,它和当前的 Web 页面完全独立,所以将来会有后台同步、推送通知等功能。Service Workers 让 Web 应用感觉更像是原生应用

Service worker has a number of advantages over AppCache: (1) Fine grained programmatic control; (2) Less fragile than AppCache; (3) Superior handling of updates to resources and the service worker. Get to know service worker and how to use it

HTML5 AppCache 示例:

<!-- clock.html -->
<!DOCTYPE HTML>
<html manifest="clock.appcache">
 <head>
  <title>Clock</title>
  <script src="clock.js"></script>
  <link rel="stylesheet" href="clock.css">
 </head>
 <body>
  <p>The time is: <output id="clock"></output></p>
 </body>
</html>

/* clock.css */
output { font: 2em sans-serif; }

/* clock.js */
setInterval(function () {
    document.getElementById('clock').value = new Date();
}, 1000);

"clock.appcache"
CACHE MANIFEST
clock.html
clock.css
clock.js

Service Workers 的功能都被实现了吗?

且看 Service Workers “之父” Jake Archibald 所写的 Is Service Worker Ready

Chromium Status

Most of the Service Worker spec features were done and started shipping from M40, including Service Worker basic interfaces, Cache and Fetch APIs. With these features SW can provide a programmable offline cache support for sites.

Push Notification support are shipped from M42.

Other SW based features under development includes Background Sync and Geofencing.

Upstream also started focusing on refactor, performance and stability. Like non-fetch SW optimization, migration to Mojo, fix cache crash.

Google also started to support SW on Chrome apps.

Intel Contributions

We used to adopt Chrome App runtime model for Crosswalk runtime model design. Later we started to follow and contribute to Service Worker after Kenneth and Anssi proposed App Lifecycle and Events spec to SysApps. The contribution continued although the above spec didn’t get much interest from vendors.

From July 2014 till now Xiang contributed 45 CLs to Chromium. The important features and enhancements contributed by Xiang includes: (1) skipWaiting() and claim() support to enable worker explicitly acquire page control; (2) .ready promise refator to support complicate registration scenarios; (3) new ServiceWorkerMessageEvent/ExtendableMessageEvent interfaces implementation; (4) worker process management optimization which reduced worker process overhead

Google also mentioned Intel Service Worker contribution on BlinkOn4 sharing.