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,另外保持对旧功能的兼容性,别说砍就砍了。

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试