Electron 废弃 API 改造的疑难杂症——protocol.register*Protocol

背景

根据 Electron 官方的更新公告:Electron 25.0.0 | Electron (electronjs.org)

以下几个 API 方法已被标记为弃用

主要原因是官方对 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.RequestglobalThis.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,另外保持对旧功能的兼容性,别说砍就砍了。

相关推荐
Auroral156几秒前
基于RabbitMQ的异步通知系统设计与实现
前端·后端
栗筝i1 分钟前
Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲
前端·网络·spring
打野赵怀真3 分钟前
H5如何禁止动画闪屏?
前端·javascript
zhangxingchao4 分钟前
关于浮点数的思考
前端
Riesenzahn4 分钟前
你喜欢Sass还是Less?为什么?
前端·javascript
玄魂5 分钟前
基于Vue框架的开源大屏项目实践
前端·开源·数据可视化
晴殇i6 分钟前
一行代码解决跨域问题,JavaScript新特性解析
前端
挖稀泥的工人10 分钟前
面试看这一篇webpack
前端·webpack
卖报的小行家_11 分钟前
Vue3源码,拦截对象,对比Vue2
前端
蒜香拿铁14 分钟前
vue3自动导入组合式api
前端·javascript