W3C TPAC 大会上的 Service workers 内容总结

5
作者:Jake Archibald

翻译:疯狂的技术宅

原文:https://jakearchibald.com/201...

未经允许严禁转载

上个月,我们在福冈举行的 W3C TPAC 会议上召开了 service worker 会议。这是几年来我们第一次专注于潜在的新功能和行为。现总结如下:

复苏(Resurrection)最终被杀死

reg.unregister();

如果你取消 service worker 注册,则会将其从注册列表中删除,但它仍会继续控制现有页面。这意味着它不会中断任何正在进行的提取等操作。不过一旦所有这些页面都消失了,就会被垃圾回收。

但是在规范中有一个地方讲到:如果一个名为 serviceWorker.register() 的页面具有相同的作用域,则被注销的 service worker 注册将会“复苏”。我不知道为什么要这么做。无论如何,这都是一个愚蠢的主意,因此我们把它删除了

// Old behaviour:
const reg1 = await navigator.serviceWorker.getRegistration();
await reg1.unregister();
const reg2 = await navigator.serviceWorker.register('/sw.js', {
  scope: reg1.scope,
});
console.log(reg1 === reg2); // true!

好吧,如果 reg1 不控制任何页面,那可能是错误的。 是的,这令人困惑。

// New behaviour:
const reg1 = await navigator.serviceWorker.getRegistration();
await reg1.unregister();
const reg2 = await navigator.serviceWorker.register('/sw.js', {
  scope: reg1.scope,
});
console.log(reg1 === reg2); // Always false

现在,保证 reg2 是一个新的注册。复苏已被杀死。

我们在 2018 年就此达成了共识,并已在 Chrome 中实现,同时在 Firefox 和 Safari 中也已经实现。

self.serviceWorker

在 service worker 中,很难获得对自己的 ServiceWorker 实例的引用。用 self.registration 可以访问你的注册,但是究竟哪个 service worker 代表你当前正在执行的服务呢? self.registration.active?也许是吧,也或许是 self.registration.waitingself.registration.installing,或者都不是。

作为代替:

console.log(self.serviceWorker);

上面的内容将为你提供引用,无论其处于什么状态。

这项小功能已在所有浏览器中达成共识,在 Chrome 中正在积极开发。

页面生命周期和 service workers

我是 page lifecycle API 的忠实拥护者,因为它标准化了多年来浏览器已经完成的各种行为,特别是在手机上,例如,撤下页面以节省内存和电池。

此外,会话历史记录可以包含 DOM 文档,这通常称为“后-转发页面缓存”或“ bfcache”。大多数浏览器中已经存在了许多年,这是 Chrome 的最新版本

这意味着页面可以是:

  • 冻结 - 该页面可以通过可见选项卡(作为顶层页面或其中的 iframe)访问,该选项卡当前未选中。事件循环已暂停,因此该页面未使用 CPU。该页面已完全存储在内存中,并且可以被冻结而不会丢失任何状态。如果用户将焦点放在此选项卡上,则该页面将被解冻。
  • Bfcached - 与 冻结类似,但是无法通过标签访问此页面。它作为历史项存在于浏览上下文中。如果存在该项目的会话导航(例如使用后退/前进),则该页面将被冻结。
  • 废弃 - 可以通过当前未选择的可见标签访问该页面。但是,选项卡实际上只是一个占位符。该页面已完全卸载,不再使用内存。如果用户将焦点放在此选项卡上,则将重新加载页面。

我们需要弄清楚这些状态怎样适合特定的 service workers 行为:

  • 一个新的 service worker 将会一直等待,直到当前活动 service worker 控制的所有页面都消失了(可以用skipWaiting()跳过)。
  • clients.matchAll() 将返回代表页面的对象。

我们决定:

  • 默认情况下,冻结的页面将由 clients.matchAll() 返回。 Chrome 希望向客户端对象添加 isFrozen 属性,但是 Apple 的同行反对。对冻结的客户端的 client.postMessage() 调用将被缓冲,就像 BroadcastChannel 一样。
  • Bfcached 和丢弃的页面不会显示在 clients.matchAll() 中。将来我们可能会提供一种选择加入的方式来获取被废弃的客户端,以便他们可以获得焦点(例如,响应通知点击)。
  • 冻结的页面将有助于防止等待的 worker 被激活。
  • Bfcached 和废弃的页面不会阻止等待中的工作程序被激活。如果 bfcached 页面的控制器变得多余(因为已激活了新的 service worker),则该 bfcached 页面将被删除。该项目保留在会话历史记录中,但如果导航到该项目,则必须完全重新加载。

我甚至对所有的情况进行了测试:

img

现在我们只需要指定它。

将状态附加到客户端

当我们讨论页面生命周期的内容时,Facebook 的同事提到了他们如何用 postMessage 向客户询问其状态,例如“用户当前是否在键入消息?”。我们还注意到,我们已经讨论过向客户端添加更多状态(大小、密度、独立模式、全屏等),但是很难划清界线。

相反,我们讨论了允许开发人员将可克隆的数据附加到客户端,这些数据将显示在 service worker 的客户端对象上。

// From a page (or other client):
await clients.setClientData({ foo: 'bar' });
// In a service worker:
const allClients = await clients.matchAll();
console.log(allClients[0].data); // { foo: 'bar' } or undefined.

现在还处于早期,但感觉是这样可以避免在 postMessage 上来回移动。

立即注销 worker

如前所述,如果你注销 service workers 注册,则会从注册列表中将其删除,但是它将慧继续控制现有页面。这意味着它不会中断正在进行的提取等操作。但是在某些情况下,无论中断什么事情,你都希望 service workers 立即离开。

这里的一个客户端是 Clear-Site-Data。如上所述,它正在注销 service workers,但是 Clear-Site-Data 是“立即摆脱一切”开关,因此当前行为不太正确。

常规注销将保持不变,但是我将指定一种方法来立即注销 service worker,这可能会终止正在运行的脚本并中止正在进行的提取。 Clear-Site-Data 将使用此方法,但我们也可以将其公开为 API:

reg.unregister({ immediate: true });

来自 LinkedIn 的 Asa Kusuma 已编写了 Clear-Site-Data 测试。我只需要进行规范工作就可以了,不幸的是说起来容易做起来难。

URL 模式匹配

这是一个很大的问题。我们在整个平台上都使用 URL 匹配,尤其是在 service worker 和 Content-Security-Policy 中。但是匹配非常简单——完全匹配或前缀匹配。开发人员倾向于使用 path-to-regexp 之类的东西。 Ben Kelly 提议我们将类似的东西带到平台上。

它需要比正则表达式路径更具限制性,因为我们希望能够在共享进程(例如:浏览器的网络进程)中处理这些问题。 RegExp 确实很复杂,并且可以进行各种拒绝服务攻击。浏览器供应商对开发人员有意锁定自己的网站感到满意,但我们并不想锁定整个浏览器。

这里是 Ben 的提案。这非常雄心勃勃,但是如果我们可以跨平台使用更富有表现力的 URL 那就太好了。这样的例子非常引人注目:

// Service worker controls `/foo` and `/foo/*`,
// but does not control `/foobar`.
navigator.serviceWorker.register(scriptURL, {
  scope: new URLPattern({
    baseUrl: self.location,
    path: '/foo/?*',
  }),
});

把流作为请求体

多年来,你可以流式传输响应:

const response = await fetch('/whatever');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(value); // Uint8Array of bytes
}

规范还说,你可以将流用作请求的主体,但没有浏览器实现。但是,Chrome 已决定再次使用它,而 Firefox 和 Safari 表示也会这样做。

let intervalId;
const stream = new ReadableStream({
  start(controller) {
    intervalId = setInterval(() => {
      controller.enqueue('Hello!');
    }, 1000);
  },
  cancel() {
    clearInterval(intervalId);
  },
}).pipeThrough(new TextEncoderStream());

fetch('/whatever', {
  method: 'POST',
  body: stream,
  headers: { 'Content-Type': 'text/plain; charset=UTF-8' },
});

上面的代码将“hello”作为单个 HTTP 请求的一部分,每秒钟一次发送到服务器。这个例子是愚蠢的,但是它展示了一项新功能——在你获得整个请求体内容之前将数据发送到服务器。当前,你只能分块或使用 websocket 来执行此操作。

一个实际的例子是涉及上传流式传输的内容。例如你可以在编码或录制的时候上传视频。

HTTP 是双向的。该模型不是先请求后响应——你可以在仍然发送请求正文的同时开始接收响应。但是,在 TPAC 大会中,浏览器开发人员注意到,鉴于当前的网络栈,在获取过程中公开这个内容确实很复杂,因此请求流的最初实现在请求完成之前不会产生响应。这还算不错——如果你想模拟双向通信,则可以使用一次 fetch 进行上传,而用另一次进行下载。

响应后执行

这已成为 service workers 中非常普遍的模式:

addEventListener('fetch', event => {
  event.respondWith(async function() {
    const response = await getResponseSomehow();

    event.waitUntil(async function() {
      await doSomeBookkeepingOrCaching();
    });

    return response;
  }());
});

但是,有些人发现他们在 waitUntil 中运行的某些 JavaScript 延迟了 return response,并用了 setTimeout hack 来解决。

为了避免这种 hack 操作,我们同意使用 event.handled,这是一个 promise,一旦 fetch 事件提供了响应或将其推迟给浏览器后便会解决。

addEventListener('fetch', event => {
  event.respondWith(async function() {
    const response = await getResponseSomehow();

    event.waitUntil(async function() {
      // And here's the new bit:
      await event.handled;
      await doSomeBookkeepingOrCaching();
    });

    return response;
  }());
});

后台同步(background sync)和后台获取(background fetch)的隐私问题

Firefox 有后台同步的实现,但由于隐私问题而被阻止,这是由 Apple 员工共享的。

当用户处于“在线”状态时,后台同步会为你提供 service worker 事件,该事件可能会立即消失,也可能会在用户离开站点后的某个时间出现。由于用户已经作为顶级页面访问了该网站(例如原始位置在URL栏中,而不是 iframe),因此 Chrome 很高兴在以后允许一个小的,保守的执行窗口。 Facebook 已经尝试过这种方法来发送分析数据并确保聊天消息的传递,而且发现了它的性能比 sendBeacon 之类的方法更好。

Mozilla 和 Apple 员工对后台获取模型更加满意,该模型在获取期间会持续显示通知,并允许用户取消。

Google搜索已使用后台同步来在线获取内容,但是他们可以用后台获取来达到类似的目的。

这次讨论并没有真正得出结论,但我感觉苹果公司可能实现了后台获取而不是后台同步。 Mozilla 也可能会做同样的事情,或者使后台同步变得更加用户可见。

内容索引

Rayan Kanso 提出了内容索引提案,它允许网站可以声明能够脱机使用的内容,因此浏览器或 OS 可以在其他位置(例如 Chrome 中的新标签页)显示此信息。

有人担心,无论这些东西出现在什么UI上,网站都可以使用它来发送垃圾邮件。但是,浏览器可以自由地忽略或验证所告知的任何内容。

这是个非常新的提案,它已作提交给小组。

启动事件

Raymes Khoury 向我们提供了有关启动事件提案的最新信息。这是 PWA 控制多个窗口的一种方式。例如,当用户单击指向你网站的链接,但是没有明确建议网站应如何打开(例如“在新窗口中打开”)时,如果开发人员可以决定是将焦点集中在网站使用的现有窗口上还是打开新窗口,那将是很好的选择。这反映了当今原生应用的工作方式。
同样,这项工作正在进行中。

声明式路由

我向开发人员提供了有关声明式路由建议的反馈。尽管对浏览器比较重要,但常规优化更加重要。很公平!这是一个规模很大的 API,需要做大量的工作。在确定我们确实需要它之前,最好先推迟一下。

service worker 的 Top-level await

Top-level await 现在是 JavaScript 中的东西!但是 service worker 的启动速度非常重要,所以在 service worker 中使用 Top-level await 可能会成为反模式。

我们有3个选择:

选择1:允许 top level await。service workers 初始化将在完全执行的主脚本上被阻止,包括 await 的事情。因为它可能是反模式,所以我们建议不要使用它,并在 devtools 中显示警告。

你可以在 service worker 中执行以下操作:

const start = Date.now();
while (Date.now() - start < 5000);

…并在初始 5 秒钟阻止执行,所以 await 有什么不同吗?嗯,也许吧,因为异步内容可能有不可预测的性能问题(例如网络),所以问题在开发过程中可能并不明显。

选择2:禁止。service workers 将在顶层使用 await,因此它将无法被安装,并且将在控制台中出现错误。

选择3:允许顶层的 await,但是一旦完成初始执行 + 微任务,则认为 service worker 已经准备就绪。这意味着 await 将继续运行,但是可以在脚本“完成”之前调用事件。根据当前定义,不允许在执行 + 微任务之后添加事件。

我们认为选择 3 太复杂,选择 1 并没有真正解决问题,因此选择 2 是合适的。在 service worker 中:

// If ./bar or any of its static imports use a top-level await,
// this will be treated as an error
// and stops the service worker from installing.
import foo from './bar';

// This top-level await causes an error
// and stops the service worker from installing.
await foo();

// This is fine.
// Also, dynamically imported modules and their
// static imports may use top-level await,
// since they aren't blocking service worker start-up.
const modulePromise = import('./utils');

获取 opt-in / opt-out

Facebook 员工注意到 service workers 对直接发送到网络的请求进行了性能回归,那讲得通。如果一个请求通过了 service worker,而结果是要做浏览器无论如何要做的事情,那么 service worker 就是开销。

Facebook 一直在寻求一种方法,针对特定的 URL 说“这不需要通过 service workers 进行”。

Kinuko Yasuda 提出了提案,我们讨论了一下,并决定了一个有点像这样的设计:

addEventListener('fetch', event => {
  event.setSubresourceRoutes({
    includes: paths,
    excludes: otherPaths,
  });

  event.respondWith(…);
});

如果响应是针对客户端(页面或 worker)的,则它将仅询问 service worker 以获取子资源请求,这些子资源请求的前缀与 includes 列表中的路径匹配(默认为所有路径),而不会查询 service worker,要求在 excludes 列表中使用前缀匹配路径的请求。

最初我不确定这个建议,因为路由信息是与页面而不是 service workers 一起工作,而我通常更喜欢由 service workers 负责。但是其他获取行为已经存在于页面中,例如 CSP,所以我认为这没什么大不了的。

这个 API 并不是很优雅,所以我们希望能搞清楚,但是 Facebook 提供了能够在 Chromium 中工作的实现,我们很高兴它可以进入来源试用,这样他们就可以查看它是否解决了实际问题。如果看起来有好处,我们可以考虑调整 API。

就这样!

还有一些因为时间问题没有讨论的其他事情,所以我们可能会在 2020 年中期举行另一场面对面的会议。同时,如果你对上述内容有什么反馈,请在在 GitHub 上告诉我。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


你可能感兴趣的

载入中...