写点什么

得物商家客服从 Electron 迁移到 Tauri 的技术实践

作者:得物技术
  • 2024-12-05
    上海
  • 本文字数:23393 字

    阅读完需:约 77 分钟

一、背景

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


  • 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 开发,涵盖:游戏、工具、聊天、金融等等领域:



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



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


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

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

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

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


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

二、技术调研

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

2.1 Electron 的核心模块

基础架构


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



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



应用构建打包


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



这样,应用程序开发者在开发 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

2.2 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 有着一定的区别。

2.3 选型总结

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



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


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

  • 自定义窗口

  • autoUpdater 自动更新

  • BrowserWindow 窗口创建

  • Notification 消息通知

  • Tray 系统托盘

  • IPC 通信


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


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

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

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


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

三、技术实现

3.1 渲染进程代码迁移

目录结构调整


在聊如何调整 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: <HttpVerb>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 支持,那就可以用。

3.2 主进程代码迁移

自定义操作栏窗口


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



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



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


const { BrowserWindow } = require('electron')const win = new BrowserWindow({ frame: false })
复制代码


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


<div class="handle-container">  <div class="minimize" @click="minimize"></div>  <div class="maximize" @click="maximize"></div>  <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<String>, 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<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {    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/通知。


然而,对于 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<R: crate::Runtime>(self, app: &crate::AppHandle<R>) -> 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<R: crate::Runtime>(self, app: &crate::AppHandle<R>) -> 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(&notification.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!(    "<toast {} {}>        <visual>          <binding template="{}">            {}            <text>{}</text>            <text>{}{}</text>          </binding>        </visual>        <audio src='ms-winsoundevent:Notification.SMS' />      </toast>",    duration,    String::new(),    template_binding,    &notification.icon,    &notification.summary,    notification.subtitle.as_ref().map_or("", AsRef::as_ref),    &notification.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<windows::UI::Notifications::ToastNotification>, result: &Option<windows::core::IInspectable>| { let event: Option< windows::core::Result<windows::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<Arc<dyn Fn(&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 本身也没有支持用户对点击回调的捕获,也需要扩展这个库的点击捕获能力。

3.3 应用构建打包

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
复制代码

3.4 应用签名 &更新

应用更新


对于 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::<Vec<_>>(),      ]      .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 控制台一闪而过:




这些都是因为 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的主版本中,你要做的就是就是升级Tauri到1.7.0升级@tauri-apps/cli到1.6.0。

四、收益 &总结

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


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


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

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

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


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

4.1 社区活跃度还需要提升

直到 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 语言学习成本是有一点的,所以,也给我们日常开发带来了不少挑战。



4.2 Webview2 的问题

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



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

4.3 成熟度和稳定性还不够

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



在遇到这些问题时,真的特别让人头大,因为社区几乎没有这些问题的答案,需要我们自己去翻 Tauri 的源码实现,有些是涉及到操作系统底层的一些 API,因此我们必须要去看一些操作系统的 API 介绍和出入参说明,才能更好的理解 Tauri 的代码实现意图,才能解决我们碰到的这些问题。另外,Tauri 和操作系统系统相关的源码都是基于 Rust 来编写的,也为我们的排查和解决增加了不少难度。最后一句名言和读者共勉:纸上得来终觉浅,绝知此事要躬行。

往期回顾

1.基于 Redis 内核的热 key 统计实现方案|得物技术


2.StarRocks 存算分离在得物的降本增效实践


3.盘点这些年搭建器在用户体验优化的实践|得物技术


4.Java 性能测试利器:JMH 入门与实践|得物技术


5.彩虹桥架构演进之路-负载均衡篇|得物技术


文 / muwoo


关注得物技术,每周更新技术干货


要是觉得文章对你有帮助的话,欢迎评论转发点赞~


未经得物技术许可严禁转载,否则依法追究法律责任。

发布于: 6 小时前阅读数: 5
用户头像

得物技术

关注

得物APP技术部 2019-11-13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
得物商家客服从Electron迁移到Tauri的技术实践_typescript_得物技术_InfoQ写作社区