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

相关推荐
前端郭德纲2 小时前
浅谈React的虚拟DOM
前端·javascript·react.js
2401_879103682 小时前
24.11.10 css
前端·css
ComPDFKit3 小时前
使用 PDF API 合并 PDF 文件
前端·javascript·macos
yqcoder4 小时前
react 中 memo 模块作用
前端·javascript·react.js
优雅永不过时·4 小时前
Three.js 原生 实现 react-three-fiber drei 的 磨砂反射的效果
前端·javascript·react.js·webgl·threejs·three
神夜大侠7 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱7 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号7 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy72937 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲8 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6