背景
根据 Electron 官方的更新公告:Electron 25.0.0 | Electron (electronjs.org)
以下几个 API 方法已被标记为弃用
registerFileProtocolregisterBufferProtocolregisterStringProtocolregisterHttpProtocolregisterStreamProtocolunregisterProtocolisProtocolRegisteredinterceptFileProtocolinterceptStringProtocolinterceptBufferProtocolinterceptHttpProtocolinterceptStreamProtocol
主要原因是官方对 protocol 系列 API 进行了"简化"(所谓的 simplification),认为旧的 API 理解成本太高,因此推荐大家使用 protocol.handle 替代上述 API。
关于 protocol.handle 的讨论,具体可以看这个链接内的描述。当然链接里面的描述只是当时的一个畅想,在实现时又是另一幅样子了,但大差不差。
当前的具体接口定义可以查阅官方文档。
显而易见,旧的 register 系列 API 和新出的 protocol.handle 很不一样,下面是一个例子:
javascript
// 在 Electron 25 中弃用
protocol.registerHttpProtocol('some-protocol', (_request, callback) => {
callback({ url: 'https://electronjs.org' });
});
// 用以代替
protocol.handle('some-protocol', (_request) => {
return net.fetch('https://electronjs.org');
});
乍一看似乎 API 的替换工作很好完成,但实际业务中不可能有这么简单而天真的代码。没有人知道有多少功能依赖于一些旧 API 上花里胡哨的用法,且这些功能是否能被新的 API 覆盖也是未知数。
下面是一些截止目前我在替换这些废弃 API 过程中遇到的问题以及解决方式记录。
API 替换问题
返回值问题
以 registerHttpProtocol 为例,其可以写出如下代码:
javascript
protocol.registerHttpProtocol('customProtocol', (request, callback) => {
if (needHandle(request.url)) {
// 一些业务逻辑
doSomething(request);
}
});
在这个例子中,要改造为使用新的 protocol.handle API 的话,需要注意------
旧实现中没有调用 callback 回调函数,为请求返回一个响应。
也就是说,这里的旧实现逻辑中,这个请求将触发一些功能代码,且永远不能收到响应。
我再们来看看,新的 API 的定义:
typescript
handle(
scheme: string,
handler: (request: Request) => Response | Promise<Response>
): void;
可以发现,这里的 handler 参数要求必须 返回一个 Response 对象,或一个可以解决为 Response 对象的 Promise。
而如果我们直接返回一个任意的 Response 对象,就会使得请求接收到响应,不符合旧实现的逻辑。
所以这里的解决方式应该是,在执行完对应的业务逻辑功能以后,返回一个永远不会解决的 Promise,使得对应的响应无法收到对应的请求:
javascript
protocol.handle('customProtocol', (request) => {
if (needHandle(request.url)) {
// 一些业务逻辑
doSomething(request);
}
// 返回一个永远不会解决的 Promise 对象
return new Promise(() => {});
});
error 的处理
以 registerHttpProtocol 为例,旧 API 可以写出如下代码:
javascript
protocol.registerHttpProtocol('customProtocol', (request, callback) => {
if (/* 命中某些条件 */) {
// 通过返回带 error 字段的对象,使请求失败
// 并通过 error 的值指定失败的理由
callback({error: -3});
}
});
旧实现中可以通过 callback 返回一个带 error 字段的对象,使请求失败。并通过 error 的值指定失败的理由。在上例中,error 指定为 -3 时,请求将被终止,在 chrome 的 devtools 中检查对应的请求,其将会像这样显示:

官网文档对 error 字段的说明如下:
errorInteger(可选的) - 如果赋值,request将会失败,并返回error错误码。 更多的错误号信息,您可以查阅网络错误列表.
但新版本 API 中(即 protocol.handle 中),返回的 Response 对象似乎并没有指定请求失败理由的功能。官方文档中也未提及如何替代旧 API 提供的这一行为。
于是去翻了 Electron 添加该功能的 PR:
在这个 commit 中,protocol.handle 被添加进来,观察其对于错误的处理:

可以看出,只要返回的对象中存在 error 字段,应该可以做到和旧 API 一样的效果,再看这个 commit 中新增的测试用例也可以大致印证这个想法:

但是!!
实验过后发现,下面的代码并不能起到和旧 API 一样的效果
javascript
protocol.handle('customProtocol', (request) => {
if (/* 命中某些条件 */) {
return {error: -3};
}
});
于是继续翻刚刚的 PR,又发现了其中的另一个 commit 中包含这样的代码:

这个 commit 中删除了对于 error 字段的支持 ,将 handler 的入参和出参规范化为 globalThis.Request 和 globalThis.Response 对象。
在返回的 Response 对象中,若 type 字段为 'error' 时,等同于旧实现中 callback({error: ERR_FAILED}) 的效果。观察其余部分的代码可以发现,这个常量值是 -2:

因此,可以得出结论:protocol.handle 中移除了对请求指定失败原因的支持。
且下面的两段代码等同:
javascript
protocol.handle('customProtocol', (request) => {
if (/* 命中某些条件 */) {
return Response.error();
}
});
protocol.registerHttpProtocol('customProtocol', (request, callback) => {
if (/* 命中某些条件 */) {
callback({error: -2});
}
});
即当新 API 的 handler 返回 Response.error() 时,其效果与旧 API 中调用 callback({error: -2}) 相同。
读取本地 path
当拦截文件类型的请求时,旧版 API 可以这样做:
javascript
protocol.registerFileProtocol('some-protocol', (request, callback) => {
callback({ filePath: '/path/to/my/file' });
});
在新版 API 中,需要注意添加上 file 协议的前缀:
javascript
protocol.handle('some-protocol', () => {
return net.fetch('file:///path/to/my/file');
});
如果觉得处理 URL 很麻烦,也可以使用 url 这个 npm 包简化操作:
javascript
const {pathToFileURL} = require('url');
protocol.handle('some-protocol', () => {
return net.fetch(pathToFileURL('/path/to/my/file').toString());
});
这是去调研了另一个开源项目 Rancher Desktop 的这个 PR,从其中的这个 commit 里借鉴来的。
结语
Electron 官方推出了新的 protocol.handle 方法,旨在简化网络协议的处理,但这一变动带来了不小的挑战 。新的 protocol.handle API 虽然在设计上更为简洁,但在实践中可能需要一些奇技淫巧才能保持对旧 API 的兼容。
唯一的感想:希望 Electron 团队能够多多完善一下文档,尤其是使用新的 API 替代旧 API 的时候,帮助大家顺利过渡到新的 API,另外保持对旧功能的兼容性,别说砍就砍了。