主要观点总结
得物商家客服团队决定从Electron迁移到Tauri框架,以解决Electron应用在性能、内存占用、CPU占用以及包体积上的问题。迁移过程涉及多个方面,包括技术调研、代码迁移、构建打包、签名更新等。尽管Tauri在性能和体积上有所提升,但仍面临社区活跃度不足、Webview2问题、以及成熟度和稳定性不够的挑战。未来,得物商家客服将继续优化应用并提升用户体验。
关键观点总结
关键观点1: 技术调研
在选择框架时,得物商家客服考虑了Electron和Tauri。最终选择Tauri是因为其更小的包体积、更高的性能和更少的内存占用。
关键观点2: 代码迁移
将Electron代码迁移到Tauri涉及调整目录结构、处理跨域请求、兼容关键性API以及进行兼容性回归测试。
关键观点3: 构建打包
Tauri提供了构建工具,支持不同平台的构建,如Windows、MacOS和Linux。在Windows 7上,需要特别处理Webview2的兼容性问题。
关键观点4: 签名更新
Tauri支持签名和更新机制,但需要自行处理签名过程,并确保更新机制兼容不同操作系统。
关键观点5: 社区和稳定性
尽管Tauri在性能和体积上有所提升,但面临社区活跃度不足、Webview2兼容性问题以及成熟度和稳定性不够的挑战。
正文
在聊如何调整Tauri目录结构之前,我们需要先来了解一下之前的Electron应用目录结构设置,一个最简单的Electron应用的目录结构大致如下:
.
├── index.html
├── main.js
├── renderer.js
├── preload.js
└── package.json
其中文件说明如下:
有的时候你可能需要划分目录来编写不同功能的代码,但是,不管功能目录怎么改,最终的渲染进程和主进程的构建产物都是期望符合类似于上面的结构。
所以,之前得物的商家客服也是类似形式的目录结构:
.
├── app
├── renderer-process
├── ...
└── package.json
对于Tauri来说,Tauri打包依托于两个部分,首先是对前端页面的构建,这块可以根据业务需要和框架选择(Vue、 React)进行构建脚本的执行。一般前端构建的产物都是一个dist文件包。
然后是Tauri后端程序部分的构建,这块主要是对Rust代码进行编译成binary crate。
(Tauri后端的编译在很大程度上依赖于操作系统原生库和工具链,因此当前无法进行有意义的交叉编译。所以,在本地编译我们通常需要准备一台mac和一台Windows电脑,以满足在这两个平台上的构建。)
整体来看,和Electron是差不多的,这里,我们就直接使用了官方提供的create-tauri-app(https://github.com/tauri-apps/create-tauri-app)脚手架来创建项目,其目录结构大致如下:
.
├── src
├── src-tauri
├── ...
└── package.json
所以,这里对渲染进程的目录调整就很清晰了,直接将我们之前Electron中的renderer-process目录中的代码迁移到src目录中即可。
注意:因为我们对渲染进程目录进行了调整,所以对应的打包工具的目录也需要进行调整。
商家客服中会有一些接口请求,这些接口请求有的是从业务中发起的,有的使用依赖的npm库中发起的请求。但因为是客户端引用,当从客户端环境发起请求时,请求所携带的origin是这样的:
那么,就会遇到一个我们熟知的一个前端跨域问题。这会导致如果不在access-ctron-allow-origin中的域名会被block掉。
如果有小伙伴对Electron比较熟悉,可能会知道在Electron实现跨域的方案之一是可以关闭浏览器的跨域安全检测:
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
或者在请求返回给浏览器之前进行拦截,手动修改
access-ctron-allow-origin
让其支持跨域:
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
'Access-Control-Allow-Origin': ['*'],
...details.responseHeaders,
},
});
});
}
那么Tauri中可以这么做吗?答案是不行的!
虽然Tauri虽然和Electron进程模型很类似,但是本质上还是有区别的,最大的区别就是Electron中的渲染进程是基于Chromium魔改的,他可以在Chromium中植入一些控制器来修改Chromium的一些默认行为。但Tauri完全是基于不同平台的内置Webview封装,考虑的兼容性问题,并没有对Webview进行改造(虽然Windows的Webview2支持 --disable-web-security,但是其他平台不行)。所以他的跨域策略是Webview默认的行为,无法调整。
那么在Tauri中,如何发起一个跨域请求了?
其实社区也有几种解决方案,接下来简单介绍一下社区的方案和问题。
使用Tauri官方的http
既然浏览器会因为跨域问题block掉请求,那么就绕过浏览器呗,没错,这也是Tauri官方提供的http模块设计的初衷和原理:https://v1.tauri.app/zh-cn/v1/api/js/http/,其设计方案就是通过JavaScript前端调用Rust后端来发请求,当请求完成后再返回给前端结果。
问题:Tauri http有一套自己的API设计和请求规范,我们必须按照他定义的格式进行请求的发送和接收。对于新项目来说问题不是很大,但对商家客服来说,这样最大的问题是之前的所有的接口请求都得改造成Tauri http的格式,我们很多请求是基于Axios的封装,改造成本非常大,回归验证也很困难,而且有很多三方npm包也依赖axios发请求,这就又增加了改造的成本和后期维护的成本。
使用axios adapter
既然使用axios改造成本大,那么就写一个axios的适配器(adapter)在数据请求的时候不使用浏览器原生的xhr发请求而是使用tauri http来发请求,顺便对axios的请求参数进行格式化,处理成Tauri http要求的那种各种。在请求响应后也进行类似的处理。
这种解决方案社区也有一个库提供:https://github.com/persiliao/axios-tauri-api-adapter
问题:假设项目中依赖一个npm库,这个库中发起了一个axios请求,那么也需要对这个库的axios进行适配器改造。这样还是解决不了三方依赖使用axios的问题。我们还是需要侵入npm包进行axios改造。另外,如果其他库使用的是xhr或者fetch来直接发请求或者,那就又无解了。
最后,不管使用方案1还是2,都有个通病,那就是请求都是走的Tauri后端来发起的,这也意味着我们将在Webview的devtools中的network看不到任何请求的信息和响应的结果,这对开发调试来说无疑是非常难以接受的。
社区对这个问题也有相关的咨询:https://github.com/tauri-apps/tauri/issues/7882,但是官方回复也是实现不了:
那我们是怎么做的呢?对于Axios来说,其在浏览器端工作的原理是通过实例化window.XMLHttpRequest 后的xhr来发起请求,同时监听xhr的onreadystatechange事件来处理请求的响应。然后对于一些请求头都是通过xhr.setRequestHeader这样的方式设置到了xhr对象上。因此,对于axios、原生XmlHttpRequest请求来说,我们就可以重写XmlHttpRequest中的send、onreadystatechange、setRequestHeader等方法,让其通过Tauri的http来发请求。
但是对window.fetch这样底层未使用XHR的请求来说,我们就需要重写window.fetch。让其在调用window.fetch的时候,调用xhr.send来发请求,这样便实现了变相调用Tauri http的功能。
核心代码:
class AdapterXMLHTTP extends EventTarget{
async send(data: unknown) {
TauriFetch(this.url, {
body: buildTauriRequestData(config.data),
headers: config.headers,
responseType: getTauriResponseType(config.responseType),
timeout: timeout,
method: this.method?.toUpperCase()
}).then((response: any) => {
}
}
}
function fetchPollify (input, init) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequst()
})
}
window.XMLHttpRequest = AdapterXMLHTTP;
window.fetch = fetchPollify;
那怎么解决devtools没法调试请求的问题呢?
为了让请求日志能出现在浏览器的webview devtools network中,我们可能需要开发一个类似于chrome plugin的方式来支持。但是很可惜,在Tauri中,webview是不支持插件开发的:https://github.com/tauri-apps/tauri/discussions/2685
所以我们只能采用新的方式来支持,那就是外接devtools。啥意思呢?就是在操作系统网络层代理掉网络请求,然后输出到另一个控制台中进行展示,原理类似于Charles。
到这里,我们就完成了对跨域网络请求的处理改造工作。核心架构图如下:
这里需要注意的是,Tauri使用的是系统自带的Webview,而Electron则是直接内置了Chromium,这里有个非常大的误区在于想当然的把Webview类比Chromium以为浏览器的API都可以直接使用。这其实是不对的,举个例子:我们在发送一些消息通知的时候,可能会使用HTML5的 Notification Web API:https://developer.mozilla.org/en-US/docs/Web/API/Notification
但是,这个API是浏览器自行实现的,也就是说,你在 Electron 中可以这么用,但是,如果你在Tauri中,你会发现一个bug:https://github.com/tauri-apps/tauri/issues/3698,这个bug的大概含义就是Tauri中的Notification不会触发click点击事件。这个bug至今还未解决。究其原因:
Tauri依赖的操作系统webview并没有实现对Notification 的支持,webview本身希望宿主应用自行实现对Notification的实现,所以Tauri就重写了JS的Notification API,当你在调用window Notification的时候,实际上你和Rust进程完成了一次通信,调用的还是tauri::Notification模块。
在Tauri源码里面,是这样实现的:
function sendNotification(options) {
if (typeof options === 'object') {
Object.freeze(options)
}
return window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Notification',
message: {
cmd: 'notification',
options:
typeof options === 'string'
? {
title: options
}
: options
}
})
}
window.Notification = function (title, options) {
const opts = options || {}
sendNotification(
Object.assign(opts, {
title: title
})
)
}
除此之外,Tauri还分别实现了:
所以,我们在对商家客服从Electron迁移到Tauri的过程中,还需要对这些关键性API进行兼容性测试和回归。一旦发现相关API不符合预期,我们需要及时调整业务策略或者给尝试进行hack。
(这里卖个关子,虽然Tauri不支持对Notification的点击事件回调,那么我们是怎么让他支持的呢?在下一节主进程代码迁移中我们会详细介绍。)
对于样式兼容性来说,因为Electron在不同操作系统内都集成了Chromium所以我们完全不用担心样式兼容性的问题。但是对于Tauri来说,因为不同操作系统使用了不同的Webview,所以在样式上,我们还是需要注意不同操作系统下的差异性,比如:以下分别是Linux和Windows渲染Element-Plus的界面:
可以看到在按钮大小、文字对齐等样式上面还是存在着不小的差距。
除了上述问题,如果你需要兼容Linux系统,那么还有webkitgtk在非整数倍缩放下的bug,应该是陈年老问题了。当然,这些问题都是上游webkitgtk的“锅”。
所以,社区也有关于讨论Tauri是否有可能在不同平台上使用同一个webview的可能性的讨论:https://github.com/tauri-apps/tauri/discussions/4591。官方是期待能有Mac版本的Webview发布,不过大概率来看不太现实,一方面是因为:微软决定不开源 Webview2的Mac和Linux版本(https://mp.weixin.qq.com/s/p6pdNI3_di7oBkv4ugDIdA),另一方面是如果要使用统一的webview那就又回到了Electron。
除了样式兼容性外,对于JS代码的兼容性也需要留意Tauri在Windows上使用的是Webview2而Webview2本身就是基于Chromium的,所以代码兼容性倒还好,但是在MacOS 上使用的就是WebKit.WKWebview,Safari就是基于他,所以到这里,我想你也明白了,这就又回到了前端处理不同浏览器兼容性的问题上来了。所以这里温馨提示一下:构建时前端代码需要进行polyfill。
对于Electron应用的用户来说,可能没有这样的烦恼,最新的API只要Chrome支持,那就可以用。