通过 Service workers 让 PWA 离线工作

这篇翻译不完整。请帮忙从英语翻译这篇文章

我们已经看到了 js13kPWA 的结构,并且看到了基本的 shell 启动和运行,那么让我们看看如何使用 Service Worker 实现离线功能。 在本文中,我们将看看它是如何在 js13kPWA example 中使用的(另请参阅源代码)。 我们将研究如何添加脱机功能。

Service workers 解释

Service Workers是浏览器和网络之间的虚拟代理。 他们最终解决了前端开发人员多年来一直在努力解决的问题 - 最值得注意的是解决了如何正确缓存网站资源并使其在用户设备离线时可用。

它们的运行在一个与我们页面的 JavaScript 主线程独立的线程上,并且没有对 DOM 结构的任何访问权限。 这引入了与传统 Web 编程不同的方法 - API 是非阻塞的,并且可以在不同的上下文之间发送和接收信息。 您可分配给 Service Worker 一些任务,并在使用基于 Promise 的方法当任务完成时收到结果。

他们不仅仅提供离线功能,还提供包括处理通知,在单独的线程上执行繁重的计算等。Service workers 非常强大,因为他们可以控制网络请求,修改网络请求,返回缓存的自定义响应,或合成响应。

安全

因为它们非常强大,所以 Service Workers 只能在安全的上下文中执行(即 HTTPS )。 如果您想在将代码推送到生产环境之前先进行实验,则可以始终在本地主机上进行测试或设置 GitHub 页面 - 两者都支持HTTPS。

离线优先

“离线优先”或“缓存优先”模式是向用户提供内容的最流行策略。 如果资源已缓存且可脱机使用,请在尝试从服务器下载资源之前先将其返回。 如果它已经不在缓存中,请下载并缓存以备将来使用。

PWA 渐进增强

当作为渐进增强功能正确实施时,service workers 可以通过提供离线支持使支持 Service​Worker API 的现代浏览器的用户受益,但不会为使用旧版浏览器的用户破坏任何内容。

js13kPWA应用程序中的 Service workers

理论足够了,让我们看一些源代码!

注册 Service Worker

首先在 app.js 文件中查看注册新 Service Worker 的代码:

if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
};

如果浏览器支持 service worker API,则使用 ServiceWorkerContainer.register() 方法对该站点进行注册。 其内容在 sw.js 文件中,可以在注册成功后执行。 它是 app.js 文件中唯一的Service Worker代码; 其他所有特定于 Service Worker 的内容都写在 sw.js 文件中。

Service Worker 的声明周期

注册完成后,sw.js 文件会自动下载,然后安装,最后激活。

安装

API 允许我们为我们感兴趣的关键事件添加事件监听器 - 第一个是 install 事件:

self.addEventListener('install', function(e) {
    console.log('[Service Worker] Install');
});

In the install listener, we can initialize the cache and add files to it for offline use. Our js13kPWA app does exactly that.

First, a variable for storing the cache name is created, the app shell files are listed in one array.

var cacheName = 'js13kPWA-v1';
var appShellFiles = [
  '/pwa-examples/js13kpwa/',
  '/pwa-examples/js13kpwa/index.html',
  '/pwa-examples/js13kpwa/app.js',
  '/pwa-examples/js13kpwa/style.css',
  '/pwa-examples/js13kpwa/fonts/graduate.eot',
  '/pwa-examples/js13kpwa/fonts/graduate.ttf',
  '/pwa-examples/js13kpwa/fonts/graduate.woff',
  '/pwa-examples/js13kpwa/favicon.ico',
  '/pwa-examples/js13kpwa/img/js13kgames.png',
  '/pwa-examples/js13kpwa/img/bg.png',
  '/pwa-examples/js13kpwa/icons/icon-32.png',
  '/pwa-examples/js13kpwa/icons/icon-64.png',
  '/pwa-examples/js13kpwa/icons/icon-96.png',
  '/pwa-examples/js13kpwa/icons/icon-128.png',
  '/pwa-examples/js13kpwa/icons/icon-168.png',
  '/pwa-examples/js13kpwa/icons/icon-192.png',
  '/pwa-examples/js13kpwa/icons/icon-256.png',
  '/pwa-examples/js13kpwa/icons/icon-512.png'
];

Next, the links to images to be loaded along with the content from the data/games.js file are generated in the second array. After that, both arrays are merged using the Array.prototype.concat() function.

var gamesImages = [];
for(var i=0; i<games.length; i++) {
  gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);

Then we can manage the install event itself:

self.addEventListener('install', function(e) {
  console.log('[Service Worker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
          console.log('[Service Worker] Caching all: app shell and content');
      return cache.addAll(contentToCache);
    })
  );
});

There are two things that need an explanation here: what ExtendableEvent.waitUntil does, and what the caches object is.

The service worker does not install until the code inside waitUntil is executed. It returns a promise — this approach is needed because installing may take some time, so we have to wait for it to finish.

caches is a special CacheStorage object available in the scope of the given Service Worker to enable saving data — saving to web storage won't work, because web storage is synchronous. With Service Workers, we use the Cache API instead.

Here, we open a cache with a given name, then add all the files our app uses to the cache, so they are available next time it loads (identified by request URL).

Activation

There is also an activate event, which is used in the same way as install. This event is usually used to delete any files that are no longer necessary and clean up after the app in general. We don't need to do that in our app, so we'll skip it.

Responding to fetches

We also have a fetch event at our disposal, which fires every time an HTTP request is fired off from our app. This is very useful, as it allows us to intercept requests and respond to them with custom responses. Here is a simple usage example:

self.addEventListener('fetch', function(e) {
    console.log('[Service Worker] Fetched resource '+e.request.url);
});

The response can be anything we want: the requested file, its cached copy, or a piece of JavaScript code that will do something specific — the possibilities are endless.

In our example app, we serve content from the cache instead of the network as long as the resource is actually in the cache. We do this whether the app is online or offline. If the file is not in the cache, the app adds it there first before then serving it:

self.addEventListener('fetch', function(e) {
  e.respondWith(
    caches.match(e.request).then(function(r) {
          console.log('[Service Worker] Fetching resource: '+e.request.url);
      return r || fetch(e.request).then(function(response) {
                return caches.open(cacheName).then(function(cache) {
          console.log('[Service Worker] Caching new resource: '+e.request.url);
          cache.put(e.request, response.clone());
          return response;
        });
      });
    })
  );
});

Here, we respond to the fetch event with a function that tries to find the resource in the cache and return the response if it's there. If not, we use another fetch request to fetch it from the network, then store the response in the cache so it will be available there next time it is requested.

The FetchEvent.respondWith method takes over control — this is the part that functions as a proxy server between the app and the network. This allows us to respond to every single request with any response we want: prepared by the Service Worker, taken from cache, modified if needed.

That's it! Our app is caching its resources on install and serving them with fetch from the cache, so it works even if the user is offline. It also caches new content whenever it is added.

Updates

There is still one point to cover: how do you upgrade a Service Worker when a new version of the app containing new assets is available? The version number in the cache name is key to this:

var cacheName = 'js13kPWA-v1';

When this updates to v2, we can then add all of our files (including our new files) to a new cache:

contentToCache.push('/pwa-examples/js13kpwa/icons/icon-32.png');

// ...

self.addEventListener('install', function(e) {
  e.waitUntil(
    caches.open('js13kPWA-v2').then(function(cache) {
      return cache.addAll(contentToCache);
    })
  );
});

A new service worker is installed in the background, and the previous one (v1) works correctly up until there are no pages using it — the new Service Worker is then activated and takes over management of the page from the old one.

Clearing the cache

Remember the activate event we skipped? It can be used to clear out the old cache we don't need anymore:

self.addEventListener('activate', function(e) {
  e.waitUntil(
    caches.keys().then(function(keyList) {
          return Promise.all(keyList.map(function(key) {
        if(cacheName.indexOf(key) === -1) {
          return caches.delete(key);
        }
      }));
    })
  );
});

This ensures we have only the files we need in the cache, so we don't leave any garbage behind; the available cache space in the browser is limited, so it is a good idea to clean up after ourselves.

Other use cases

Serving files from cache is not the only feature Service Worker offers. If you have heavy calculations to do, you can offload them from the main thread and do them in the worker, and receive results as soon as they are available. Performance-wise, you can prefetch resources that are not needed right now, but might be in the near future, so the app will be faster when you actually need those resources.

Summary

In this article we took a simple look at how you can make your PWA work offline with service workers. Be sure to check out our further documentation if you want to learn more about the concepts behind the Service Worker API and how to use it in more detail.

Service Workers are also used when dealing with push notifications — this will be explained in a subsequent article.

文档标签和贡献者

此页面的贡献者: zjffun
最后编辑者: zjffun,