上个月,我们在福冈举行的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。