自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

得物商家客服從Electron遷移到Tauri的技術(shù)實(shí)踐

開發(fā) 前端
每個(gè)Electron的應(yīng)用程序都有一個(gè)主入口文件,它所在的進(jìn)程被稱為 主進(jìn)程(Main Process)。而主進(jìn)程中創(chuàng)建的窗體都有自己運(yùn)行的進(jìn)程,稱為渲染進(jìn)程(Renderer Process)。每個(gè)Electron的應(yīng)用程序有且僅有一個(gè)主進(jìn)程,但可以有多個(gè)渲染進(jìn)程。

一、背景

得物商家客服采用的是桌面端應(yīng)用表現(xiàn)形式,而桌面端應(yīng)用主要架構(gòu)形式就是一套和操作系統(tǒng)交互的“后端” + 一套呈現(xiàn)界面的“前端(渲染層)”。而桌面端技術(shù)又可以根據(jù)渲染層的不同核心劃分為以下幾類:

  • C語(yǔ)言家族:原生開發(fā)、QT
  • Chromium家族:NW、Electron、CEF
  • Webview 家族:Tauri、pywebview、webview_java
  • 自立山頭:Flutter

在2022年5月份左右,得物商家客服開始投入桌面端應(yīng)用業(yè)務(wù),其目標(biāo)是一個(gè)可以適配多操作系統(tǒng)(MacOS、Windows)、快速迭代、富交互的產(chǎn)品。

考慮到以上前提,我們當(dāng)時(shí)可以選擇的框架是Chromium家族或者Webview家族。但是當(dāng)時(shí)對(duì)于Webview來(lái)說(shuō),Tauri 還并不成熟(在 2022年6月才發(fā)布了1.0版本)生態(tài)也不夠豐富。對(duì)于pywebview和webview_java相對(duì)于前端來(lái)說(shuō),一方面門檻較高,另一方面生態(tài)也非常少。所以,在當(dāng)時(shí),我們選擇了Chromium家族中的Electron框架。這是因?yàn)閷?duì)于CEF、Electron、NW來(lái)說(shuō),Electron有著對(duì)前端開發(fā)非常友好的技術(shù)棧,僅使用JavaScript就可以完成和操作系統(tǒng)的交互以及交互視覺(jué)的編寫,另外,Electron的社區(qū)活躍度和生態(tài)相對(duì)于其他兩者也有非常大的優(yōu)勢(shì)。最重要的是:真的很快!

圖片圖片

但是,隨著時(shí)間的推移,直到2024年的今天,商家客服的入駐量和使用用戶越來(lái)越多,用戶的電腦配置也是參差不齊,Electron的弊端開始顯現(xiàn):

  • 性能方面:隨著商家客服入駐數(shù)量的快速增加,現(xiàn)有Electron桌面應(yīng)用在多賬戶+多會(huì)話高并發(fā)場(chǎng)景下,占用內(nèi)存特別大,存在性能瓶頸;
  • 安全方面:Electron在內(nèi)存安全性、跨平臺(tái)攻擊、不受限制的上下文和依賴管理等方面存在一些潛在的弱點(diǎn);
  • 體驗(yàn)方面:現(xiàn)有Electron桌面應(yīng)用包體積大,下載、更新成本較高;
  • 信息集成方面:商家客服目前需要在商家后臺(tái)、商家客服后臺(tái)、商家客服工作臺(tái)3個(gè)系統(tǒng)來(lái)回切換操作,使用成本很高。

我們也發(fā)現(xiàn),之前調(diào)研過(guò)的Tauri作為后起之秀,其生態(tài)和穩(wěn)定性在今天已經(jīng)變得非常出色,我們熟知的以下應(yīng)用都是基于Tauri開發(fā),涵蓋:游戲、工具、聊天、金融等等領(lǐng)域:

  • 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

除此之外,因?yàn)門auri是基于操作系統(tǒng)自帶的Webview + Rust的框架。首先,因?yàn)椴挥么虬粋€(gè)Chromium,所以包體積非常的?。?/p>

圖片圖片

其次Rust作為一門系統(tǒng)級(jí)編程語(yǔ)言,具有以下特點(diǎn):

  • 內(nèi)存安全:Rust通過(guò)所有權(quán)和借用機(jī)制,在編譯時(shí)檢查內(nèi)存訪問(wèn)的安全性,避免了常見(jiàn)的內(nèi)存安全問(wèn)題,如空指針引用、數(shù)據(jù)競(jìng)爭(zhēng)等;
  • 零成本抽象:Rust提供了豐富的抽象機(jī)制,如結(jié)構(gòu)體、枚舉、泛型等,但不引入運(yùn)行時(shí)開銷。這意味著開發(fā)者可以享受高級(jí)語(yǔ)言的便利性,同時(shí)保持接近底層語(yǔ)言的性能;
  • 并發(fā)性能:Rust內(nèi)置支持并發(fā)和異步編程,通過(guò)輕量級(jí)的線程(稱為任務(wù))和異步函數(shù)(稱為異步任務(wù))來(lái)實(shí)現(xiàn)高效的并發(fā)處理。Rust的并發(fā)模型保證了線程安全和數(shù)據(jù)競(jìng)爭(zhēng)的檢查,以及高性能的任務(wù)調(diào)度和通信機(jī)制;
  • 可靠性和可維護(hù)性:Rust強(qiáng)調(diào)代碼的可讀性、可維護(hù)性和可靠性。它鼓勵(lì)使用清晰的命名和良好的代碼結(jié)構(gòu),以及提供豐富的工具和生態(tài)系統(tǒng)來(lái)支持代碼質(zhì)量和測(cè)試覆蓋率;

Rust的這些額外的特性使其成為改善桌面應(yīng)用程序性能和安全性的理想選擇。

二、技術(shù)調(diào)研

要實(shí)現(xiàn)Electron遷移到Tauri,得先分別了解Electron和Tauri的核心功能和架構(gòu)模型,只有了解了這些,才能對(duì)整體的遷移成本做一個(gè)把控。

Electron的核心模塊

基礎(chǔ)架構(gòu)

首先來(lái)看看Electron的基礎(chǔ)架構(gòu)模型:Electron繼承了來(lái)自Chromium的多進(jìn)程架構(gòu),Chromium始于其主進(jìn)程。從主進(jìn)程可以派生出渲染進(jìn)程。渲染進(jìn)程與瀏覽器窗口是一個(gè)意思。主進(jìn)程保存著對(duì)渲染進(jìn)程的引用,并且可以根據(jù)需要?jiǎng)?chuàng)建/刪除渲染器進(jìn)程。

圖片圖片

每個(gè)Electron的應(yīng)用程序都有一個(gè)主入口文件,它所在的進(jìn)程被稱為 主進(jìn)程(Main Process)。而主進(jìn)程中創(chuàng)建的窗體都有自己運(yùn)行的進(jìn)程,稱為渲染進(jìn)程(Renderer Process)。每個(gè)Electron的應(yīng)用程序有且僅有一個(gè)主進(jìn)程,但可以有多個(gè)渲染進(jìn)程。

圖片圖片

應(yīng)用構(gòu)建打包

打包一個(gè)Electron應(yīng)用程序簡(jiǎn)單來(lái)說(shuō)就是通過(guò)構(gòu)建工具創(chuàng)建一個(gè)桌面安裝程序(.dmg、.exe、.deb 等)。在Electron早期作為 Atom 編輯器的一部分時(shí),應(yīng)用程序開發(fā)者通常通過(guò)手動(dòng)編輯Electron二進(jìn)制文件來(lái)為應(yīng)用程序做分發(fā)準(zhǔn)備。隨著時(shí)間的推移,Electron社區(qū)構(gòu)建了豐富的工具生態(tài)系統(tǒng),用于處理Electron應(yīng)用程序的各種分發(fā)任務(wù),其中包括:

  • 應(yīng)用程序打包https://github.com/electron/packager
  • 代碼簽名,例如https://github.com/electron/osx-sign
  • 創(chuàng)建特定平臺(tái)的安裝程序,例如https://github.com/electron/windows-installer或https://github.com/electron-userland/electron-installer-dmg
  • 本地Node.js原生擴(kuò)展模塊重新構(gòu)建https://github.com/electron/rebuild
  • 通用MacOS構(gòu)建https://github.com/electron/universal

這樣,應(yīng)用程序開發(fā)者在開發(fā)Electron應(yīng)用時(shí),為了構(gòu)建出跨平臺(tái)的桌面端應(yīng)用,不得不去了解每個(gè)包的功能并需要將這些功能進(jìn)行組合構(gòu)建,這對(duì)新手而言過(guò)于復(fù)雜,無(wú)疑是勸退的。

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

應(yīng)用簽名&更新

現(xiàn)在絕大多數(shù)的應(yīng)用簽名都采用了簽名狗的應(yīng)用簽名方式,而我們的商家客服桌面端應(yīng)用也是類似,Electron Builder提供了一個(gè)sign的鉤子配置,可以幫助我們來(lái)實(shí)現(xiàn)對(duì)應(yīng)用代碼的簽名:

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

(詳細(xì)的可以直接閱讀electron builder官網(wǎng)介紹,這里只做簡(jiǎn)單說(shuō)明)

對(duì)于應(yīng)用更新而言,我們之前采用的是electron-updater自動(dòng)更新模式:

圖片圖片

如果對(duì)這塊感興趣,可以閱讀我們之前的文章:https://juejin.cn/post/7195447709904404536?searchId=202408131832375B6C2C76DEEE740762EA

Tauri的核心模塊

基礎(chǔ)架構(gòu)

那么,Tauri的基礎(chǔ)架構(gòu)模型是什么樣的?其實(shí)官網(wǎng)對(duì)這塊的介紹比較有限,但是我們可以通過(guò)其源碼倉(cāng)庫(kù)和代碼結(jié)構(gòu)管中窺豹的了解Tauri的核心架構(gòu)模型,為了方便大家理解,我們以得物商家客服桌面端應(yīng)用為模型,簡(jiǎn)單的畫了一個(gè)草圖:

圖片圖片

一些核心模塊的解釋:

WRY

由于Web技術(shù)具有表現(xiàn)力強(qiáng)和開發(fā)成本低的特點(diǎn),與 Electron 和NW等框架類似,Tauri應(yīng)用程序的前端實(shí)現(xiàn)是使用Web技術(shù)棧編寫的。那么Tauri是如何解決Electron/CEF等框架遇到的Chromium內(nèi)核體積過(guò)大的問(wèn)題呢?

也許你會(huì)想,如果每個(gè)應(yīng)用程序都需要打包瀏覽器內(nèi)核以實(shí)現(xiàn)Web頁(yè)面的渲染,那么只要所有應(yīng)用程序共享相同的內(nèi)核,這樣在分發(fā)應(yīng)用程序時(shí)就無(wú)需打包瀏覽器內(nèi)核,只需打包Web頁(yè)面資源。

WRY是Tauri的封裝Webview框架,它在不同的操作系統(tǒng)平臺(tái)上封裝了系統(tǒng)的Webview實(shí)現(xiàn):MacOS上使用WebKit.WKWebview,Windows上使用Webview2,Linux上使用WebKitGTK。這樣,在運(yùn)行Tauri應(yīng)用程序時(shí),直接使用系統(tǒng)的Webview來(lái)渲染應(yīng)用程序的前端展示。

TAO

跨平臺(tái)應(yīng)用窗口創(chuàng)建庫(kù),使用Rust編寫,支持Windows、MacOS、Linux、iOS和Android等所有主要平臺(tái)。該庫(kù)是winit的一個(gè)分支,Tauri根據(jù)自己的需求進(jìn)行了擴(kuò)展,如菜單欄和系統(tǒng)托盤功能。

JS API

這個(gè)API是一個(gè)JS庫(kù),提供調(diào)用Tauri Rust后端的一些API能力,利用這個(gè)庫(kù)可以很方便的完成和Tauri Rust后端的交互以及通信。

看起來(lái)有點(diǎn)復(fù)雜,其實(shí)核心也是分成了主進(jìn)程和渲染進(jìn)程兩個(gè)部分。

  • Tauri的主進(jìn)程使用Rust編寫,Tauri在主進(jìn)程中提供了一些常用的Rust API比如窗口創(chuàng)建、消息提醒... 如果我們覺(jué)得主進(jìn)程提供的API不夠,那么我們可以通過(guò)Tauri的插件體系自行擴(kuò)展。
  • Tauri的渲染進(jìn)程則是運(yùn)行在操作系統(tǒng)的Webview當(dāng)中的,我們可以直接通過(guò)JS + HTML + CSS來(lái)編寫,同時(shí),Tauri會(huì)為渲染進(jìn)程注入一些全局的JS API函數(shù)。比如fs、path、shell等等。

Tauri

這是將所有組件拼到一起的crate。它將運(yùn)行時(shí)、宏、實(shí)用程序和API集成為一款最終產(chǎn)品

應(yīng)用構(gòu)建打包

Tauri提供了一個(gè)CLI工具:https://v1.tauri.app/zh-cn/v1/api/cli/,通過(guò)這個(gè)CLI工具的一個(gè)命令,我們可以直接將應(yīng)用程序打包成目標(biāo)產(chǎn)物:

yarn tauri build

此命令會(huì)將渲染進(jìn)程的Web資源 與 主進(jìn)程的Rust代碼一起嵌入到一個(gè)單獨(dú)的二進(jìn)制文件中。二進(jìn)制文件本身將位于src-tauri/target/release/[應(yīng)用程序名稱],而安裝程序?qū)⑽挥趕rc-tauri/target/release/bundle/。

第一次運(yùn)行此命令需要一些時(shí)間來(lái)收集Rust包并構(gòu)建所有內(nèi)容,但在隨后的運(yùn)行中,它只需要重新構(gòu)建您的應(yīng)用程序代碼,速度要快得多。

應(yīng)用簽名&更新

Tauri的簽名和Electron類似,如果需要自定義簽名鉤子方法,在Tauri中現(xiàn)在也是支持的:

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

后面我們會(huì)詳細(xì)介紹該能力的使用方式。

而對(duì)于更新而言,Tauri則有自己的一套體系:Updater | Tauri Apps這里還是和Electron有著一定的區(qū)別。

選型總結(jié)

通過(guò)上面的架構(gòu)模型對(duì)比,我們可以很直觀的感受到如果要將我們的Electron應(yīng)用遷移到Tauri上,整體的遷移改造工作可以總結(jié)成以下圖所示:

圖片圖片

核心內(nèi)容就變成了以下四部分內(nèi)容:

  • 主進(jìn)程的遷移:而對(duì)于商家客服來(lái)說(shuō),目前主要用的有:自定義窗口autoUpdater自動(dòng)更新BrowserWindow窗口創(chuàng)建Notification消息通知Tray系統(tǒng)托盤IPC通信

而這些API在Tauri中都有對(duì)應(yīng)的實(shí)現(xiàn),所以整體來(lái)看,遷移成本和技術(shù)可行性都是可控的。

  • 渲染進(jìn)程的遷移:渲染進(jìn)程改造相對(duì)而言就少很多了,因?yàn)門auri和Electron都可以直接使用前端框架來(lái)編寫渲染層代碼,所以幾乎可以將之前的前端代碼直接平移過(guò)來(lái)。但是還是有一些小細(xì)節(jié)需要注意,比如IPC通信、JS API的改變、兼容性... 這部分后面也會(huì)詳細(xì)介紹。
  • 應(yīng)用構(gòu)建打包:從之前的Electron構(gòu)建模式改成Tauri構(gòu)建模式,并自動(dòng)化整個(gè)構(gòu)建流程和鏈路。
  • 應(yīng)用簽名&更新:簽名形式不用改,主要需要調(diào)整簽名的配置,實(shí)現(xiàn)對(duì)Tauri應(yīng)用的自動(dòng)簽名和自動(dòng)更新能力。

最終,我們選擇了Tauri對(duì)現(xiàn)有的商家客服桌面端進(jìn)行架構(gòu)優(yōu)化升級(jí)。

三、技術(shù)實(shí)現(xiàn)

渲染進(jìn)程代碼遷移

目錄結(jié)構(gòu)調(diào)整

在聊如何調(diào)整Tauri目錄結(jié)構(gòu)之前,我們需要先來(lái)了解一下之前的Electron應(yīng)用目錄結(jié)構(gòu)設(shè)置,一個(gè)最簡(jiǎn)單的Electron應(yīng)用的目錄結(jié)構(gòu)大致如下:

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

其中文件說(shuō)明如下:

  • index.html:渲染進(jìn)程的入口HTML文件。
  • renderer.js:渲染進(jìn)程的入口JS文件。
  • main.js:主進(jìn)程入口文件
  • preload.js:預(yù)加載腳本文件
  • package.json:包的描述信息,依賴信息

有的時(shí)候你可能需要?jiǎng)澐帜夸泚?lái)編寫不同功能的代碼,但是,不管功能目錄怎么改,最終的渲染進(jìn)程和主進(jìn)程的構(gòu)建產(chǎn)物都是期望符合類似于上面的結(jié)構(gòu)。

圖片圖片

所以,之前得物的商家客服也是類似形式的目錄結(jié)構(gòu):

.
├── app              // 主進(jìn)程代碼目錄
├── renderer-process // 渲染進(jìn)程代碼目錄
├── ...              // 一些其他配置文件,vite 構(gòu)建文件等等
└── package.json

對(duì)于Tauri來(lái)說(shuō),Tauri打包依托于兩個(gè)部分,首先是對(duì)前端頁(yè)面的構(gòu)建,這塊可以根據(jù)業(yè)務(wù)需要和框架選擇(Vue、 React)進(jìn)行構(gòu)建腳本的執(zhí)行。一般前端構(gòu)建的產(chǎn)物都是一個(gè)dist文件包。

然后是Tauri后端程序部分的構(gòu)建,這塊主要是對(duì)Rust代碼進(jìn)行編譯成binary crate。

(Tauri后端的編譯在很大程度上依賴于操作系統(tǒng)原生庫(kù)和工具鏈,因此當(dāng)前無(wú)法進(jìn)行有意義的交叉編譯。所以,在本地編譯我們通常需要準(zhǔn)備一臺(tái)mac和一臺(tái)Windows電腦,以滿足在這兩個(gè)平臺(tái)上的構(gòu)建。)

整體來(lái)看,和Electron是差不多的,這里,我們就直接使用了官方提供的create-tauri-app(https://github.com/tauri-apps/create-tauri-app)腳手架來(lái)創(chuàng)建項(xiàng)目,其目錄結(jié)構(gòu)大致如下:

.
├── src              // 渲染進(jìn)程代碼
├── src-tauri        // Rust 后端代碼
├── ...              // 一些其他配置文件,vite 構(gòu)建文件等等
└── package.json

所以,這里對(duì)渲染進(jìn)程的目錄調(diào)整就很清晰了,直接將我們之前Electron中的renderer-process目錄中的代碼遷移到src目錄中即可。

注意:因?yàn)槲覀儗?duì)渲染進(jìn)程目錄進(jìn)行了調(diào)整,所以對(duì)應(yīng)的打包工具的目錄也需要進(jìn)行調(diào)整。

跨域請(qǐng)求處理

商家客服中會(huì)有一些接口請(qǐng)求,這些接口請(qǐng)求有的是從業(yè)務(wù)中發(fā)起的,有的使用依賴的npm庫(kù)中發(fā)起的請(qǐng)求。但因?yàn)槭强蛻舳艘?,?dāng)從客戶端環(huán)境發(fā)起請(qǐng)求時(shí),請(qǐng)求所攜帶的origin是這樣的:

https://tauri.localhost

那么,就會(huì)遇到一個(gè)我們熟知的一個(gè)前端跨域問(wèn)題。這會(huì)導(dǎo)致如果不在access-ctron-allow-origin中的域名會(huì)被block掉。

圖片圖片

如果有小伙伴對(duì)Electron比較熟悉,可能會(huì)知道在Electron實(shí)現(xiàn)跨域的方案之一是可以關(guān)閉瀏覽器的跨域安全檢測(cè):

const mainWindow = new BrowserWindow({
  webPreferences: {
    webSecurity: false
  }
})

或者在請(qǐng)求返回給瀏覽器之前進(jìn)行攔截,手動(dòng)修改access-ctron-allow-origin讓其支持跨域:

mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
    callback({
      responseHeaders: {
        // 通過(guò)請(qǐng)求源校驗(yàn)
        'Access-Control-Allow-Origin': ['*'],
        ...details.responseHeaders,
      },
    });
  });
}

達(dá)到的效果就像這樣:

圖片圖片

那么Tauri中可以這么做嗎?答案是不行的!

雖然Tauri雖然和Electron進(jìn)程模型很類似,但是本質(zhì)上還是有區(qū)別的,最大的區(qū)別就是Electron中的渲染進(jìn)程是基于Chromium魔改的,他可以在Chromium中植入一些控制器來(lái)修改Chromium的一些默認(rèn)行為。但Tauri完全是基于不同平臺(tái)的內(nèi)置Webview封裝,考慮的兼容性問(wèn)題,并沒(méi)有對(duì)Webview進(jìn)行改造(雖然Windows的Webview2支持 --disable-web-security,但是其他平臺(tái)不行)。所以他的跨域策略是Webview默認(rèn)的行為,無(wú)法調(diào)整。

那么在Tauri中,如何發(fā)起一個(gè)跨域請(qǐng)求了?

其實(shí)社區(qū)也有幾種解決方案,接下來(lái)簡(jiǎn)單介紹一下社區(qū)的方案和問(wèn)題。

使用Tauri官方的http

既然瀏覽器會(huì)因?yàn)榭缬騿?wèn)題block掉請(qǐng)求,那么就繞過(guò)瀏覽器唄,沒(méi)錯(cuò),這也是Tauri官方提供的http模塊設(shè)計(jì)的初衷和原理:https://v1.tauri.app/zh-cn/v1/api/js/http/,其設(shè)計(jì)方案就是通過(guò)JavaScript前端調(diào)用Rust后端來(lái)發(fā)請(qǐng)求,當(dāng)請(qǐng)求完成后再返回給前端結(jié)果。

圖片圖片

問(wèn)題:Tauri http有一套自己的API設(shè)計(jì)和請(qǐng)求規(guī)范,我們必須按照他定義的格式進(jìn)行請(qǐng)求的發(fā)送和接收。對(duì)于新項(xiàng)目來(lái)說(shuō)問(wèn)題不是很大,但對(duì)商家客服來(lái)說(shuō),這樣最大的問(wèn)題是之前的所有的接口請(qǐng)求都得改造成Tauri http的格式,我們很多請(qǐng)求是基于Axios的封裝,改造成本非常大,回歸驗(yàn)證也很困難,而且有很多三方npm包也依賴axios發(fā)請(qǐng)求,這就又增加了改造的成本和后期維護(hù)的成本。

使用axios adapter

既然使用axios改造成本大,那么就寫一個(gè)axios的適配器(adapter)在數(shù)據(jù)請(qǐng)求的時(shí)候不使用瀏覽器原生的xhr發(fā)請(qǐng)求而是使用tauri http來(lái)發(fā)請(qǐng)求,順便對(duì)axios的請(qǐng)求參數(shù)進(jìn)行格式化,處理成Tauri http要求的那種各種。在請(qǐng)求響應(yīng)后也進(jìn)行類似的處理。

圖片圖片

這種解決方案社區(qū)也有一個(gè)庫(kù)提供:https://github.com/persiliao/axios-tauri-api-adapter

問(wèn)題:假設(shè)項(xiàng)目中依賴一個(gè)npm庫(kù),這個(gè)庫(kù)中發(fā)起了一個(gè)axios請(qǐng)求,那么也需要對(duì)這個(gè)庫(kù)的axios進(jìn)行適配器改造。這樣還是解決不了三方依賴使用axios的問(wèn)題。我們還是需要侵入npm包進(jìn)行axios改造。另外,如果其他庫(kù)使用的是xhr或者fetch來(lái)直接發(fā)請(qǐng)求或者,那就又無(wú)解了。

最后,不管使用方案1還是2,都有個(gè)通病,那就是請(qǐng)求都是走的Tauri后端來(lái)發(fā)起的,這也意味著我們將在Webview的devtools中的network看不到任何請(qǐng)求的信息和響應(yīng)的結(jié)果,這對(duì)開發(fā)調(diào)試來(lái)說(shuō)無(wú)疑是非常難以接受的。

社區(qū)對(duì)這個(gè)問(wèn)題也有相關(guān)的咨詢:https://github.com/tauri-apps/tauri/issues/7882,但是官方回復(fù)也是實(shí)現(xiàn)不了:

圖片圖片

那我們是怎么做的呢?對(duì)于Axios來(lái)說(shuō),其在瀏覽器端工作的原理是通過(guò)實(shí)例化window.XMLHttpRequest  后的xhr來(lái)發(fā)起請(qǐng)求,同時(shí)監(jiān)聽(tīng)xhr的onreadystatechange事件來(lái)處理請(qǐng)求的響應(yīng)。然后對(duì)于一些請(qǐng)求頭都是通過(guò)xhr.setRequestHeader這樣的方式設(shè)置到了xhr對(duì)象上。因此,對(duì)于axios、原生XmlHttpRequest請(qǐng)求來(lái)說(shuō),我們就可以重寫XmlHttpRequest中的send、onreadystatechange、setRequestHeader等方法,讓其通過(guò)Tauri的http來(lái)發(fā)請(qǐng)求。

但是對(duì)window.fetch這樣底層未使用XHR的請(qǐng)求來(lái)說(shuō),我們就需要重寫window.fetch。讓其在調(diào)用window.fetch的時(shí)候,調(diào)用xhr.send來(lái)發(fā)請(qǐng)求,這樣便實(shí)現(xiàn)了變相調(diào)用Tauri http的功能。

核心代碼:

class AdapterXMLHTTP extends EventTarget{
    // ...
    // 重寫 send 方法
    async send(data: unknown) {
        // 通過(guò) TauriFetch 來(lái)發(fā)請(qǐng)求
        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 來(lái)發(fā)請(qǐng)求
      const xhr = new XMLHttpRequst()
    })
}


// 重寫 window.XMLHttpRequest
window.XMLHttpRequest = AdapterXMLHTTP;
// 重寫 window.featch
window.fetch = fetchPollify;

那怎么解決devtools沒(méi)法調(diào)試請(qǐng)求的問(wèn)題呢?

為了讓請(qǐng)求日志能出現(xiàn)在瀏覽器的webview devtools network中,我們可能需要開發(fā)一個(gè)類似于chrome plugin的方式來(lái)支持。但是很可惜,在Tauri中,webview是不支持插件開發(fā)的:https://github.com/tauri-apps/tauri/discussions/2685

所以我們只能采用新的方式來(lái)支持,那就是外接devtools。啥意思呢?就是在操作系統(tǒng)網(wǎng)絡(luò)層代理掉網(wǎng)絡(luò)請(qǐng)求,然后輸出到另一個(gè)控制臺(tái)中進(jìn)行展示,原理類似于Charles。

到這里,我們就完成了對(duì)跨域網(wǎng)絡(luò)請(qǐng)求的處理改造工作。核心架構(gòu)圖如下:

關(guān)鍵性API兼容

這里需要注意的是,Tauri使用的是系統(tǒng)自帶的Webview,而Electron則是直接內(nèi)置了Chromium,這里有個(gè)非常大的誤區(qū)在于想當(dāng)然的把Webview類比Chromium以為瀏覽器的API都可以直接使用。這其實(shí)是不對(duì)的,舉個(gè)例子:我們?cè)诎l(fā)送一些消息通知的時(shí)候,可能會(huì)使用HTML5的 Notification Web API:https://developer.mozilla.org/en-US/docs/Web/API/Notification

但是,這個(gè)API是瀏覽器自行實(shí)現(xiàn)的,也就是說(shuō),你在 Electron 中可以這么用,但是,如果你在Tauri中,你會(huì)發(fā)現(xiàn)一個(gè)bug:https://github.com/tauri-apps/tauri/issues/3698,這個(gè)bug的大概含義就是Tauri中的Notification不會(huì)觸發(fā)click點(diǎn)擊事件。這個(gè)bug至今還未解決。究其原因:

Tauri依賴的操作系統(tǒng)webview并沒(méi)有實(shí)現(xiàn)對(duì)Notification 的支持,webview本身希望宿主應(yīng)用自行實(shí)現(xiàn)對(duì)Notification的實(shí)現(xiàn),所以Tauri就重寫了JS的Notification API,當(dāng)你在調(diào)用window  Notification的時(shí)候,實(shí)際上你和Rust進(jìn)程完成了一次通信,調(diào)用的還是tauri::Notification模塊。

在Tauri源碼里面,是這樣實(shí)現(xiàn)的:

// 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 后端通信,調(diào)用 Rust 發(fā)送系統(tǒng)通知
    return window.__TAURI_INVOKE__('tauri', {
      __tauriModule: 'Notification',
      message: {
        cmd: 'notification',
        options:
          typeof options === 'string'
            ? {
              title: options
            }
            : options
      }
    })
  }
  //  這里便是對(duì) Notification 的重寫實(shí)現(xiàn)
  window.Notification = function (title, options) {
    const opts = options || {}
    sendNotification(
      Object.assign(opts, {
        title: title
      })
    )
  }

除此之外,Tauri還分別實(shí)現(xiàn)了:

  • DOM上標(biāo)簽的點(diǎn)擊跳轉(zhuǎn)功能,使用內(nèi)置的Tauri API進(jìn)行打開webview。
  • 差異化操作系統(tǒng)原生窗口的拖拽和最大化事件:在Windows和Linux上,當(dāng)鼠標(biāo)按下時(shí)拖動(dòng),雙擊時(shí)最大化;而在MacOS上,最大化應(yīng)該在鼠標(biāo)抬起時(shí)發(fā)生,如果雙擊后鼠標(biāo)移動(dòng),應(yīng)該取消最大化。
  • window.alert
  • window.confirm
  • window.print(Macos)

所以,我們?cè)趯?duì)商家客服從Electron遷移到Tauri的過(guò)程中,還需要對(duì)這些關(guān)鍵性API進(jìn)行兼容性測(cè)試和回歸。一旦發(fā)現(xiàn)相關(guān)API不符合預(yù)期,我們需要及時(shí)調(diào)整業(yè)務(wù)策略或者給嘗試進(jìn)行hack。

(這里賣個(gè)關(guān)子,雖然Tauri不支持對(duì)Notification的點(diǎn)擊事件回調(diào),那么我們是怎么讓他支持的呢?在下一節(jié)主進(jìn)程代碼遷移中我們會(huì)詳細(xì)介紹。)

兼容性回歸

對(duì)于樣式兼容性來(lái)說(shuō),因?yàn)镋lectron在不同操作系統(tǒng)內(nèi)都集成了Chromium所以我們完全不用擔(dān)心樣式兼容性的問(wèn)題。但是對(duì)于Tauri來(lái)說(shuō),因?yàn)椴煌僮飨到y(tǒng)使用了不同的Webview,所以在樣式上,我們還是需要注意不同操作系統(tǒng)下的差異性,比如:以下分別是Linux和Windows渲染Element-Plus的界面:

圖片圖片

圖片圖片

可以看到在按鈕大小、文字對(duì)齊等樣式上面還是存在著不小的差距。

除了上述問(wèn)題,如果你需要兼容Linux系統(tǒng),那么還有webkitgtk在非整數(shù)倍縮放下的bug,應(yīng)該是陳年老問(wèn)題了。當(dāng)然,這些問(wèn)題都是上游webkitgtk的“鍋”。

所以,社區(qū)也有關(guān)于討論Tauri是否有可能在不同平臺(tái)上使用同一個(gè)webview的可能性的討論:https://github.com/tauri-apps/tauri/discussions/4591。官方是期待能有Mac版本的Webview發(fā)布,不過(guò)大概率來(lái)看不太現(xiàn)實(shí),一方面是因?yàn)椋何④洓Q定不開源 Webview2的Mac和Linux版本(https://mp.weixin.qq.com/s/p6pdNI3_di7oBkv4ugDIdA),另一方面是如果要使用統(tǒng)一的webview那就又回到了Electron。

除了樣式兼容性外,對(duì)于JS代碼的兼容性也需要留意Tauri在Windows上使用的是Webview2而Webview2本身就是基于Chromium的,所以代碼兼容性倒還好,但是在MacOS 上使用的就是WebKit.WKWebview,Safari就是基于他,所以到這里,我想你也明白了,這就又回到了前端處理不同瀏覽器兼容性的問(wèn)題上來(lái)了。所以這里溫馨提示一下:構(gòu)建時(shí)前端代碼需要進(jìn)行polyfill。

對(duì)于Electron應(yīng)用的用戶來(lái)說(shuō),可能沒(méi)有這樣的煩惱,最新的API只要Chrome支持,那就可以用。

主進(jìn)程代碼遷移

自定義操作欄窗口

默認(rèn)情況,在構(gòu)建窗口的時(shí)候,會(huì)使用系統(tǒng)自帶的原生窗口樣式,比如在MacOS下的樣式:

在有些情況下,操作系統(tǒng)的原生窗口并不能符合我們的一些視覺(jué)和交互需求。所以,在創(chuàng)建桌面應(yīng)用的時(shí)候,有時(shí)候我們希望能完全掌控窗口的樣式,而隱藏掉系統(tǒng)提供的窗口邊框和標(biāo)題欄等。這個(gè)時(shí)候就需要用到自定義操作欄窗口。比如在Windows中,我們希望在右上角有一排自定義的操作欄,就像是這樣:

商家客服桌面端的窗口就是一個(gè)無(wú)邊框的自定義操作欄的窗口,在Electron中,我們可以這樣操作快速創(chuàng)建一個(gè)無(wú)邊框窗口:

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

然后在渲染進(jìn)程中,自己 “畫一個(gè)標(biāo)題欄”:

<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中,要實(shí)現(xiàn)自定窗口首先需要在窗口創(chuàng)建的時(shí)候設(shè)置decoration無(wú)裝飾樣式,比如這樣:(也可以在tauri.config.json中設(shè)置,道理是一樣的)

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類似,自己畫一個(gè)控制欄,詳細(xì)的代碼可以參考這里: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>

單例模式

通過(guò)使用窗口單例模式,可以確保應(yīng)用程序在用戶嘗試多次打開時(shí)只會(huì)有一個(gè)主窗口實(shí)例,從而提高用戶體驗(yàn)并避免不必要的資源占用。在Electron中可以很容易做到這一點(diǎn):

app.on('second-instance', (event, commandLine, workingDirectory) => {
  // 當(dāng)運(yùn)行第二個(gè)實(shí)例時(shí),將會(huì)聚焦到myWindow這個(gè)窗口
  if (myWindow) {
    mainWindow.show()
    if (myWindow.isMinimized()) myWindow.restore()
    myWindow.focus()
  }
})

但是,在Tauri中,我需要引入一個(gè)單例插件才可以:

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這個(gè)Crate中的CreateMutexW API來(lái)創(chuàng)建一個(gè)互斥體,確保只有一個(gè)實(shí)例可以運(yùn)行,并在用戶嘗試啟動(dòng)多個(gè)實(shí)例時(shí),聚焦于已經(jīng)存在的實(shí)例并傳遞數(shù)據(jù),簡(jiǎn)化后的代碼大致如下:

pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
    plugin::Builder::new("single-instance")
        .setup(|app| {
            // ...
            // 創(chuàng)建互斥體
            let hmutex = unsafe { 
                    CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr())
                };
            // 如果 GetLastError 返回 ERROR_ALREADY_EXISTS,則表示已有實(shí)例在運(yùn)行。
            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
                unsafe {
                    // 找到已存在窗口的句柄
                    let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());


                    if hwnd != 0 {
                        // ...
                        // 通過(guò) SendMessageW 發(fā)送數(shù)據(jù)給該窗口
                        SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _);
                        // 最后退出當(dāng)前應(yīng)用
                        app.exit(0);
                    }
                }
            }
            // ...
            Ok(())
        })
        .build()
}

(注意:這里有坑,如果你的應(yīng)用需要實(shí)現(xiàn)一個(gè)重新啟動(dòng)功能,那么在單例模式下將不會(huì)生效,核心原因是因?yàn)閼?yīng)用重啟的邏輯是先打開一個(gè)新的實(shí)例再關(guān)閉舊的運(yùn)行實(shí)例。而打開新的實(shí)例在單例模式下就被阻止了,這塊的詳細(xì)原因和解決方案我們已經(jīng)給Tauri提了PR:https://github.com/tauri-apps/tauri/pull/11684)

系統(tǒng)消息通知能力

消息通知是商家客服桌面端應(yīng)用必不可少的能力,消息通知能力一般可以分為以下兩種:

  • 觸達(dá)操作系統(tǒng)的消息通知
  • 用戶點(diǎn)擊消息后的回調(diào)事件

前面我們有提到,在Electron中,我們需要顯示來(lái)自渲染進(jìn)程的通知,那么可以直接使用HTML5的Web API來(lái)發(fā)送一條系統(tǒng)消息通知:

function notifyMe() {
  if (!("Notification" in window)) {
    // 檢查瀏覽器是否支持通知
    alert("當(dāng)前瀏覽器不支持桌面通知");
  } else if (Notification.permission === "granted") {
    // 檢查是否已授予通知權(quán)限;如果是的話,創(chuàng)建一個(gè)通知
    const notification = new Notification("你好!");
    // …
  } else if (Notification.permission !== "denied") {
    // 我們需要征求用戶的許可
    Notification.requestPermission().then((permission) => {
      // 如果用戶接受,我們就創(chuàng)建一個(gè)通知
      if (permission === "granted") {
        const notification = new Notification("你好!");
        // …
      }
    });
  }
  // 最后,如果用戶拒絕了通知,并且你想尊重用戶的選擇,則無(wú)需再打擾他們
}

如果我們需要為消息通知添加點(diǎn)擊回調(diào)事件,那么我們可以這么寫:

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

當(dāng)然,Electron也提供了主進(jìn)程使用的API,更多的能力可以直接參考Electron的官方文檔:https://www.electronjs.org/zh/docs/latest/api/%E9%80%9A%E7%9F%A5。

然而,對(duì)于Tauri來(lái)說(shuō),只實(shí)現(xiàn)了第1個(gè)能力,也就是消息觸達(dá)。Tauri本身不支持點(diǎn)擊回調(diào)的功能,這就導(dǎo)致了用戶發(fā)來(lái)了一個(gè)消息,但是業(yè)務(wù)無(wú)法感知客服點(diǎn)擊消息的事件。而且原生的Web API也是Tauri自己寫的,原理還是調(diào)用了Rust的通知能力。接下來(lái),我也會(huì)詳細(xì)介紹一下我們是如何擴(kuò)展消息點(diǎn)擊回調(diào)能力的。

Tauri在Rust層,我們可以通過(guò)下面這段代碼來(lái)調(diào)用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 可以調(diào)用
Notification::new(&app.config().tauri.bundle.identifier)
  .title("New message")
  .body("You've got a new message.")
  .show();


// 兼容 win7 的調(diào)用形式
Notification::new(&app.config().tauri.bundle.identifier)
  .title("Tauri")
  .body("Tauri is awesome!")
  .notify(&app.handle())
  .unwrap();


// run the app
app.run(|_app_handle, _event| {});

Tauri的Notification Rust實(shí)現(xiàn)源碼位置在:https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/notification.rs這個(gè)文件中,其中看一下show函數(shù)的實(shí)現(xiàn):

pub fn show(self) -> crate::api::Result<()> {
    #[cfg(feature = "dox")]
    return Ok(());
    #[cfg(not(feature = "dox"))]
    {
      // 使用 notify_rust 構(gòu)造 notification 實(shí)例
      let mut notification = notify_rust::Notification::new();
      // 設(shè)置消息通知的 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函數(shù)非win7環(huán)境下show函數(shù)調(diào)用的是notify_rust這個(gè)庫(kù),而在win7環(huán)境下調(diào)用的是win7_notifications這個(gè)庫(kù)。而notify_rust這個(gè)庫(kù),本身確實(shí)未完成實(shí)現(xiàn)對(duì)MacOS和Windows點(diǎn)擊回調(diào)事件。

所以我們需要自定義一個(gè)Notification的Tauri插件,實(shí)現(xiàn)對(duì)點(diǎn)擊回調(diào)的能力。(因?yàn)槠颍@里只介紹一些核心的實(shí)現(xiàn)邏輯)

MacOS 支持消息點(diǎn)擊回調(diào)能力

notify_rust在Mac上實(shí)現(xiàn)消息通知是基于Mac_notification_sys這個(gè)庫(kù)的,這個(gè)庫(kù)本身是支持對(duì)點(diǎn)擊action的response,只是notify_rust沒(méi)有處理而已,所以我們可以為notify_rust增加對(duì)Mac上點(diǎn)擊回調(diào)的處理能力:

#[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,
  };
 // 發(fā)通過(guò) 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()
  {
    // 響應(yīng)點(diǎn)擊事件,回調(diào)前端的 handle 函數(shù)
    Ok(response) => match response {
      NotificationResponse::ActionButton(id) => {
        if action_name.eq(&id) {
          let js = tauri::api::ipc::format_callback(handle, &id)
              .expect("點(diǎn)擊 action 報(bào)錯(cuò)");
           window_.eval(js.as_str());
        };
      }
      NotificationResponse::Click => {
        let data = &sid;
        let js = tauri::api::ipc::format_callback(handle, &data)
            .expect("消息點(diǎn)擊報(bào)錯(cuò)");
         window_.eval(js.as_str());
      }
      _ => {}
    },
    Err(err) => println!("Error handling notification {}", err),
  }
}

Win 10上支持消息點(diǎn)擊回調(diào)能力

在Windows 10操作系統(tǒng)中,notify_rust則是通過(guò)winrt_notification這個(gè)Crate來(lái)發(fā)送消息通知,winrt_notification 則是調(diào)用的windows這個(gè)crate來(lái)實(shí)現(xiàn)消息通知,windows這個(gè)crate的官方描述是:為Rust開發(fā)人員提供了一種自然和習(xí)慣的方式來(lái)調(diào)用Windows API。這里,主要會(huì)用到以下幾個(gè)方法:

  • windows::UI::Notifications::ToastNotification::CreateToastNotification:這個(gè)函數(shù)的作用是根據(jù)指定的參數(shù)創(chuàng)建一個(gè)Toast通知對(duì)象,可以設(shè)置通知的標(biāo)題、文本內(nèi)容、圖標(biāo)、音頻等屬性,并可以指定通知被點(diǎn)擊時(shí)的響應(yīng)行為。通過(guò)調(diào)用這個(gè)函數(shù),可以在Windows應(yīng)用程序中創(chuàng)建并顯示自定義的Toast通知,向用戶展示相關(guān)信息。
  • windows::Data::Xml::Dom::XmlDocument:這是一個(gè)用于在Windows應(yīng)用程序中創(chuàng)建和處理XML文檔的類。它主要提供了一種方便的方式來(lái)創(chuàng)建、解析和操作XML數(shù)據(jù)。
  • windows::UI::Notifications::ToastNotificationManager::CreateToastNotifierWithId:通過(guò)調(diào)用CreateToastNotifierWithId函數(shù),可以創(chuàng)建一個(gè)Toast通知管理器對(duì)象,并指定一個(gè)唯一的標(biāo)識(shí)符。這個(gè)標(biāo)識(shí)符通常用于標(biāo)識(shí)應(yīng)用程序或者特定的通知渠道,以確保通知的正確分發(fā)和管理。創(chuàng)建了Toast通知管理器之后,就可以使用它來(lái)生成和發(fā)送Toast通知,向用戶展示相關(guān)信息,并且可以根據(jù)標(biāo)識(shí)符進(jìn)行個(gè)性化的通知管理。
  • windows::Foundation::TypedEventHandler:這是Windows Runtime API中的一個(gè)委托(delegate)類型。在Windows Runtime中,委托類型用于表示事件處理程序,允許開發(fā)人員編寫事件處理邏輯并將其附加到特定的事件上。

所以,要想在> win7的操作系統(tǒng)中顯示消息同時(shí)的主要流程大致是:

  • 通過(guò)XmlDocument來(lái)創(chuàng)建一個(gè)Xml消息通知模板。
  • 然后將創(chuàng)建好的Xml消息模板作為CreateToastNotification的入?yún)?lái)創(chuàng)建一個(gè)toast通知。
  • 最后調(diào)用CreateToastNotifierWithId來(lái)創(chuàng)建一個(gè)Toast通知管理器對(duì)象,創(chuàng)建成功后顯示toast。
  • 通過(guò)TypedEventHandler監(jiān)聽(tīng)用戶點(diǎn)擊事件并完成回調(diào)觸發(fā)

但是winrt_notification這個(gè)庫(kù),只完成了1-3步驟,所以我們需要手動(dòng)實(shí)現(xiàn)步驟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();
  // 設(shè)置消息持續(xù)狀態(tài),支持 short 和 long
  // short 就是默認(rèn) 6s
  // long 是常駐消息
  let duration = match notification.timeout {
    notify_rust::Timeout::Default => "duratinotallow=\"short\"",
    notify_rust::Timeout::Never => "duratinotallow=\"long\"",
    notify_rust::Timeout::Milliseconds(t) => {
      if t >= 25000 {
        "duratinotallow=\"long\""
      } else {
        "duratinotallow=\"short\""
      }
    }
  };
  
  // 創(chuàng)建消息模版 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,
    ?ification.icon,
    ?ification.summary,
    notification.subtitle.as_ref().map_or("", AsRef::as_ref),
    ?ification.body,
  ))) {
    println!("Error creating windows toast xml {}", err);
    return;
  };


  // 根據(jù) xml 創(chuàng)建 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;
        }
      };
  // 創(chuàng)建消息點(diǎn)擊監(jiān)聽(tīng)捕獲
  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("消息點(diǎn)擊報(bào)錯(cuò)");
        } else {
          let data = &sid;
          js = tauri::api::ipc::format_callback(handle, &data)
              .expect("消息點(diǎn)擊報(bào)錯(cuò)");
        }
        let _ = window_.eval(js.as_str());
      };
      Ok(())
    },
  );


  // 通過(guò)消息管理器發(fā)送消息
  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上支持消息通知點(diǎn)擊回調(diào)能力

在Windows 7中,Tauri調(diào)用的是win7_notifications這個(gè)庫(kù),這個(gè)庫(kù)本身也沒(méi)有實(shí)現(xiàn)對(duì)消息點(diǎn)擊的回調(diào)處理,我們需要擴(kuò)展win7_notifications的能力來(lái)實(shí)現(xiàn)對(duì)消息通知的回調(diào)事件。我們希望這個(gè)庫(kù)可以這樣調(diào)用:

win7_notify::Notification::new()
    .appname(&app_name)
    .body(&body)
    .summary(&title)
    .timeout(duration)
    .click_event(move |str| {
      // 用戶自定義的參數(shù)
      let data = &sid;
      // 觸發(fā)前端的回調(diào)能力
      let js = tauri::api::ipc::format_callback(handle, &data)
          .expect("消息點(diǎn)擊報(bào)錯(cuò)");
      let _ = window_.eval(js.as_str());
    })
    .show();

而我們要做的,就是為win7_notify這個(gè)庫(kù)中的Notification結(jié)構(gòu)體增加一個(gè)click_event函數(shù),這個(gè)函數(shù)支持傳入一個(gè)閉包,這個(gè)閉包在點(diǎn)擊消息通知的時(shí)候執(zhí)行。

pub struct Notification {
    // ...
    // 添加 click_event 屬性
    pub click_event: Option<Arc<dyn Fn(&str) + Send>>,
}


impl Notification {
    // ...
    // 添加 click_event 事件注冊(cè)
    pub fn click_event<F: Fn(&str) + Send + 'static>(&mut self, func: F) -> &mut Notification {
        // 將事件綁定到 Notification 中
        self.click_event = Some(Arc::new(func));
        self
    }
    // 支持對(duì) click_event 的調(diào)用
    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 {
       // ....
       // 增加對(duì)點(diǎn)擊事件的調(diào)用
       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 增加點(diǎn)擊參數(shù)
            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)
        }
    }
    
}

總結(jié):

圖片圖片

  • Tauri本身不支持Notification的點(diǎn)擊事件,需要自行實(shí)現(xiàn)。
  • 需要對(duì)不同操作系統(tǒng)分別實(shí)現(xiàn)點(diǎn)擊回調(diào)能力。
  • MacOS  mac_notification_sys庫(kù)本來(lái)就有點(diǎn)擊回調(diào),只是Tauri沒(méi)有捕獲處理,需要自定義捕獲處理邏輯就好了。
  • Windows > 7中,通過(guò)windows這個(gè)crate,來(lái)完成調(diào)用Windows操作系統(tǒng)API的能力,但是winrt_notification這個(gè)庫(kù)并沒(méi)有實(shí)現(xiàn)對(duì)Windows API回調(diào)點(diǎn)擊的捕獲處理,所以需要重寫winrt_notification這個(gè)庫(kù)。
  • Windows 7中,消息通知其實(shí)是通過(guò)繪制窗口和監(jiān)聽(tīng)鼠標(biāo)點(diǎn)擊來(lái)觸發(fā)的,但是win7_notify本身也沒(méi)有支持用戶對(duì)點(diǎn)擊回調(diào)的捕獲,也需要擴(kuò)展這個(gè)庫(kù)的點(diǎn)擊捕獲能力。

應(yīng)用構(gòu)建打包

Windows 10

Tauri 1.3版本之前,應(yīng)用程序在Windows上使用的是WiX(Windows Installer)Toolset v3工具進(jìn)行構(gòu)建,構(gòu)建產(chǎn)物是Microsoft安裝程序(.msi文件)。1.3之后,使用的是NSIS來(lái)構(gòu)建應(yīng)用的xxx-setup.exe安裝包。

Tauri CLI默認(rèn)情況下使用當(dāng)前編譯機(jī)器的體系結(jié)構(gòu)來(lái)編譯可執(zhí)行文件。假設(shè)當(dāng)前是在64位計(jì)算機(jī)上開發(fā),CLI將生成64位應(yīng)用程序。如果需要支持32位計(jì)算機(jī),可以使用--target標(biāo)志使用不同的Rust目標(biāo)編譯應(yīng)用程序:

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

為了支持不同架構(gòu)的編譯,需要為Rust添加對(duì)應(yīng)的環(huán)境支持,比如:

rustup target add i686-pc-windows-msvc

其次,需要為構(gòu)建增加不同的環(huán)境變量,以便為了在不同的環(huán)境進(jìn)行代碼測(cè)試,對(duì)應(yīng)到package.json中的構(gòu)建代碼:

{
  "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參數(shù)指定了構(gòu)建的配置文件路徑,Tauri會(huì)和src-tauri中的tarui.conf.json文件進(jìn)行合并。除此之外,還可以通過(guò)tarui.{{platform}}.conf.json的形式指定不同平臺(tái)的獨(dú)特配置,優(yōu)先級(jí)關(guān)系:

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

Windows 7

Webview 2

Tauri在Windows 7上運(yùn)行有兩個(gè)東西需要注意,一個(gè)是Tauri的前端跨平臺(tái)在Windows上依托于Webview2但是Windows 7中并不會(huì)內(nèi)置Webview2因此我們需要在構(gòu)建時(shí)指明引入Webview的方式:

圖片圖片

綜合比較下來(lái),embedBootstrapper目前是比較好的方案,一方面可以減少安裝包體積,一方面減少不必要的靜態(tài)資源下載。

Windows 7一些特性

在Tauri中,會(huì)通過(guò)"Windows7-compat"來(lái)構(gòu)建一些Win7特有的環(huán)境代碼,比如:

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

在Tauri文檔中也有相關(guān)介紹,主要是在使用Notification的時(shí)候,需要加入Windows7-compat特性。不過(guò),因?yàn)?Tauri 對(duì)Notification的點(diǎn)擊事件回調(diào)是不支持,所以我重寫了Tauri的所有Notification模塊,已經(jīng)內(nèi)置了Windows7-compat能力,因此可以不用設(shè)置了。

MacOS

MacOS操作系統(tǒng)也有M1和Intel的區(qū)分,所以為了可以構(gòu)建出兼容兩個(gè)版本的產(chǎn)物,我們需要使用universal-apple-darwin模式來(lái)編譯:

{  "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

應(yīng)用簽名&更新

應(yīng)用更新

對(duì)于Tauri來(lái)說(shuō),應(yīng)用更新的詳細(xì)配置步驟可以直接看官網(wǎng)的介紹:https://tauri.app/zh-cn/v1/guides/distribution/updater/。這里為了方便大家理解,簡(jiǎn)單畫了個(gè)更新流程圖:

圖片圖片

核心流程如下:

  • 對(duì)于需要更新的應(yīng)用,可以在渲染進(jìn)程通過(guò)JS調(diào)用 installUpdate() API
  • Tauri內(nèi)部會(huì)發(fā)送一個(gè)更新協(xié)議事件:
pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
br
  • Tauri主進(jìn)程Updater模塊會(huì)響應(yīng)這個(gè)事件,執(zhí)行download_and_install函數(shù)通過(guò)tauri.config.json中配置的endpoints來(lái)尋找下載地址下載endpoints服務(wù)器上的zip包內(nèi)容并解壓存儲(chǔ)到一個(gè)臨時(shí)文件夾,Windows中大概位置在C:\Users\admin\AppData\Local\Temp這里。然后通過(guò)PowerShell來(lái)執(zhí)行下載的setup.exe文件:["-NoProfile", "-WindowStyle", "Hidden", "Start-Process"],這些參數(shù)告訴PowerShell在后臺(tái)運(yùn)行,不顯示任何窗口,并啟動(dòng)一個(gè)新的進(jìn)程。
if found_path.extension() == Some(OsStr::new("exe")) {
      // 創(chuàng)建一個(gè)新的 OsString,并將 found_path 包裹在引號(hào)中,以便在 PowerShell 中正確處理路徑
      let mut installer_path = std::ffi::OsString::new();
      installer_path.push("\"");
      installer_path.push(&found_path);
      installer_path.push("\"");
      
      // 構(gòu)造安裝程序參數(shù)
      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();


      // 創(chuàng)建一個(gè)新的命令,指向 PowerShell 的路徑。
      // 使用 Start-Process 命令來(lái)啟動(dòng)安裝程序,
      // 并設(shè)置 -NoProfile 和 -WindowStyle Hidden 選項(xiàng),
      // 以確保 PowerShell 不會(huì)加載用戶配置文件,并且窗口保持隱藏
      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() 方法啟動(dòng)命令,如果失敗,則輸出錯(cuò)誤信息。
      cmd
        .spawn()
        .expect("Running NSIS installer from powershell has failed to start");


      exit(0);
    }

  • 在通過(guò)PowerShell啟動(dòng)應(yīng)用安裝程序的時(shí)候,就會(huì)使用到tauri.config.json中配置的updater.windows.installMode功能:"basicUi":指定安裝過(guò)程中包括最終對(duì)話框在內(nèi)的基本用戶界面,需要用戶手動(dòng)點(diǎn)擊下一步。"quiet":安靜模式表示無(wú)需用戶交互。如果安裝程序需要管理員權(quán)限(WiX),則需要管理員權(quán)限。"passive":會(huì)顯示一個(gè)只有安裝進(jìn)度條的UI,安裝過(guò)程用戶無(wú)需參與。

需要注意的是:如果以為更新是增量更新,不會(huì)卸載之前已經(jīng)安裝好的應(yīng)用程序只更新需要變更的部分。其實(shí)是不對(duì)的,整個(gè)安裝過(guò)程可以理解為Tauri在后臺(tái)幫你重新下載了一個(gè)最新的安裝包,然后幫你重新安裝了一下。

總結(jié):更新的核心原理就是通過(guò)使用Windows的PowerShell來(lái)對(duì)下載后的安裝包進(jìn)行open。然后由安裝包進(jìn)行安裝。

為什么我要花這么大的篇幅來(lái)介紹 Tauri 的更新原理呢?

這是因?yàn)槲覀冊(cè)诟碌倪^(guò)程中碰到了兩個(gè)比較大的問(wèn)題:

  • 通過(guò)cmd調(diào)用PowerShell來(lái)安裝時(shí),會(huì)在安裝過(guò)程中出現(xiàn)一個(gè)藍(lán)色的PowerShell控制臺(tái)一閃而過(guò):

圖片圖片

  • 在部分開啟了病毒防護(hù)的Windows電腦上,使用PowerShell來(lái)執(zhí)行對(duì)安裝包的打開,會(huì)報(bào)錯(cuò):Permission Denied,導(dǎo)致安裝更新失?。篽ttps://github.com/rust-lang/rustlings/issues/604

這些都是因?yàn)門auri直接使用 Powershell的問(wèn)題,那需要怎么改呢?很簡(jiǎn)單,那就是使用Windows操作系統(tǒng)提供的ShellExecuteW來(lái)運(yùn)行安裝程序,核心代碼如下:

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

但是這塊是Tauri的源碼,我們沒(méi)法直接修改,但這個(gè)問(wèn)題的解決方法我們已經(jīng)給Tauri提了PR并已合入到官方的1.6.8正式版本當(dāng)中:https://github.com/tauri-apps/tauri/pull/9818

所以,你要做的就是確保Tauri升級(jí)到v1.6.8及以后版本。

應(yīng)用簽名

Tauri應(yīng)用程序簽名可以分成2個(gè)部分,第一部分是應(yīng)用程序簽名,第二部分是安裝包程序簽名,官網(wǎng)上介紹的簽名方法需要配置tauri.config.json中如下字段:

"windows": {
    // 簽名指紋
    "certificateThumbprint": "xxx",
    // 簽名算法
    "digestAlgorithm": "sha256",
    // 時(shí)間戳
    "timestampUrl": "http://timestamp.comodoca.com"
}

如果你按照官方的步驟來(lái)進(jìn)行簽名:https://v1.tauri.app/zh-cn/v1/guides/distribution/sign-windows/,很快就會(huì)發(fā)現(xiàn)問(wèn)題所在:官網(wǎng)中簽名有一個(gè)重要的步驟就是導(dǎo)出一個(gè).pfx文件,但是現(xiàn)在業(yè)界簽名工具基本上都是采用簽名狗的方式進(jìn)行的,這是一個(gè)類似于U盾簽名工具,需要插入電腦中才可以進(jìn)行簽名,不支持直接導(dǎo)出.pfx格式的文件:

圖片圖片

所以我們需要額外處理一下:

簽名狗支持導(dǎo)出一個(gè).cert證書,可以查看到證書的指紋:

圖片圖片

這里證書的指紋對(duì)應(yīng)的就是certificateThumbprint字段。

然后需要插入我們?cè)诤灻麢C(jī)構(gòu)購(gòu)買的USB key。這樣,在構(gòu)建的時(shí)候,就會(huì)提示讓我們輸入密碼:

圖片圖片

到這里就可以完成對(duì)應(yīng)用程序的簽名。

不過(guò)對(duì)于我們而言,USB key簽名狗是整個(gè)公司共享的,通常不在前端開發(fā)手里(尤其是異地辦公)。一種做法是在Tauri構(gòu)建的過(guò)程中,對(duì)于需要簽名的軟件提供一個(gè)signCommand命令鉤子,并為這個(gè)命令傳入文件的路徑,然后交由開發(fā)者對(duì)文件進(jìn)行自行簽名(比如上傳到擁有簽名工具的電腦,上傳上去后,遠(yuǎn)程進(jìn)行簽名,簽名完成再下載)。所以這就需要讓Tauri將簽名功能暴露出來(lái),讓我們自行進(jìn)行簽名,比如這樣:

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

該命令中包含一個(gè)%1,它只是二進(jìn)制路徑的占位符,Tauri在構(gòu)建的時(shí)候會(huì)將需要簽名的文件路徑替換掉%1。

圖片圖片

這個(gè)功能官網(wǎng)上還沒(méi)有更新相關(guān)的介紹,所以你可能看不到這塊的使用方式,因?yàn)橐彩俏覀冏罱峤坏腜R:https://github.com/tauri-apps/tauri/pull/9902。不過(guò)目前,這個(gè)PR已經(jīng)被合入Tauri的主版本中,你要做的就是就是升級(jí)Tauri到1.7.0升級(jí)@tauri-apps/cli到1.6.0。

四、收益&總結(jié)

經(jīng)過(guò)我們的不懈努力(不斷地填坑)到目前,得物商家客服Tauri版本終于如期上線,基于Tauri遷移帶來(lái)的收益如下:

整體性能測(cè)試相比之前的Electron應(yīng)用有比較明顯的提升:

  • 包體積7M,Electron 80M下降91.25%。
  • 平均內(nèi)存占用249M Electron 497M下降49.9%。
  • 平均CPU占用百分比20%,Electron 63.5%下降 63.19%。

整體在性能體驗(yàn)上有一個(gè)非常顯著改善。但是,這里也暴露出使用Tauri的一些問(wèn)題。

責(zé)任編輯:武曉燕 來(lái)源: 得物技術(shù)
相關(guān)推薦

2023-02-01 18:33:44

得物商家客服

2023-11-27 18:38:57

得物商家測(cè)試

2022-12-02 18:45:06

SOP機(jī)器人技術(shù)

2022-10-20 14:35:48

用戶畫像離線

2025-03-20 10:47:15

2022-12-09 18:58:10

2023-03-30 18:39:36

2022-08-27 21:31:04

Tauri框架二進(jìn)制

2023-12-27 18:46:05

云原生容器技術(shù)

2023-02-06 18:35:05

架構(gòu)探測(cè)技術(shù)

2022-06-03 09:30:31

店鋪W3C體系渲染

2025-03-13 06:48:22

2023-10-09 18:35:37

得物Redis架構(gòu)

2022-05-12 11:41:16

開發(fā)框架程序

2023-02-08 18:33:49

SRE探索業(yè)務(wù)

2022-12-14 18:40:04

得物染色環(huán)境

2010-08-20 11:18:49

Exchange Se

2023-07-07 19:26:50

自建DTS平臺(tái)

2013-03-19 09:56:36

云計(jì)算遷移

2023-07-19 22:17:21

Android資源優(yōu)化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)