diff --git a/content/tutorials/service-worker/introduction/zh/index.html b/content/tutorials/service-worker/introduction/zh/index.html new file mode 100644 index 000000000..9e5eca2a1 --- /dev/null +++ b/content/tutorials/service-worker/introduction/zh/index.html @@ -0,0 +1,356 @@ +{% extends "tutorial.html" %} +{% load mixin from templatefilters %} + +{% block pagebreadcrumb %}{{ tut.title }}{% endblock %} + +{% block head %}{% endblock %} + +{% block translator %} +
+ 翻译: Nianpeng Li +
+{% endblock %} + +{% block content %} + +

丰富的离线用户体验、周期性的后台同步、消息推送—这些原本只存在于原生应用(native application)上的功能,即将登陆Web应用。正是 Service workers 为这些功能在Web应用上的实现提供了技术基础。 + +

什么是Service Worker?

+ +

Service worker 是一个浏览器在后台运行的且独立于当前页面的脚本。它允许浏览器在没有页面指令和用户交互的情况下依然能够实现一些功能。现在 service worker 已经能够实现消息推送,未来其功能将包括后台更新、地理围栏(geofencing)等。本教程讲主要讨论 service worker 的拦截和处理网络请求能力,包括如何通过程序管理响应缓存。

+ +

之所以说这是个令人兴奋的API是因为它对离线体验的支持。同时,它赋予开发者对离线功能的所有环节的完全掌控。

+ +

在 service worker 出现以前,另一个提供离线用户体验支持的编程接口是 App Cache。但使用中 App Cache 常常带来很多棘手的问题,同时其设计初衷本来只是为了满足单页应用(single page web apps)而不是多页网站。Service worker 的出现正是为了解决这些痛点和不便。

+ +

关于 Service Worker 几点需要注意的地方:

+ + + +

Service Worker 的生命周期

+ +

每个 service worker 都拥有一个完全独立于页面的生命周期。

+ +

安装 service worker 的方法是使用 JavaScirpt 在页面上对其进行注册。注册后浏览器将在后台启动 service worker 的安装步骤。

+ +

通常你会在 service worker 的安装阶段缓存一些静态资源。只有当所有资源都下载且缓存完毕时,service worker才提示安装成功。否则,它将无法启动(或者说,安装失败)。在安装失败的情况下,它会稍后重新尝试。所以你可以放心的认为,安装成功时所有需要的静态资源都已经就绪。

+ +

Server worker 在安装完成后会进入启动程序。在这一阶段,你可以处理之前的遗留缓存。有关这一点,我们将在后面的 service worker 的更新 部分具体讨论.

+ +

启动之后,service worker 将会接管所有其管辖范围内的页面。这里有一个例外,service worker 的注册页面不会在启动时直接被接管,之后再次访问该页面时 service worker 将正常生效。Service worker 启动后处于这两种状态之一:要么关闭以节省内存资源, 要么在网页有网络请求或信息传送时处理 fetchmessage

事件。 + +

下图简要概括了 service worker 的初次安装及其后的生命周期。

+ + + +

准备工作

+ +

下载 https://github.com/coonsta/cache-polyfill 提供的浏览器缓存兼容代码(cache polyfill)。 + +

因为 Google Chrome M43 的 Cache +API 目前还不支持 Cache.addAll, 所以我们需要上述的兼容代码。

+ +

dist/serviceworker-cache-polyfill.js 加入你的网站,然后在你的 service worker 程序中通过 importScripts 方法加载该脚本。所有被加载的脚本都会被 service worker 自动缓存以备再次使用。

+ +
importScripts('serviceworker-cache-polyfill.js');
+ +

必须使用 HTTPS

+ +

在开发环境下,你可以在 localhost 上测试 service worker。但如果需要上线,你的网站必须使用 HTTPS 协议。

+ +

你可以通过 service worker 来拦截网络连接,伪造和过滤服务器响应。是不是非常给力?但它强大的功能也是一把双刃剑,在提供功能帮助的同时它也给中间人攻击(man-in-the-middle)提供了可能。为了避免类似的网络攻击,你只能在支持 HTTPS 协议的页面上注册和使用 service worker,只有这样我们才能确保页面加载的 service worker 没有在传输过程中被篡改。

+ +

GitHub Pages 由于对 HTTPS 的支持,非常适合用来做 service worker 的测试 。

+ +

要想让你的服务器支持 HTTPS, 你需要一个 TLS 证书并将其安装至你的服务器。 具体方法取决于你的服务器构架,所以建议你仔细阅读你的服务器文档,而且不要忘记通过 Mozilla's SSL config generator 了解最佳实践。

+ +

Service worker 的使用

+ +

假设我们已经加载了 polyfill 并且使用 HTTPS 协议,接下来让我们看一下具体怎么使用 service worker。

+ +

如何注册和安装 Service Worker

+ +

在页面上注册 service worker 会开启 service worker 的安装。这一步浏览器需要知道 service worker 的 JavaScript 文件地址。

+ +
if ('serviceWorker' in navigator) {
+  navigator.serviceWorker.register('/sw.js').then(function(registration) {
+    // 注册成功
+    console.log('ServiceWorker registration successful with scope: ', registration.scope);
+  }).catch(function(err) {
+    // 注册失败 :(
+    console.log('ServiceWorker registration failed: ', err);
+  });
+}
+ +

上面的代码检查 service worker API 是否可用,如果可用,service worker 代码 /sw.js 将会被。

+ +

你可以放心地在每次页面加载时执行注册代码。浏览器会判断该 service worker 是否已经被注册,不会出现重复注册的问题。

+ +

上面代码中有一个需要注意的地方--register 使用的 service worker 脚本地址。你可能已经注意到示例中的 service woker 文件位于域名的根目录下。这表示该 service worker 的作用范围为所在站点的整个域。或者说,该 service worker 将可以接收到所在网站整个域发出的 fetch 事件。假设 service worker 的注册地址改为 /example/sw.js, 那么它就只能收到以 /example/ 起始的 URL 页面上的 fetch 事件 (比如:/example/page1/, /example/page2/)。

+ +

如果你想知道一个 service worker 是否在正常运行,你可以访问 chrome://inspect/#service-workers 然后在列表中寻找你的网站。

+ + + +

在最早的 service worker 实现版本中,你可以通过 chrome://serviceworker-internals 来得到更多的信息。现在这个方法很多时候也还非常有效,通过它你能很方便的了解 service wokers 的生命周期。但它很有可能在不久的将来被 chrome://inspect/#service-workers 取代。

+ +

浏览器的匿名模式(Incognito)为 service worker 的测试提供了非常好的环境,因为每次打开和关闭匿名窗口你都会得到一个全新的 service worker 环境。匿名窗口在关闭时会把 service worker 的注册信息以及使用中产生的缓存彻底清除。

+ +

Service Worker 的安装步骤

+ +

在页面开始了注册步骤之后,让我们把视角转到 service worker 脚本上来。因为这里你将有机会处理安装(install)事件。

+ +

一种最基本的情况是你需要为安装(install)事件定义一个 callback 来决定哪些资源要被缓存。

+ +
// 需要缓存的资源
+var urlsToCache = [
+  '/',
+  '/styles/main.css',
+  '/script/main.js'
+];
+
+// 定义安装事件的 callback
+self.addEventListener('install', function(event) {
+  // 执行安装步骤
+});
+ +

在安装(install)callback 函数中我们使用如下步骤:

+ +
    +
  1. 启用一个缓存
  2. +
  3. 将资源存入缓存
  4. +
  5. 确认是否所有必要资源都已加入缓存
  6. +
+ +
var CACHE_NAME = 'my-site-cache-v1';
+var urlsToCache = [
+  '/',
+  '/styles/main.css',
+  '/script/main.js'
+];
+
+self.addEventListener('install', function(event) {
+  // 执行安装步骤
+  event.waitUntil(
+    caches.open(CACHE_NAME)
+      .then(function(cache) {
+        console.log('Opened cache');
+        return cache.addAll(urlsToCache);
+      })
+  );
+});
+ +

如上面的代码所示,我们通过 caches.open 打开指定的缓存文件,然后再调用 cache.addAll 并传入我们的文件名数组。这里有一连串的 promise(caches.opencache.addAll)。 event.waitUntil 使用一个 promise 作为参数,并通过它得到安装步骤运行的时间以及安装是否完成。

+ +

如果所有指定文件都缓存成功,service worker 就算顺利完成安装。任一文件下载失败都会导致安装失败。这一方面确保了你可以得到所有指定的资源,但同时也要求你在定义你的缓存文件列表时非常谨慎。缓存文件列表越长,其中有个别文件缓存失败的几率就越大,也就是说你的 service worker 有更大的可能安装失败。

+ +

上面只是安装(install)的一个例子。你还可以在安装(install)时执行其他任务。 当然,你也可以选择直接忽略安装(install)事件。

+ +

如何缓存和返回请求(Requests)

+ +

既然已经安装好了 service worker,那为什么不用它返回一个已缓存的响应(response)呢?

+ +

假设现在 service worker 已经安装完成,当用户开始浏览另一个页面或者刷新当前页面时, service worker 就可以监听并接收到 fetch 事件,如下所示:

+ +
self.addEventListener('fetch', function(event) {
+  event.respondWith(
+    caches.match(event.request)
+      .then(function(response) {
+        // 缓存命中
+        if (response) {
+          return response;
+        }
+
+        return fetch(event.request);
+      })
+  );
+});
+ +

这里我们定义了 fetch 函数,并通过 caches.match 把一个 promise 传入 event.respondWithcaches.match 会检视请求(request),从 service worker 之前生成的缓存中找到相应的结果。

+ +

如果缓存中有相符的响应,我们直接返回缓存值。否则会返回 fetch 的结果,也即发送网络请求并返回响应数据--假如有响应的话。这个简单的例子可以应用于任何我们在安装(install)阶段缓存的资源。

+ +

如果你想积累式地(cumulatively)缓存新请求,你可以参考下面这个例子来处理 fetch 请求的响应并将其加入缓存:

+ +
self.addEventListener('fetch', function(event) {
+  event.respondWith(
+    caches.match(event.request)
+      .then(function(response) {
+        // 缓存命中 - 返回响应
+        if (response) {
+          return response;
+        }
+
+        // 要点:克隆请求(request)。请求是数据流(stream)而且只能
+        // 被一次性使用。由于我们要在检查缓存时和 fetch
+        // 时共使用两次,所以克隆请求是不可少的。
+        var fetchRequest = event.request.clone();
+
+        return fetch(fetchRequest)
+          .then(function(response) {
+            // 检查接收到的响应是否有效
+            if (!response || response.status !== 200 || response.type !== 'basic') {
+              return response;
+            }
+
+            // 要点:克隆响应(response)。响应也是数据流(stream),
+            // 浏览器要使用一次,缓存要使用一次。所以通过克隆响应来
+            // 得到两个流。
+            var responseToCache = response.clone();
+
+            caches.open(CACHE_NAME)
+              .then(function(cache) {
+                cache.put(event.request, responseToCache);
+              });
+
+            return response;
+          });
+      })
+  );
+});
+ +

我们来解释一下上面的代码:

+ +
    +
  1. fetch 请求加一个回调函数 .then
  2. +
  3. 得到返回值后执行一下步骤:
  4. +
      +
    1. 检查确保响应数据存在。
    2. +
    3. 检查响应状态代码(status code)为 200
    4. +
    5. 检查返回结果类型为 basic 以保证请求是由当前网站域发送。这样的话我们就不会缓存其他第三方资源。
    6. +
    +
  5. 当所有检查都通过时,我们 clone 返回结果。因为响应值是个数据流,所以它只能被使用一次。浏览器需要得到该响应的同时我们还希望将它缓存起来,因此这一步的克隆非常关键。
  6. +
+ +

如何更新 Service Worker

+ +

或早或迟你总会需要更新你的 service worker,当这个时刻到来时,请参照下列步骤:

+ +
    +
  1. 更新 service worker 的 JavaScript 脚本文件。
  2. +
      +
    1. 当用户访问你的网站时,浏览器会试图在后台重新下载 service worker 的脚本文件。如果浏览器比较后发现新文件与现存文件有任何差别,它都会开始准备更新。
    2. +
    +
  3. 浏览器将启动新 service worker 并触发 install 事件。
  4. +
  5. 与此同时,整个页面还是处于旧的 service worker 的控制下,新 service worker 会进入待机(waiting)状态。
  6. +
  7. 当前页面被关闭时,旧 service worker 将被终止,新 service worker 获得控制权。
  8. +
  9. 新 service worker 获得控制权的同时,它的激活(activate)事件将被触发。
  10. +
+ +

在激活(activate)事件的回调函数中进行缓存管理是种常见的做法。之所以选择激活事件的回调函数是因为:假设你在安装(install)步骤清除旧缓存或者旧 service worker,它们还保持着对当前页面的控制,清除操作会导致缓存文件无法读取而产生错误。

+ +

举例来说,如果我们希望把旧的缓存文件 'my-site-cache-v1' 分成两个缓存,一个给常规页面,一个给博客文章。这意味着在安装(install)阶段我们需要创建两个缓存,'pages-cache-v1' 和 'blog-posts-cache-v1', 这样在激活(active)步骤我们就可以安全地清除 'my-site-cache-v1' 并开始使用新缓存。

+ +

下面的代码将会循环检索所有 service worker 使用的缓存,并删除其中那些不在白名单(white list)上的缓存。

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

疑难问题与常见陷阱

+ +

Service worker 目前还很新且不成熟。下面列出了一些使用过程中的常见问题。希望将来我们不再需要这一部分,但是目前了解一下还是很有帮助的。

+ +

你没有什么可靠的办法知道 service worker 安装失败了。

+ +

如果一个 worker 进行了注册(register),却没有出现在chrome://inspect/#service-workerschrome://serviceworker-internals 中,那最大的可能是由于某个抛出错误或 rejected promise 被传给了 event.waitUntil 并导致了安装(install)失败 。

+ +

一个变通的解决方案是访问 chrome://serviceworker-internals,勾选"Opens the DevTools window for service worker on start for debugging"并在安装(install)程序开始处设置一个 debugger。再加上在 DevTool 的 Pause on uncaught exceptions 应该可以帮助你发现问题的原因。

+ +

fetch() 的默认设置

+

无预设用户名密码

+ +

当你使用 fetch() 时,默认请求不带有任何包括 cookies 在内的用户凭证(credentials)。如果你需要使用用户凭证(credentials),那函数调用方式应为:

+ +
fetch(url, {
+  credentials: 'include'
+})
+ +

这一规则是设计者有意为之,相比与 XHR 那种更复杂的"在发送 URL 时同域(same-origin)则带用户凭证,异域则不带"的规则,这貌似是个更好的选择。Fetch 的行为模式更像是跨域资源共享(CORS),比如 <img crossorigin>,除非你通过 <img crossorigin="use-credentials"> 选择性使用,否则它并不会自动发送 cookies。

+ +

非跨域资源共享(Non-CORS)请求会默认失败

+ +

用来获取第三方资源的 URL 如果不支持跨域资源共享(CORS),那 fetch 函数将默认失败。如果你一定需要这样做,你可以给 Request 添加 non-CORS 选项。但这样你会得到一个 'opaque' response,也就是说你无法判断响应成功与否。

+ +
cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
+  return new Request(urlToPrefetch, { mode: 'no-cors' });
+})).then(function() {
+  console.log('所有资源都已下载并缓存。');
+});
+ +

处理响应式图片(Responsive Images)

+ +

图片的srcset 属性或者 <picture> 元素会在运行时选择最合适的图片资源并发送网络请求。 + +

如果你想用 service worker 在安装阶段缓存图片,你有如下几个选择:

+ +
    +
  1. 安装srcset 属性以及 <picture> 元素可能选择的所有图片
  2. +
  3. 仅安装图片的一个低像素版本
  4. +
  5. 仅安装图片的一个高像素版本
  6. +
+ +

一般来说选项2和选项3更加实际,因为下载所有图片会浪费大量内存。

+ +

让我们来设想你选择在安装时仅缓存低像素版本,然后在图片实际加载时首先尝试下载高分辨率版。当高分辨率版下载失败时,使用已缓存的低像素版作为后备。这是一个可行方案,但是如下所示,这里有一个需要注意的问题。

+ +

假设页面上有两张图片:

+ + + + + + + + + + + + + + + + + +
屏幕密度宽度高度
1x400400
2x800800
+ +

使用 srcset 的图片一般会用类似这样的标记(markup):

+ +
<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />
+ +

当我们使用 2x 显示的时候,浏览器选择下载 image-2x.png。如果这是在离线的情况下进行, service worker 会 .catch 请求并返回缓存的 image-src.png。但浏览器期待的返回图片拥有更多像素来填充 2x 的屏幕,结果是图片被显示在 200x200 的 CSS 像素下,而非我们希望的 400x400 像素。唯一的解决方案是给图片设定一个固定(fixed)宽度和高度。

+ +
<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x"
+  style="width:400px; height: 400px;" />
+ + + +

这个问题在使用<picture> 元素作为艺术导向(art direction)时会变得更加棘手,而且很大程度上取决于你的图片的创建和使用方式。但是也许你可以使用类似 srcset 的方法。

+ +

更多资料

+ +

这里能够找到一系列关于 service worker 的文档 https://jakearchibald.github.io/isserviceworkerready/resources.html

+ +

求助

+ +

如果你在使用 service worker 的过程中遇到问题,请将问题发布在 Stackoverflow,并使用'service-worker' 标签来保证问题更可能被检索到,并得到大家的帮助。

+ +{% endblock %}