讨论TPAC潜在的新功能

90 阅读12分钟

上个月,我们在福冈举行的W3C TPAC会议上召开了一次服务工作者会议。这是几年来的第一次,我们集中讨论了潜在的新功能和行为。这里有一个总结。

复活终于杀青

reg.unregister();

如果你取消了一个服务工作者的注册,它就会从注册列表中删除,但它会继续控制现有页面。这意味着它不会破坏任何正在进行的获取等。一旦所有这些页面消失,该注册就会被垃圾回收。

然而,我们在规范中提到,如果一个页面以相同的范围调用serviceWorker.register() ,未注册的服务工作者的注册就会起死回生。我不确定我们为什么这样做。我想我们是担心页面会 "捣乱 "注册。总之,这是个愚蠢的想法,所以我们删除了它

// 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

在一个服务工作者中,要获得对你自己的ServiceWorker 实例的引用有点困难。self.registration 可以访问你的注册,但哪个服务工作者代表你目前正在执行的那个?self.registration.active?也许是。但也许是self.registration.waiting ,或者self.registration.installing ,或者都不是。

相反:

console.log(self.serviceWorker);

上面会给你一个对服务工作者的引用,无论它处于什么状态。

这个小功能在各浏览器之间达成了一致,被规范化了,并且在Chrome中正在积极开发。

页面生命周期和服务工作者

我是页面生命周期 API的忠实粉丝,因为它规范了浏览器多年来的各种行为,尤其是在移动端。例如,拆分页面以节省内存和电池。

另外,会话历史可以包含DOM文档,通常被称为 "前向页面缓存",或 "bfcache"。这在大多数浏览器中已经存在多年,但对Chrome来说是最近的事情。

这意味着页面可以被:

  • 冻结--可以通过一个可见的标签(无论是作为顶层页面,还是其中的iframe)访问该页面,而该标签目前没有被选中。事件循环是暂停的,所以页面不使用CPU。该页面完全在内存中,可以解冻而不丢失任何状态。如果用户关注这个标签,页面将被解冻。
  • Bfcached- 和冻结的一样,但是这个页面不能通过标签访问。它作为一个历史项目存在于一个浏览环境中。如果有一个会话导航到这个项目(例如使用后退/前进),这个页面将被解冻。
  • 丢弃--该页面可以通过一个可见的标签访问,而这个标签目前还没有被选中。然而,这个标签只是一个真正的占位符。该页面已经被完全卸载,不再使用内存。如果用户关注这个标签,页面将被重新加载。

我们需要弄清楚这些状态如何与特定的服务工作者行为相适应:

  • 一个新的服务工作者将保持等待,直到当前活动的服务工作者控制的所有页面都消失(可以使用skipWaiting() )。
  • clients.matchAll() 将返回代表页面的对象。

我们决定:

  • 冻结的页面将默认由clients.matchAll() 返回。Chrome希望为客户端对象添加一个isFrozen 属性,但苹果公司的人反对。对冻结的客户端的client.postMessage() 的调用将被缓冲,就像已经发生在BroadcastChannel
  • Bfcached和丢弃的页面将不会显示在clients.matchAll() 。将来,我们可能会提供一种选择的方式来获取被丢弃的客户端,这样他们就可以被关注(例如在回应一个通知的点击时)。
  • 冻结的页面将被计入阻止等待的工作者激活。
  • Bfcached和丢弃的页面不会阻止等待中的工作者激活。如果一个bfcached页面的控制器成为多余的(因为一个较新的服务工作者已经激活),该bfcached页面将被丢弃。该项目仍保留在会话历史中,但如果它被导航到,它将不得不完全重新加载。

我甚至把我所有的艺术技巧都拿出来测试了:

我相信这就把一切都弄清楚了。

现在我们只需要规范它。

将状态附加到客户端

在我们讨论页面生命周期的时候,Facebook的人提到他们如何使用postMessage 来询问客户端的状态,例如 "用户目前是否正在输入信息?"。我们还注意到,我们已经讨论过向客户端添加更多的状态(大小、密度、独立模式、全屏等等等等),但很难划定一个界限。

相反,我们讨论了允许开发者将可克隆的数据附加到客户端,这将显示在服务工作者的客户端对象上:

// 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

立即取消工作者注册

正如我之前提到的,如果你取消了一个服务工作者的注册,它就会从注册列表中删除,但它会继续控制现有的页面。这意味着它不会破坏任何正在进行的检索等。然而,在有些情况下,你希望服务工作者立即消失,而不考虑破坏事情。

这里的一位顾客是 Clear-Site-Data.它目前如上所述取消了服务工作者的注册,但Clear-Site-Data 是一个 "现在就摆脱一切 "的开关,所以目前的行为并不完全正确。

常规的取消注册将保持不变,但我将规范一种立即取消服务工作者注册的方法,这可能会终止正在运行的脚本并中止正在进行的检索。Clear-Site-Data ,但我们也可能将其作为API公开:

reg.unregister({ immediate: true });

来自LinkedIn的Asa Kusuma已经Clear-Site-Data 写了测试。我只需要做规范工作,不幸的是,这说起来容易做起来难。要使某件事情可被中止,就需要对整个规范进行梳理,并在每个点上定义中止的方式。

URL模式匹配

哦,这是一个大问题。我们在平台上到处使用URL匹配,特别是在服务工作者和内容安全政策中。然而,匹配真的很简单--精确匹配或前缀匹配。而开发者倾向于使用像path-to-regexp这样的东西。本-凯利建议我们在平台上引入类似于 "那个但不是那个 "的东西。

它需要比path-to-regexp更有限制性,因为我们希望能够在共享进程(例如,浏览器的网络进程)中处理这些。正则表达式真的很复杂,允许各种拒绝服务的攻击。浏览器供应商对开发者故意锁定自己的网站感到高兴,但我们不想让它有可能锁定整个浏览器。

这是本的建议。这很有野心,但如果我们能在整个平台上对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中暴露这一点真的很复杂,所以请求流的初始实现在请求完成之前不会产生响应。这还不算太糟--如果你想模拟双向通信,你可以用一个fetch来上传,另一个来下载。

响应后执行

这已经成为服务工作者的一个相当普遍的模式:

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

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

      return response;
    })(),
  );
});

然而,一些人发现,他们在waitUntil 中运行的一些JavaScript会延迟return response ,并使用setTimeout 的黑客来解决这个问题。

为了避免这种黑客行为,我们就event.handled ,这是一个承诺,一旦获取事件提供了一个响应,就会解决,或者推迟到浏览器上:

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

背景同步和背景获取的隐私问题

Firefox有一个后台同步的实现,但它因为隐私问题而受阻,这一点苹果公司的人也有同感。

当用户 "在线 "时,后台同步会给你一个服务工作者事件,这可能是直接的,但也可能是在未来的某个时候,在用户离开网站之后。由于用户已经作为顶级页面访问了该网站(例如,起源在URL栏中,与iframes不同),Chrome很高兴允许以后有一个小的、保守的执行窗口。脸谱网已经尝试用这种方式来发送分析报告,并确保聊天信息的传递,发现它比sendBeacon

Mozilla和苹果公司的人担心的是 "稍后 "是很晚的情况。这可能是飞行中的任何一方,IP的变化会暴露出更多的数据,而不是用户想要的。特别是,在Chrome浏览器中,用户不会被通知这些待定的行动。

Mozilla和苹果公司的人对后台获取模式更满意,它在获取的过程中显示一个持续的通知,并允许用户取消。

谷歌搜索已经使用后台同步来获取在线时的内容,但他们可以使用后台获取来实现类似的事情。

这次讨论并没有真正的结论,但感觉苹果可能会实现背景获取而不是背景同步。Mozilla可能也会这样做,或者让背景同步更容易被用户看到。

内容索引

Rayan Kanso介绍了内容索引的建议,它允许网站声明离线可用的内容,这样浏览器/操作系统就可以在其他地方显示这些信息,比如Chrome的新标签页。

有些人担心,网站可能会利用这一点来破坏这些东西出现的任何用户界面。但是,浏览器将可以自由地忽略/验证任何被告知的内容。

这个建议是相当新的。它是作为一个提示提交给小组的。

启动仪式

Raymes Khoury向我们介绍了关于启动事件的最新建议。这是一种让PWA控制多个窗口的方法。例如,当用户点击一个链接到你的网站,并且没有明确建议网站应该如何打开(例如 "在新窗口中打开"),如果开发者能够决定是聚焦网站使用的现有窗口,还是打开一个新窗口,那就更好了。这反映了今天本地应用程序的工作方式。

同样,这只是对正在进行的工作的一个提醒。

声明式路由

我介绍了开发者对我的声明式路由建议的反馈。浏览器对它感兴趣,但他们首先对一般的优化更感兴趣。这很公平。这是一个很大的API,有很多工作要做。在我们知道我们真的需要它之前,似乎最好先缓一缓。

服务工作者中的顶级等待(Top-level await

顶层等待现在在JavaScript中是一个东西!但是,服务工作者的顶层等待是必不可少的。然而,服务工作者尽可能快地启动是非常重要的,所以在服务工作者中使用顶层 await 可能是一种反模式。

我们有3个选择。

选项1:允许顶层await 。服务工作者的初始化将在主脚本完全执行时被阻止,包括awaited的东西。因为这很可能是一种反模式,所以我们提倡不要使用它,也许在devtools中显示警告。

你已经可以在服务工作者中做这样的事情了:

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

...并阻止初始执行5秒,那么await 有什么不同吗?也许吧,因为异步东西的性能不容易预测(比如网络),所以这个问题在开发过程中可能不明显。

选项2:禁止它。服务工作者会抛出顶层await ,所以它不会安装,而且控制台中会出现错误。

选项3:允许顶层await ,但是一旦初始执行+微任务完成,服务工作者就被认为准备好了。这意味着await,但在脚本 "完成 "之前可能会调用事件。按照目前的定义,在执行+微任务之后添加事件是不允许的。

我们决定,选项3太复杂了,选项1没有真正解决问题,所以我们要用选项2。因此,在一个服务工作者:

// 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');

取入/退出选择

Facebook的人已经注意到服务工作者对直接进入网络的请求产生了性能倒退。这是有道理的。如果一个请求通过了服务工作者,而结果只是做了浏览器无论如何都会做的事,那么服务工作者就只是开销。

Facebook的人一直在要求一种方法,对于特定的URL,可以说 "这不需要通过服务工作者"。

Kinuko Yasuda提出了一个建议,我们对此进行了讨论,并确定了一个有点像这样的设计:

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

  event.respondWith(…);
});

如果响应是针对客户端(页面或工作者)的,它将只咨询服务工作者对includes 列表(默认为所有路径)中前缀匹配的子资源请求,而不咨询服务工作者对excludes 列表中前缀匹配的请求。

我最初对这一建议并不确定,因为路由信息存在于页面中,而不是服务工作者,而且我通常更喜欢由服务工作者来负责。然而,其他的获取行为已经存在于页面中了,比如CSP,所以我不认为这是一个大问题。

API并不完全优雅,所以希望我们能解决这个问题,但Facebook已经提出在Chromium中进行实现工作,我们很乐意让它进入起源试验,这样他们就能看到它是否解决了现实世界的问题。如果它看起来是一个好处,我们可以考虑调整API。