专栏名称: OSC开源社区
OSChina 开源中国 官方微信账号
目录
相关文章推荐
赛尔实验室  ·  哈工大SCIR 8篇长文被 COLING ... ·  2 天前  
OSC开源社区  ·  PostgreSQL将不再支持MD5密码 ·  2 天前  
程序员小灰  ·  公司来的新人把 MyBatis 替换成 ... ·  4 天前  
程序员鱼皮  ·  离谱!学编程两年,还不会用工具类? ·  4 天前  
程序员鱼皮  ·  离谱!学编程两年,还不会用工具类? ·  4 天前  
程序员小灰  ·  真心建议大家拿下软考证(风口) ·  6 天前  
51好读  ›  专栏  ›  OSC开源社区

从Electron迁移到Tauri,安装包体积减少91%、内存占用下降50%

OSC开源社区  · 公众号  · 程序员  · 2024-12-07 19:00

正文


↓推荐关注↓



目录

一、背景

二、技术调研

    1. Electron的核心模块

    2. Tauri的核心模块

    3. 选型总结

三、技术实现

    1. 渲染进程代码迁移

    2. 主进程代码迁移

    3. 应用构建打包

    4. 应用签名&更新

四、收益&总结

    1. 社区活跃度还需要提升

    2. Webview2的问题

    3. 成熟度和稳定性还不够


背景

得物商家客服采用的是桌面端应用表现形式,而桌面端应用主要架构形式就是一套和操作系统交互的“后端” + 一套呈现界面的“前端(渲染层)”。而桌面端技术又可以根据渲染层的不同核心划分为以下几类:

  • C语言家族:原生开发、QT

  • Chromium家族:NW、Electron、CEF

  • Webview 家族:Tauri、pywebview、webview_java

  • 自立山头:Flutter


在2022年5月份左右,得物商家客服开始投入桌面端应用业务,其目标是一个可以适配多操作系统(MacOS、Windows)、快速迭代、富交互的产品。


考虑到以上前提,我们当时可以选择的框架是Chromium家族或者Webview家族。但是当时对于Webview来说,Tauri 还并不成熟(在 2022年6月才发布了1.0版本)生态也不够丰富。


对于pywebview和webview_java相对于前端来说,一方面门槛较高,另一方面生态也非常少。


所以,在当时,我们选择了Chromium家族中的Electron框架。这是因为对于CEF、Electron、NW来说,Electron有着对前端开发非常友好的技术栈,仅使用JavaScript就可以完成和操作系统的交互以及交互视觉的编写,另外,Electron的社区活跃度和生态相对于其他两者也有非常大的优势。最重要的是:真的很快!


但是,随着时间的推移,直到2024年的今天,商家客服的入驻量和使用用户越来越多,用户的电脑配置也是参差不齐,Electron的弊端开始显现:

  • 性能方面:随着商家客服入驻数量的快速增加,现有Electron桌面应用在多账户+多会话高并发场景下,占用内存特别大,存在性能瓶颈;

  • 安全方面:Electron在内存安全性、跨平台攻击、不受限制的上下文和依赖管理等方面存在一些潜在的弱点;

  • 体验方面:现有Electron桌面应用包体积大,下载、更新成本较高;

  • 信息集成方面:商家客服目前需要在商家后台、商家客服后台、商家客服工作台3个系统来回切换操作,使用成本很高。


我们也发现,之前调研过的Tauri作为后起之秀,其生态和稳定性在今天已经变得非常出色,我们熟知的以下应用都是基于Tauri开发,涵盖:游戏、工具、聊天、金融等等领域:

  • ChatBox:https://github.com/Bin-Huang/chatbox 20k+ star

  • ChatGPT 桌面端:https://github.com/lencx/ChatGPT 51k+ star

  • Clash Verge:https://github.com/clash-verge-rev/clash-verge-rev 28k+ star


除此之外,因为Tauri是基于操作系统自带的Webview + Rust的框架。首先,因为不用打包一个Chromium,所以包体积非常的小:


其次Rust作为一门系统级编程语言,具有以下特点:

  • 内存安全:Rust通过所有权和借用机制,在编译时检查内存访问的安全性,避免了常见的内存安全问题,如空指针引用、数据竞争等;

  • 零成本抽象:Rust提供了丰富的抽象机制,如结构体、枚举、泛型等,但不引入运行时开销。这意味着开发者可以享受高级语言的便利性,同时保持接近底层语言的性能;

  • 并发性能:Rust内置支持并发和异步编程,通过轻量级的线程(称为任务)和异步函数(称为异步任务)来实现高效的并发处理。Rust的并发模型保证了线程安全和数据竞争的检查,以及高性能的任务调度和通信机制;

  • 可靠性和可维护性:Rust强调代码的可读性、可维护性和可靠性。它鼓励使用清晰的命名和良好的代码结构,以及提供丰富的工具和生态系统来支持代码质量和测试覆盖率;


Rust的这些额外的特性使其成为改善桌面应用程序性能和安全性的理想选择。


技术调研

要实现Electron迁移到Tauri,得先分别了解Electron和Tauri的核心功能和架构模型,只有了解了这些,才能对整体的迁移成本做一个把控。


Electron的核心模块

基础架构

首先来看看Electron的基础架构模型:Electron继承了来自Chromium的多进程架构,Chromium始于其主进程。从主进程可以派生出渲染进程。渲染进程与浏览器窗口是一个意思。主进程保存着对渲染进程的引用,并且可以根据需要创建/删除渲染器进程。


每个Electron的应用程序都有一个主入口文件,它所在的进程被称为 主进程(Main Process)。而主进程中创建的窗体都有自己运行的进程,称为渲染进程(Renderer Process)。每个Electron的应用程序有且仅有一个主进程,但可以有多个渲染进程。


应用构建打包

打包一个Electron应用程序简单来说就是通过构建工具创建一个桌面安装程序(.dmg、.exe、.deb 等)。在Electron早期作为 Atom 编辑器的一部分时,应用程序开发者通常通过手动编辑Electron二进制文件来为应用程序做分发准备。随着时间的推移,Electron社区构建了丰富的工具生态系统,用于处理Electron应用程序的各种分发任务,其中包括:

  • 应用程序打包https://github.com/electron/packager

  • 代码签名,例如https://github.com/electron/osx-sign

  • 创建特定平台的安装程序,例如https://github.com/electron/windows-installer或https://github.com/electron-userland/electron-installer-dmg

  • 本地Node.js原生扩展模块重新构建https://github.com/electron/rebuild

  • 通用MacOS构建https://github.com/electron/universal


这样,应用程序开发者在开发Electron应用时,为了构建出跨平台的桌面端应用,不得不去了解每个包的功能并需要将这些功能进行组合构建,这对新手而言过于复杂,无疑是劝退的。


所以,基于以上背景,目前使用的比较多的是社区提供的Electron Builder(https://github.com/electron-userland/electron-builder)一体化打包解决方案。得物商家客服也是采用的上述方案。


应用签名&更新

现在绝大多数的应用签名都采用了签名狗的应用签名方式,而我们的商家客服桌面端应用也是类似,Electron Builder提供了一个sign的钩子配置,可以帮助我们来实现对应用代码的签名:

...    "win": {      "target": "nsis",      "sign": "./sign.js"    },...


(详细的可以直接阅读electron builder官网介绍,这里只做简单说明)


对于应用更新而言,我们之前采用的是electron-updater自动更新模式:


如果对这块感兴趣,可以阅读我们之前的文章:
https://juejin.cn/post/7195447709904404536?searchId=202408131832375B6C2C76DEEE740762EA


Tauri的核心模块

基础架构

那么,Tauri的基础架构模型是什么样的?其实官网对这块的介绍比较有限,但是我们可以通过其源码仓库和代码结构管中窥豹的了解Tauri的核心架构模型,为了方便大家理解,我们以得物商家客服桌面端应用为模型,简单的画了一个草图:


一些核心模块的解释:

WRY

由于Web技术具有表现力强和开发成本低的特点,与 Electron 和NW等框架类似,Tauri应用程序的前端实现是使用Web技术栈编写的。那么Tauri是如何解决Electron/CEF等框架遇到的Chromium内核体积过大的问题呢?


也许你会想,如果每个应用程序都需要打包浏览器内核以实现Web页面的渲染,那么只要所有应用程序共享相同的内核,这样在分发应用程序时就无需打包浏览器内核,只需打包Web页面资源。


WRY是Tauri的封装Webview框架,它在不同的操作系统平台上封装了系统的Webview实现:MacOS上使用WebKit.WKWebview,Windows上使用Webview2,Linux上使用WebKitGTK。这样,在运行Tauri应用程序时,直接使用系统的Webview来渲染应用程序的前端展示。


TAO

跨平台应用窗口创建库,使用Rust编写,支持Windows、MacOS、Linux、iOS和Android等所有主要平台。该库是winit的一个分支,Tauri根据自己的需求进行了扩展,如菜单栏和系统托盘功能。


JS API

这个API是一个JS库,提供调用Tauri Rust后端的一些API能力,利用这个库可以很方便的完成和Tauri Rust后端的交互以及通信。


看起来有点复杂,其实核心也是分成了主进程和渲染进程两个部分。

  • Tauri的主进程使用Rust编写,Tauri在主进程中提供了一些常用的Rust API比如窗口创建、消息提醒... 如果我们觉得主进程提供的API不够,那么我们可以通过Tauri的插件体系自行扩展。

  • Tauri的渲染进程则是运行在操作系统的Webview当中的,我们可以直接通过JS + HTML + CSS来编写,同时,Tauri会为渲染进程注入一些全局的JS API函数。比如fs、path、shell等等。


Tauri

这是将所有组件拼到一起的crate。它将运行时、宏、实用程序和API集成为一款最终产品


应用构建打包

Tauri提供了一个CLI工具:https://v1.tauri.app/zh-cn/v1/api/cli/,通过这个CLI工具的一个命令,我们可以直接将应用程序打包成目标产物:

yarn tauri build


此命令会将渲染进程的Web资源 与 主进程的Rust代码一起嵌入到一个单独的二进制文件中。二进制文件本身将位于src-tauri/target/release/[应用程序名称],而安装程序将位于src-tauri/target/release/bundle/。


第一次运行此命令需要一些时间来收集Rust包并构建所有内容,但在随后的运行中,它只需要重新构建您的应用程序代码,速度要快得多。


应用签名&更新

Tauri的签名和Electron类似,如果需要自定义签名钩子方法,在Tauri中现在也是支持的:

{   "signCommand": "signtool.exe --host xxxx %1"}


后面我们会详细介绍该能力的使用方式。


而对于更新而言,Tauri则有自己的一套体系:Updater | Tauri Apps这里还是和Electron有着一定的区别。


选型总结

通过上面的架构模型对比,我们可以很直观的感受到如果要将我们的Electron应用迁移到Tauri上,整体的迁移改造工作可以总结成以下图所示:


核心内容就变成了以下四部分内容:

  • 主进程的迁移:而对于商家客服来说,目前主要用的有:

    自定义窗口

    autoUpdater自动更新

    BrowserWindow窗口创建

    Notification消息通知

    Tray系统托盘

    IPC通信

而这些API在Tauri中都有对应的实现,所以整体来看,迁移成本和技术可行性都是可控的。


  • 渲染进程的迁移:渲染进程改造相对而言就少很多了,因为Tauri和Electron都可以直接使用前端框架来编写渲染层代码,所以几乎可以将之前的前端代码直接平移过来。但是还是有一些小细节需要注意,比如IPC通信、JS API的改变、兼容性... 这部分后面也会详细介绍。

  • 应用构建打包:从之前的Electron构建模式改成Tauri构建模式,并自动化整个构建流程和链路。

  • 应用签名&更新:签名形式不用改,主要需要调整签名的配置,实现对Tauri应用的自动签名和自动更新能力。

最终,我们选择了Tauri对现有的商家客服桌面端进行架构优化升级。


技术实现

渲染进程代码迁移

目录结构调整

在聊如何调整Tauri目录结构之前,我们需要先来了解一下之前的Electron应用目录结构设置,一个最简单的Electron应用的目录结构大致如下:

.├── index.html├── main.js├── renderer.js├── preload.js└── package.json


其中文件说明如下:

  • index.html:渲染进程的入口HTML文件。

  • renderer.js:渲染进程的入口JS文件。

  • main.js:主进程入口文件

  • preload.js:预加载脚本文件

  • package.json:包的描述信息,依赖信息

有的时候你可能需要划分目录来编写不同功能的代码,但是,不管功能目录怎么改,最终的渲染进程和主进程的构建产物都是期望符合类似于上面的结构。


所以,之前得物的商家客服也是类似形式的目录结构:

.├── app              // 主进程代码目录├── renderer-process // 渲染进程代码目录├── ...              // 一些其他配置文件,vite 构建文件等等└── 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        // Rust 后端代码├── ...              // 一些其他配置文件,vite 构建文件等等└── package.json


所以,这里对渲染进程的目录调整就很清晰了,直接将我们之前Electron中的renderer-process目录中的代码迁移到src目录中即可。


注意:因为我们对渲染进程目录进行了调整,所以对应的打包工具的目录也需要进行调整。


跨域请求处理

商家客服中会有一些接口请求,这些接口请求有的是从业务中发起的,有的使用依赖的npm库中发起的请求。但因为是客户端引用,当从客户端环境发起请求时,请求所携带的origin是这样的:

https://tauri.localhost


那么,就会遇到一个我们熟知的一个前端跨域问题。这会导致如果不在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{    // ...    // 重写 send 方法    async send(data: unknown) {        // 通过 TauriFetch 来发请求        TauriFetch(this.url, {          body: buildTauriRequestData(config.data),          headers: config.headers,          responseType: getTauriResponseType(config.responseType),          timeout: timeout,          method: this.method?.toUpperCase()        }).then((response: any) => {           // todo        }    }}
function fetchPollify (input, init) { return new Promise((resolve, reject) => { // ... // 使用 xhr 来发请求 const xhr = new XMLHttpRequst() })}
// 重写 window.XMLHttpRequestwindow.XMLHttpRequest = AdapterXMLHTTP;// 重写 window.featchwindow.fetch = fetchPollify;


那怎么解决devtools没法调试请求的问题呢?


为了让请求日志能出现在浏览器的webview devtools network中,我们可能需要开发一个类似于chrome plugin的方式来支持。但是很可惜,在Tauri中,webview是不支持插件开发的:https://github.com/tauri-apps/tauri/discussions/2685


所以我们只能采用新的方式来支持,那就是外接devtools。啥意思呢?就是在操作系统网络层代理掉网络请求,然后输出到另一个控制台中进行展示,原理类似于Charles。


到这里,我们就完成了对跨域网络请求的处理改造工作。核心架构图如下:


关键性API兼容

这里需要注意的是,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源码里面,是这样实现的:

  // https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/scripts/core.js#L256-L282  function sendNotification(options) {    if (typeof options === 'object') {      Object.freeze(options)    }    // 和 Rust 后端通信,调用 Rust 发送系统通知    return window.__TAURI_INVOKE__('tauri', {      __tauriModule: 'Notification',      message: {        cmd: 'notification',        options:          typeof options === 'string'            ? {              title: options            }            : options      }    })  }  //  这里便是对 Notification 的重写实现  window.Notification = function (title, options) {    const opts = options || {}    sendNotification(      Object.assign(opts, {        title: title      })    )  }


除此之外,Tauri还分别实现了:

  • DOM上标签的点击跳转功能,使用内置的Tauri API进行打开webview。

  • 差异化操作系统原生窗口的拖拽和最大化事件:在Windows和Linux上,当鼠标按下时拖动,双击时最大化;而在MacOS上,最大化应该在鼠标抬起时发生,如果双击后鼠标移动,应该取消最大化。

  • window.alert

  • window.confirm

  • window.print(Macos)


所以,我们在对商家客服从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支持,那就可以用。


主进程代码迁移

自定义操作栏窗口

默认情况,在构建窗口的时候,会使用系统自带的原生窗口样式,比如在MacOS下的样式:


在有些情况下,操作系统的原生窗口并不能符合我们的一些视觉和交互需求。所以,在创建桌面应用的时候,有时候我们希望能完全掌控窗口的样式,而隐藏掉系统提供的窗口边框和标题栏等。这个时候就需要用到自定义操作栏窗口。比如在Windows中,我们希望在右上角有一排自定义的操作栏,就像是这样:


商家客服桌面端的窗口就是一个无边框的自定义操作栏的窗口,在Electron中,我们可以这样操作快速创建一个无边框窗口:

const { BrowserWindow } = require('electron')const win = new BrowserWindow({ frame: false })


然后在渲染进程中,自己 “画一个标题栏”:

class="handle-container"> <div class="minimize" @click="minimize">div>
class="maximize" @click="maximize">div>
class="close" @click="close">div></div>


然后定义一下icon的样式:

.minimize {  background: center / 20px no-repeat url("./assets/minimize.svg");}.maximize {  background: center / 20px no-repeat url("./assets/maximize.svg");}.unmaximize {  background: center / 20px no-repeat url("./assets/unmaximize.svg");}.close {  background: center / 20px no-repeat url("./assets/close.svg");}.close:hover {  background-color: #e53935;  background-image: url("./assets/close-hover.svg");}


但是在Tauri中,要实现自定窗口首先需要在窗口创建的时候设置decoration无装饰样式,比如这样:(也可以在tauri.config.json中设置,道理是一样的)

let window = WindowBuilder::new(  &app,  "main",  WindowUrl::App("/src/index.html".into()),)  .inner_size(400., 300.)  .visible(true)  .resizable(false)  .decorations(false)  .build()  .unwrap();


然后就是和Electron类似,自己画一个控制栏,详细的代码可以参考这里:https://v1.tauri.app/v1/guides/features/window-customization/

<div data-tauri-drag-region class="titlebar">  <div class="titlebar-button" id="titlebar-minimize">    <img      src="https://api.iconify.design/mdi:window-minimize.svg"      alt="minimize"    />  div>  <div class="titlebar-button" id="titlebar-maximize">    <img      src="https://api.iconify.design/mdi:window-maximize.svg"      alt="maximize"    />  div>  <div class="titlebar-button" id="titlebar-close">    <img src="https://api.iconify.design/mdi:close.svg" alt="close" />  div>div>


单例模式

通过使用窗口单例模式,可以确保应用程序在用户尝试多次打开时只会有一个主窗口实例,从而提高用户体验并避免不必要的资源占用。在Electron中可以很容易做到这一点:

app.on('second-instance', (event, commandLine, workingDirectory) => {  // 当运行第二个实例时,将会聚焦到myWindow这个窗口  if (myWindow) {    mainWindow.show()    if (myWindow.isMinimized()) myWindow.restore()    myWindow.focus()  }})


但是,在Tauri中,我需要引入一个单例插件才可以:

use tauri::{Manager};
#[derive(Clone, serde::Serialize)]struct Payload { args: Vec, cwd: String,}
fn main() { tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { app.emit("single-instance", Payload { args: argv, cwd }).unwrap(); })) .run(tauri::generate_context!()) .expect("error while running tauri application");}


其在Windows下判断单例的核心原理是借助了windows_sys这个Crate中的CreateMutexW API来创建一个互斥体,确保只有一个实例可以运行,并在用户尝试启动多个实例时,聚焦于已经存在的实例并传递数据,简化后的代码大致如下:

pub fn init(f: Box>) -> TauriPlugin {    plugin::Builder::new("single-instance")        .setup(|app| {            // ...            // 创建互斥体            let hmutex = unsafe {                     CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr())                };            // 如果 GetLastError 返回 ERROR_ALREADY_EXISTS,则表示已有实例在运行。            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {                unsafe {                    // 找到已存在窗口的句柄                    let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());
if hwnd != 0 { // ... // 通过 SendMessageW 发送数据给该窗口 SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); // 最后退出当前应用 app.exit(0); } } } // ... Ok(()) }) .build()}


(注意:这里有坑,如果你的应用需要实现一个重新启动功能,那么在单例模式下将不会生效,核心原因是因为应用重启的逻辑是先打开一个新的实例再关闭旧的运行实例。而打开新的实例在单例模式下就被阻止了,这块的详细原因和解决方案我们已经给Tauri提了PR:https://github.com/tauri-apps/tauri/pull/11684)


系统消息通知能力

消息通知是商家客服桌面端应用必不可少的能力,消息通知能力一般可以分为以下两种:

  • 触达操作系统的消息通知

  • 用户点击消息后的回调事件


前面我们有提到,在Electron中,我们需要显示来自渲染进程的通知,那么可以直接使用HTML5的Web API来发送一条系统消息通知:

function notifyMe() {  if (!("Notification" in window)) {    // 检查浏览器是否支持通知    alert("当前浏览器不支持桌面通知");  } else if (Notification.permission === "granted") {    // 检查是否已授予通知权限;如果是的话,创建一个通知    const notification = new Notification("你好!");    // …  } else if (Notification.permission !== "denied") {    // 我们需要征求用户的许可    Notification.requestPermission().then((permission) => {      // 如果用户接受,我们就创建一个通知      if (permission === "granted") {        const notification = new Notification("你好!");        // …      }    });  }  // 最后,如果用户拒绝了通知,并且你想尊重用户的选择,则无需再打扰他们}


如果我们需要为消息通知添加点击回调事件,那么我们可以这么写:

 notification.onclick = (event) => {};


当然,Electron也提供了主进程使用的API,更多的能力可以直接参考Electron的官方文档:https://www.electronjs.org/zh/docs/latest/api/%E9%80%9A%E7%9F%A5。


然而,对于Tauri来说,只实现了第1个能力,也就是消息触达。Tauri本身不支持点击回调的功能,这就导致了用户发来了一个消息,但是业务无法感知客服点击消息的事件。而且原生的Web API也是Tauri自己写的,原理还是调用了Rust的通知能力。接下来,我也会详细介绍一下我们是如何扩展消息点击回调能力的。


Tauri在Rust层,我们可以通过下面这段代码来调用Notification:

use tauri::api::notification::Notification;
let app = tauri::Builder::default() .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) .expect("error while building tauri application");
// 非 win7 可以调用Notification::new(&app.config().tauri.bundle.identifier) .title("New message") .body("You've got a new message.") .show();
// 兼容 win7 的调用形式Notification::new(&app.config().tauri.bundle.identifier) .title("Tauri") .body("Tauri is awesome!") .notify(&app.handle()) .unwrap();
// run the appapp.run(|_app_handle, _event| {});


Tauri的Notification Rust实现源码位置在:https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/notification.rs这个文件中,其中看一下show函数的实现:

pub fn show(self) -> crate::api::Result {    #[cfg(feature = "dox")]    return Ok(());    #[cfg(not(feature = "dox"))]    {      // 使用 notify_rust 构造 notification 实例      let mut notification = notify_rust::Notification::new();      // 设置消息通知的 body\title\icon 等等      if let Some(body) = self.body {        notification.body(&body);      }      if let Some(title) = self.title {        notification.summary(&title);      }      if let Some(icon) = self.icon {        notification.icon(&icon);      } else {        notification.auto_icon();      }      // ... 省略部分代码      crate::async_runtime::spawn(async move {        let _ = notification.show();      });
Ok(()) } } #[cfg(feature = "windows7-compat")] #[cfg_attr(doc_cfg, doc(cfg(feature = "windows7-compat")))] #[allow(unused_variables)] pub fn notify(self, app: &crate::AppHandle) -> crate::api::Result { #[cfg(windows)] { if crate::utils::platform::is_windows_7() { self.notify_win7(app) } else { #[allow(deprecated)] self.show() } } #[cfg(not(windows))] { #[allow(deprecated)] self.show() } } #[cfg(all(windows, feature = "windows7-compat"))] fn notify_win7(self, app: &crate::AppHandle) -> crate::api::Result { let app = app.clone(); let default_window_icon = app.manager.inner.default_window_icon.clone(); let _ = app.run_on_main_thread(move || { let mut notification = win7_notifications::Notification::new(); if let Some(body) = self.body { notification.body(&body); } if let Some(title) = self.title { notification.summary(&title); } notification.silent(self.sound.is_none()); if let Some(crate::Icon::Rgba { rgba, width, height, }) = default_window_icon { notification.icon(rgba, width, height); } let _ = notification.show(); });
Ok(()) }}


这里,我们可以看到notify函数非win7环境下show函数调用的是notify_rust这个库,而在win7环境下调用的是win7_notifications这个库。而notify_rust这个库,本身确实未完成实现对MacOS和Windows点击回调事件。


所以我们需要自定义一个Notification的Tauri插件,实现对点击回调的能力。(因为篇幅原因,这里只介绍一些核心的实现逻辑)


MacOS 支持消息点击回调能力

notify_rust在Mac上实现消息通知是基于Mac_notification_sys这个库的,这个库本身是支持对点击action的response,只是notify_rust没有处理而已,所以我们可以为notify_rust增加对Mac上点击回调的处理能力:

#[cfg(target_os = "macos")]fn show_mac_action(  window: tauri::Window,  app_id: String,  notification: Notification,  action_id: String,  action_name: String,  handle: CallbackFn,  sid: String,) {  let window_ = window.clone();  // Notify-rust 不支持 macos actions 但是 mac_notification 是支持的  use mac_notification_sys::{    Notification as MacNotification,    MainButton,    Sound,    NotificationResponse,  }; // 发通过 mac_notification_sys 送消息通知  match MacNotification::default()      .title(notification.summary.as_str())      .message(¬ification.body)      .sound(Sound::Default)      .maybe_subtitle(notification.subtitle.as_deref())      .main_button(MainButton::SingleAction(&action_name))      .send()  {    // 响应点击事件,回调前端的 handle 函数    Ok(response) => match response {      NotificationResponse::ActionButton(id) => {        if action_name.eq(&id) {          let js = tauri::api::ipc::format_callback(handle, &id)              .expect("点击 action 报错");           window_.eval(js.as_str());        };      }      NotificationResponse::Click => {        let data = &sid;        let js = tauri::api::ipc::format_callback(handle, &data)            .expect("消息点击报错");         window_.eval(js.as_str());      }      _ => {}    },    Err(err) => println!("Error handling notification {}", err),  }}


Win 10上支持消息点击回调能力

在Windows 10操作系统中,notify_rust则是通过winrt_notification这个Crate来发送消息通知,winrt_notification 则是调用的windows这个crate来实现消息通知,windows这个crate的官方描述是:为Rust开发人员提供了一种自然和习惯的方式来调用Windows API。这里,主要会用到以下几个方法:

  • windows::UI::Notifications::ToastNotification::CreateToastNotification:这个函数的作用是根据指定的参数创建一个Toast通知对象,可以设置通知的标题、文本内容、图标、音频等属性,并可以指定通知被点击时的响应行为。通过调用这个函数,可以在Windows应用程序中创建并显示自定义的Toast通知,向用户展示相关信息。

  • windows::Data::Xml::Dom::XmlDocument:这是一个用于在Windows应用程序中创建和处理XML文档的类。它主要提供了一种方便的方式来创建、解析和操作XML数据。

  • windows::UI::Notifications::ToastNotificationManager::CreateToastNotifierWithId:通过调用CreateToastNotifierWithId函数,可以创建一个Toast通知管理器对象,并指定一个唯一的标识符。这个标识符通常用于标识应用程序或者特定的通知渠道,以确保通知的正确分发和管理。创建了Toast通知管理器之后,就可以使用它来生成和发送Toast通知,向用户展示相关信息,并且可以根据标识符进行个性化的通知管理。

  • windows::Foundation::TypedEventHandler:这是Windows Runtime API中的一个委托(delegate)类型。在Windows Runtime中,委托类型用于表示事件处理程序,允许开发人员编写事件处理逻辑并将其附加到特定的事件上。


所以,要想在> win7的操作系统中显示消息同时的主要流程大致是:

  • 通过XmlDocument来创建一个Xml消息通知模板。

  • 然后将创建好的Xml消息模板作为CreateToastNotification的入参来创建一个toast通知。

  • 最后调用CreateToastNotifierWithId来创建一个Toast通知管理器对象,创建成功后显示toast。

  • 通过TypedEventHandler监听用户点击事件并完成回调触发


但是winrt_notification这个库,只完成了1-3步骤,所以我们需要手动实现步骤4。核心代码如下:

fn show_win_action(  window: tauri::Window,  app_id: String,  notification: Notification,  action_id: String,  action_name: String,  handle: CallbackFn,  sid: String,) {  let window_ = window.clone();  // 设置消息持续状态,支持 short 和 long  // short 就是默认 6s  // long 是常驻消息  let duration = match notification.timeout {    notify_rust::Timeout::Default => "duration=\"short\"",    notify_rust::Timeout::Never => "duration=\"long\"",    notify_rust::Timeout::Milliseconds(t) => {      if t >= 25000 {        "duration=\"long\""      } else {        "duration=\"short\""      }    }  };    // 创建消息模版 xml  let template_binding = "ToastGeneric";  let toast_xml = windows::Data::Xml::Dom::XmlDocument::new().unwrap();  if let Err(err) = toast_xml.LoadXml(&windows::core::HSTRING::from(format!(    "                              {}            {}            {}{}                                ",    duration,    String::new(),    template_binding,    ¬ification.icon,    ¬ification.summary,    notification.subtitle.as_ref().map_or("", AsRef::as_ref),    ¬ification.body,  ))) {    println!("Error creating windows toast xml {}", err);    return;  };
// 根据 xml 创建 toast let toast_notification = match windows::UI::Notifications::ToastNotification::CreateToastNotification(&toast_xml) { Ok(toast_notification) => toast_notification, Err(err) => { println!("Error creating windows toast {}", err); return; } }; // 创建消息点击监听捕获 let handler = windows::Foundation::TypedEventHandler::new( move |_sender: &Option<:ui::notifications::toastnotification>, result: &Option<:core::iinspectable>| { let event: Option< windows::core::Result<:ui::notifications::toastactivatedeventargs>, > = result.as_ref().map(windows::core::Interface::cast); let arguments = event .and_then(|val| val.ok()) .and_then(|args| args.Arguments().ok()); if let Some(val) = arguments { let mut js; if val.to_string_lossy().eq(&action_id) { js = tauri::api::ipc::format_callback(handle, &action_id) .expect("消息点击报错"); } else { let data = &sid; js = tauri::api::ipc::format_callback(handle, &data) .expect("消息点击报错"); } let _ = window_.eval(js.as_str()); }; Ok(()) }, );
// 通过消息管理器发送消息 match windows::UI::Notifications::ToastNotificationManager::CreateToastNotifierWithId( &windows::core::HSTRING::from(&app_id), ) { Ok(toast_notifier) => { if let Err(err) = toast_notifier.Show(&toast_notification) { println!("Error showing windows toast {}", err); } } Err(err) => println!("Error handling notification {}", err), }}


Win 7上支持消息通知点击回调能力

在Windows 7中,Tauri调用的是win7_notifications这个库,这个库本身也没有实现对消息点击的回调处理,我们需要扩展win7_notifications的能力来实现对消息通知的回调事件。我们希望这个库可以这样调用:

win7_notify::Notification::new()    .appname(&app_name)    .body(&body)    .summary(&title)    .timeout(duration)    .click_event(move |str| {      // 用户自定义的参数      let data = &sid;      // 触发前端的回调能力      let js = tauri::api::ipc::format_callback(handle, &data)          .expect("消息点击报错");      let _ = window_.eval(js.as_str());    })    .show();


而我们要做的,就是为win7_notify这个库中的Notification结构体增加一个click_event函数,这个函数支持传入一个闭包,这个闭包在点击消息通知的时候执行。

pub struct Notification {    // ...    // 添加 click_event 属性    pub click_event: Option<ArcFn(&str) + Send>>,}
impl Notification { // ... // 添加 click_event 事件注册 pub fn click_event<F: Fn(&str) + Send + 'static>(&mut self, func: F) -> &mut Notification { // 将事件绑定到 Notification 中 self.click_event = Some(Arc::new(func)); self } // 支持对 click_event 的调用 fn perform_click_event(&self, message: &str) { if let Some(ref click_event) = self.click_event { click_event(message); } }}
pub unsafe extern "system" fn window_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM,) -> LRESULT { let mut userdata = GetWindowLongPtrW(hwnd, GWL_USERDATA); match msg { // .... // 增加对点击事件的调用 w32wm::WM_LBUTTONDOWN => { let (x, y) = (GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)); let userdata = userdata as *mut WindowData; let notification = &(*userdata).notification; // todo 增加点击参数 let data = "default"; notification.perform_click_event(&data); if util::rect_contains(CLOSE_BTN_RECT_EXTRA, x as i32, y as i32) { println!("close"); close_notification(hwnd) } DefWindowProcW(hwnd, msg, wparam, lparam) } } }


总结:


  • Tauri本身不支持Notification的点击事件,需要自行实现。

  • 需要对不同操作系统分别实现点击回调能力。

  • MacOS  mac_notification_sys库本来就有点击回调,只是Tauri没有捕获处理,需要自定义捕获处理逻辑就好了。

  • Windows > 7中,通过windows这个crate,来完成调用Windows操作系统API的能力,但是winrt_notification这个库并没有实现对Windows API回调点击的捕获处理,所以需要重写winrt_notification这个库。

  • Windows 7中,消息通知其实是通过绘制窗口和监听鼠标点击来触发的,但是win7_notify本身也没有支持用户对点击回调的捕获,也需要扩展这个库的点击捕获能力。


应用构建打包

Windows 10

Tauri 1.3版本之前,应用程序在Windows上使用的是WiX(Windows Installer)Toolset v3工具进行构建,构建产物是Microsoft安装程序(.msi文件)。1.3之后,使用的是NSIS来构建应用的xxx-setup.exe安装包。


Tauri CLI默认情况下使用当前编译机器的体系结构来编译可执行文件。假设当前是在64位计算机上开发,CLI将生成64位应用程序。如果需要支持32位计算机,可以使用--target标志使用不同的Rust目标编译应用程序:

tauri build --target i686-pc-windows-msvc


为了支持不同架构的编译,需要为Rust添加对应的环境支持,比如:

rustup target add i686-pc-windows-msvc


其次,需要为构建增加不同的环境变量,以便为了在不同的环境进行代码测试,对应到package.json中的构建代码:

{  "scripts": {    "tauri-build-win:t1": "tauri build -t i686-pc-windows-msvc -c src-tauri/t1.json",    "tauri-build-win:pre": "tauri build -t i686-pc-windows-msvc -c src-tauri/pre.json",    "tauri-build-win:prod": "tauri build -t i686-pc-windows-msvc",  }}


-c参数指定了构建的配置文件路径,Tauri会和src-tauri中的tarui.conf.json文件进行合并。除此之外,还可以通过tarui.{{platform}}.conf.json的形式指定不同平台的独特配置,优先级关系:


-c path >> tarui.{{platform}}.conf.json >> tarui.conf.json


Windows 7

Webview 2

Tauri在Windows 7上运行有两个东西需要注意,一个是Tauri的前端跨平台在Windows上依托于Webview2但是Windows 7中并不会内置Webview2因此我们需要在构建时指明引入Webview的方式:

综合比较下来,embedBootstrapper目前是比较好的方案,一方面可以减少安装包体积,一方面减少不必要的静态资源下载。


Windows 7一些特性

在Tauri中,会通过"Windows7-compat"来构建一些Win7特有的环境代码,比如:

#[cfg(feature = "windows7-compat")]{ // todo}


在Tauri文档中也有相关介绍,主要是在使用Notification的时候,需要加入Windows7-compat特性。不过,因为 Tauri 对Notification的点击事件回调是不支持,所以我重写了Tauri的所有Notification模块,已经内置了Windows7-compat能力,因此可以不用设置了。


MacOS

MacOS操作系统也有M1和Intel的区分,所以为了可以构建出兼容两个版本的产物,我们需要使用universal-apple-darwin模式来编译:

{  "scripts": {    "tauri-build:t1": "tauri build -t universal-apple-darwin -c src-tauri/t1.json",    "tauri-build:pre": "tauri build -t universal-apple-darwin -c src-tauri/pre.json",    "tauri-build:prod": "tauri build -t universal-apple-darwin"  }}br


应用签名&更新

应用更新

对于Tauri来说,应用更新的详细配置步骤可以直接看官网的介绍:https://tauri.app/zh-cn/v1/guides/distribution/updater/。这里为了方便大家理解,简单画了个更新流程图:



核心流程如下:

  • 对于需要更新的应用,可以在渲染进程通过JS调用 installUpdate() API

  • Tauri内部会发送一个更新协议事件:

pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";br
  • Tauri主进程Updater模块会响应这个事件,执行download_and_install函数

    通过tauri.config.json中配置的endpoints来寻找下载地址

    下载endpoints服务器上的zip包内容并解压存储到一个临时文件夹,Windows中大概位置在C:\Users\admin\AppData\Local\Temp这里。

    然后通过PowerShell来执行下载的setup.exe文件:

    ["-NoProfile", "-WindowStyle", "Hidden", "Start-Process"],这些参数告诉PowerShell在后台运行,不显示任何窗口,并启动一个新的进程。

if found_path.extension() == Some(OsStr::new("exe")) {      // 创建一个新的 OsString,并将 found_path 包裹在引号中,以便在 PowerShell 中正确处理路径      let mut installer_path = std::ffi::OsString::new();      installer_path.push("\"");      installer_path.push(&found_path);      installer_path.push("\"");            // 构造安装程序参数      let installer_args = [        config          .tauri          .updater          .windows          .install_mode          .nsis_args()          .iter()          .map(ToString::to_string)          .collect(),        vec!["/ARGS".to_string()],        current_exe_args,        config          .tauri          .updater          .windows          .installer_args          .iter()          .map(ToString::to_string)          .collect::>(),      ]      .concat();
// 创建一个新的命令,指向 PowerShell 的路径。 // 使用 Start-Process 命令来启动安装程序, // 并设置 -NoProfile 和 -WindowStyle Hidden 选项, // 以确保 PowerShell 不会加载用户配置文件,并且窗口保持隐藏 let mut cmd = Command::new(powershell_path); cmd .args(["-NoProfile", "-WindowStyle", "Hidden", "Start-Process"]) .arg(installer_path); if !installer_args.is_empty() { cmd.arg("-ArgumentList").arg(installer_args.join(", ")); } // 使用 spawn() 方法启动命令,如果失败,则输出错误信息。 cmd .spawn() .expect("Running NSIS installer from powershell has failed to start");
exit(0); }


  • 在通过PowerShell启动应用安装程序的时候,就会使用到tauri.config.json中配置的updater.windows.installMode功能:

    "basicUi":指定安装过程中包括最终对话框在内的基本用户界面,需要用户手动点击下一步。

    "quiet":安静模式表示无需用户交互。如果安装程序需要管理员权限(WiX),则需要管理员权限。

    "passive":会显示一个只有安装进度条的UI,安装过程用户无需参与。


需要注意的是:如果以为更新是增量更新,不会卸载之前已经安装好的应用程序只更新需要变更的部分。其实是不对的,整个安装过程可以理解为Tauri在后台帮你重新下载了一个最新的安装包,然后帮你重新安装了一下。


总结:更新的核心原理就是通过使用Windows的PowerShell来对下载后的安装包进行open。然后由安装包进行安装。


为什么我要花这么大的篇幅来介绍 Tauri 的更新原理呢?


这是因为我们在更新的过程中碰到了两个比较大的问题:

  • 通过cmd调用PowerShell来安装时,会在安装过程中出现一个蓝色的PowerShell控制台一闪而过:


  • 在部分开启了病毒防护的Windows电脑上,使用PowerShell来执行对安装包的打开,会报错:Permission Denied,导致安装更新失败:https://github.com/rust-lang/rustlings/issues/604


这些都是因为Tauri直接使用 Powershell的问题,那需要怎么改呢?很简单,那就是使用Windows操作系统提供的ShellExecuteW来运行安装程序,核心代码如下:

windows::Win32::UI::Shell::ShellExecuteW(  0,  operation.as_ptr(),  file.as_ptr(),  parameters.as_ptr(),  std::ptr::null(),  SW_SHOW,)


但是这块是Tauri的源码,我们没法直接修改,但这个问题的解决方法我们已经给Tauri提了PR并已合入到官方的1.6.8正式版本当中:https://github.com/tauri-apps/tauri/pull/9818


所以,你要做的就是确保Tauri升级到v1.6.8及以后版本。


应用签名

Tauri应用程序签名可以分成2个部分,第一部分是应用程序签名,第二部分是安装包程序签名,官网上介绍的签名方法需要配置tauri.config.json中如下字段:

"windows": {    // 签名指纹    "certificateThumbprint": "xxx",    // 签名算法    "digestAlgorithm": "sha256",    // 时间戳    "timestampUrl": "http://timestamp.comodoca.com"}


如果你按照官方的步骤来进行签名:https://v1.tauri.app/zh-cn/v1/guides/distribution/sign-windows/,很快就会发现问题所在:官网中签名有一个重要的步骤就是导出一个.pfx文件,但是现在业界签名工具基本上都是采用签名狗的方式进行的,这是一个类似于U盾签名工具,需要插入电脑中才可以进行签名,不支持直接导出.pfx格式的文件:


所以我们需要额外处理一下:


签名狗支持导出一个.cert证书,可以查看到证书的指纹:


这里证书的指纹对应的就是certificateThumbprint字段。


然后需要插入我们在签名机构购买的USB key。这样,在构建的时候,就会提示让我们输入密码:


到这里就可以完成对应用程序的签名。


不过对于我们而言,USB key签名狗是整个公司共享的,通常不在前端开发手里(尤其是异地办公)。一种做法是在Tauri构建的过程中,对于需要签名的软件提供一个signCommand命令钩子,并为这个命令传入文件的路径,然后交由开发者对文件进行自行签名(比如上传到拥有签名工具的电脑,上传上去后,远程进行签名,签名完成再下载)。所以这就需要让Tauri将签名功能暴露出来,让我们自行进行签名,比如这样:

{   "signCommand": "signtool.exe --host xxxx %1"}


该命令中包含一个%1,它只是二进制路径的占位符,Tauri在构建的时候会将需要签名的文件路径替换掉%1

这个功能官网上还没有更新相关的介绍,所以你可能看不到这块的使用方式,因为也是我们最近提交的PR:https://github.com/tauri-apps/tauri/pull/9902。不过目前,这个PR已经被合入Tauri的主版本中,你要做的就是就是升级Tauri1.7.0升级@tauri-apps/cli1.6.0


收益&总结

经过我们的不懈努力(不断地填坑)到目前,得物商家客服Tauri版本终于如期上线,基于Tauri迁移带来的收益如下:


整体性能测试相比之前的Electron应用有比较明显的提升:

  • 包体积7M,Electron 80M下降91.25%。

  • 平均内存占用249M Electron 497M下降49.9%。

  • 平均CPU占用百分比20%,Electron 63.5%下降 63.19%。


整体在性能体验上有一个非常显著改善。但是,这里也暴露出使用Tauri的一些问题。


社区活跃度还需要提升

直到2024年的今天,Tauri依然还不是特别完美,目前官方主要精力是放在了2.0的开发上,对于1.x的版本维护显得力不从心,主要原因也是因为官方人少。


比如,Tauri: dev分支上,主要贡献者(> 30 commit)只有4个人;相对于Electron:主要贡献者有10人。


除此之外,Electron实现了对Chromium的高级定制,因此在Electron中,我们可以使用BrowserView这样的功能,相对于Electron来说,Tauri目前所做的仅仅是对Webview的封装,Webview不支持的功能暂时都不行。另外,系统性的API确实少的可怜。如果要实现一些其他的功能,比如自动更新显示进度条等能力,就不得不使用Rust来扩展API。然后Rust语言学习成本是有一点的,所以,也给我们日常开发带来了不少挑战。


Webview2的问题

因为Tauri在Windows系统上比较依托于Webview2作为渲染的容器,虽然Tauri提供了检测本地电脑是否有安装Webview2以及提供联网下载的能力,但是因为Windows电脑千奇百怪,经常会出现未内置Webview2的Windows电脑下载不成功而导致程序无法启动的情况:

对于这种情况,我们虽然可以将Webview2内置到安装包里面,在用户安装的时候进行内置解压安装,但是这样包体积就跟Electron相差不大。


成熟度和稳定性还不够

我们在将得物商家客服迁移到Tauri的过程中,就遇到了非常多的问题,有些问题是Tauribug。有些问题是Tauri的feature不够,有的是Rust社区的问题。单纯这一个迁移工作,我们就为Tauri社区共享了7个左右的PR:

  • 多窗口失去焦点无法闪烁:https://github.com/tauri-apps/tao/pull/931

  • 单窗口最小化后消息通知无法闪烁:https://github.com/tauri-apps/tao/pull/947

  • 在有病毒防护的Windows电脑中,应用无法正常更新:https://github.com/tauri-apps/tauri/pull/9818

  • 拓展tauri v1.x自定义签名的能力:https://github.com/tauri-apps/tauri/pull/9902

  • win7下消息提醒会导致主窗口失去焦点:https://github.com/tauri-apps/win7-notifications/pull/65

  • win7下消息提醒过多会导致窗口崩溃:https://github.com/tauri-apps/win7-notifications/pull/69

  • Windows单例模式下重启功能不生效:https://github.com/tauri-apps/tauri/pull/11684


在遇到这些问题时,真的特别让人头大,因为社区几乎没有这些问题的答案,需要我们自己去翻Tauri的源码实现,有些是涉及到操作系统底层的一些API,因此我们必须要去看一些操作系统的API介绍和出入参说明,才能更好的理解Tauri的代码实现意图,才能解决我们碰到的这些问题。


另外,Tauri和操作系统系统相关的源码都是基于Rust来编写的,也为我们的排查和解决增加了不少难度。最后一句名言和读者共勉:纸上得来终觉浅,绝知此事要躬行。