背景
根据 Electron 官方的更新公告:Electron 25.0.0 | Electron (electronjs.org)
以下几个 API 方法已被标记为弃用
registerFileProtocol
registerBufferProtocol
registerStringProtocol
registerHttpProtocol
registerStreamProtocol
unregisterProtocol
isProtocolRegistered
interceptFileProtocol
interceptStringProtocol
interceptBufferProtocol
interceptHttpProtocol
interceptStreamProtocol
主要原因是官方对 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
字段的说明如下:
error
Integer(可选的) - 如果赋值,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,另外保持对旧功能的兼容性,别说砍就砍了。