得物商家客服從Electron遷移到Tauri的技術實踐
一、背景
得物商家客服采用的是桌面端應用表現(xiàn)形式,而桌面端應用主要架構形式就是一套和操作系統(tǒng)交互的“后端” + 一套呈現(xiàn)界面的“前端(渲染層)”。而桌面端技術又可以根據(jù)渲染層的不同核心劃分為以下幾類:
- C語言家族:原生開發(fā)、QT
- Chromium家族:NW、Electron、CEF
- Webview 家族:Tauri、pywebview、webview_java
- 自立山頭:Flutter
在2022年5月份左右,得物商家客服開始投入桌面端應用業(yè)務,其目標是一個可以適配多操作系統(tǒng)(MacOS、Windows)、快速迭代、富交互的產品。
考慮到以上前提,我們當時可以選擇的框架是Chromium家族或者Webview家族。但是當時對于Webview來說,Tauri 還并不成熟(在 2022年6月才發(fā)布了1.0版本)生態(tài)也不夠豐富。對于pywebview和webview_java相對于前端來說,一方面門檻較高,另一方面生態(tài)也非常少。所以,在當時,我們選擇了Chromium家族中的Electron框架。這是因為對于CEF、Electron、NW來說,Electron有著對前端開發(fā)非常友好的技術棧,僅使用JavaScript就可以完成和操作系統(tǒng)的交互以及交互視覺的編寫,另外,Electron的社區(qū)活躍度和生態(tài)相對于其他兩者也有非常大的優(yōu)勢。最重要的是:真的很快!
圖片
但是,隨著時間的推移,直到2024年的今天,商家客服的入駐量和使用用戶越來越多,用戶的電腦配置也是參差不齊,Electron的弊端開始顯現(xiàn):
- 性能方面:隨著商家客服入駐數(shù)量的快速增加,現(xiàn)有Electron桌面應用在多賬戶+多會話高并發(fā)場景下,占用內存特別大,存在性能瓶頸;
- 安全方面:Electron在內存安全性、跨平臺攻擊、不受限制的上下文和依賴管理等方面存在一些潛在的弱點;
- 體驗方面:現(xiàn)有Electron桌面應用包體積大,下載、更新成本較高;
- 信息集成方面:商家客服目前需要在商家后臺、商家客服后臺、商家客服工作臺3個系統(tǒng)來回切換操作,使用成本很高。
我們也發(fā)現(xiàn),之前調研過的Tauri作為后起之秀,其生態(tài)和穩(wěn)定性在今天已經(jīng)變得非常出色,我們熟知的以下應用都是基于Tauri開發(fā),涵蓋:游戲、工具、聊天、金融等等領域:
- 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是基于操作系統(tǒng)自帶的Webview + Rust的框架。首先,因為不用打包一個Chromium,所以包體積非常的小:
圖片
其次Rust作為一門系統(tǒng)級編程語言,具有以下特點:
- 內存安全:Rust通過所有權和借用機制,在編譯時檢查內存訪問的安全性,避免了常見的內存安全問題,如空指針引用、數(shù)據(jù)競爭等;
- 零成本抽象:Rust提供了豐富的抽象機制,如結構體、枚舉、泛型等,但不引入運行時開銷。這意味著開發(fā)者可以享受高級語言的便利性,同時保持接近底層語言的性能;
- 并發(fā)性能:Rust內置支持并發(fā)和異步編程,通過輕量級的線程(稱為任務)和異步函數(shù)(稱為異步任務)來實現(xiàn)高效的并發(fā)處理。Rust的并發(fā)模型保證了線程安全和數(shù)據(jù)競爭的檢查,以及高性能的任務調度和通信機制;
- 可靠性和可維護性:Rust強調代碼的可讀性、可維護性和可靠性。它鼓勵使用清晰的命名和良好的代碼結構,以及提供豐富的工具和生態(tài)系統(tǒng)來支持代碼質量和測試覆蓋率;
Rust的這些額外的特性使其成為改善桌面應用程序性能和安全性的理想選擇。
二、技術調研
要實現(xiàn)Electron遷移到Tauri,得先分別了解Electron和Tauri的核心功能和架構模型,只有了解了這些,才能對整體的遷移成本做一個把控。
Electron的核心模塊
基礎架構
首先來看看Electron的基礎架構模型:Electron繼承了來自Chromium的多進程架構,Chromium始于其主進程。從主進程可以派生出渲染進程。渲染進程與瀏覽器窗口是一個意思。主進程保存著對渲染進程的引用,并且可以根據(jù)需要創(chuàng)建/刪除渲染器進程。
圖片
每個Electron的應用程序都有一個主入口文件,它所在的進程被稱為 主進程(Main Process)。而主進程中創(chuàng)建的窗體都有自己運行的進程,稱為渲染進程(Renderer Process)。每個Electron的應用程序有且僅有一個主進程,但可以有多個渲染進程。
圖片
應用構建打包
打包一個Electron應用程序簡單來說就是通過構建工具創(chuàng)建一個桌面安裝程序(.dmg、.exe、.deb 等)。在Electron早期作為 Atom 編輯器的一部分時,應用程序開發(fā)者通常通過手動編輯Electron二進制文件來為應用程序做分發(fā)準備。隨著時間的推移,Electron社區(qū)構建了豐富的工具生態(tài)系統(tǒng),用于處理Electron應用程序的各種分發(fā)任務,其中包括:
- 應用程序打包https://github.com/electron/packager
- 代碼簽名,例如https://github.com/electron/osx-sign
- 創(chuàng)建特定平臺的安裝程序,例如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
這樣,應用程序開發(fā)者在開發(fā)Electron應用時,為了構建出跨平臺的桌面端應用,不得不去了解每個包的功能并需要將這些功能進行組合構建,這對新手而言過于復雜,無疑是勸退的。
所以,基于以上背景,目前使用的比較多的是社區(qū)提供的Electron Builder(https://github.com/electron-userland/electron-builder)一體化打包解決方案。得物商家客服也是采用的上述方案。
應用簽名&更新
現(xiàn)在絕大多數(shù)的應用簽名都采用了簽名狗的應用簽名方式,而我們的商家客服桌面端應用也是類似,Electron Builder提供了一個sign的鉤子配置,可以幫助我們來實現(xiàn)對應用代碼的簽名:
...
"win": {
"target": "nsis",
"sign": "./sign.js"
},
...
(詳細的可以直接閱讀electron builder官網(wǎng)介紹,這里只做簡單說明)
對于應用更新而言,我們之前采用的是electron-updater自動更新模式:
圖片
如果對這塊感興趣,可以閱讀我們之前的文章:https://juejin.cn/post/7195447709904404536?searchId=202408131832375B6C2C76DEEE740762EA
Tauri的核心模塊
基礎架構
那么,Tauri的基礎架構模型是什么樣的?其實官網(wǎng)對這塊的介紹比較有限,但是我們可以通過其源碼倉庫和代碼結構管中窺豹的了解Tauri的核心架構模型,為了方便大家理解,我們以得物商家客服桌面端應用為模型,簡單的畫了一個草圖:
圖片
一些核心模塊的解釋:
WRY
由于Web技術具有表現(xiàn)力強和開發(fā)成本低的特點,與 Electron 和NW等框架類似,Tauri應用程序的前端實現(xiàn)是使用Web技術棧編寫的。那么Tauri是如何解決Electron/CEF等框架遇到的Chromium內核體積過大的問題呢?
也許你會想,如果每個應用程序都需要打包瀏覽器內核以實現(xiàn)Web頁面的渲染,那么只要所有應用程序共享相同的內核,這樣在分發(fā)應用程序時就無需打包瀏覽器內核,只需打包Web頁面資源。
WRY是Tauri的封裝Webview框架,它在不同的操作系統(tǒng)平臺上封裝了系統(tǒng)的Webview實現(xiàn):MacOS上使用WebKit.WKWebview,Windows上使用Webview2,Linux上使用WebKitGTK。這樣,在運行Tauri應用程序時,直接使用系統(tǒng)的Webview來渲染應用程序的前端展示。
跨平臺應用窗口創(chuàng)建庫,使用Rust編寫,支持Windows、MacOS、Linux、iOS和Android等所有主要平臺。該庫是winit的一個分支,Tauri根據(jù)自己的需求進行了擴展,如菜單欄和系統(tǒng)托盤功能。
JS API
這個API是一個JS庫,提供調用Tauri Rust后端的一些API能力,利用這個庫可以很方便的完成和Tauri Rust后端的交互以及通信。
看起來有點復雜,其實核心也是分成了主進程和渲染進程兩個部分。
- Tauri的主進程使用Rust編寫,Tauri在主進程中提供了一些常用的Rust API比如窗口創(chuàng)建、消息提醒... 如果我們覺得主進程提供的API不夠,那么我們可以通過Tauri的插件體系自行擴展。
- Tauri的渲染進程則是運行在操作系統(tǒng)的Webview當中的,我們可以直接通過JS + HTML + CSS來編寫,同時,Tauri會為渲染進程注入一些全局的JS API函數(shù)。比如fs、path、shell等等。
這是將所有組件拼到一起的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中現(xiàn)在也是支持的:
{
"signCommand": "signtool.exe --host xxxx %1"
}
后面我們會詳細介紹該能力的使用方式。
而對于更新而言,Tauri則有自己的一套體系:Updater | Tauri Apps這里還是和Electron有著一定的區(qū)別。
選型總結
通過上面的架構模型對比,我們可以很直觀的感受到如果要將我們的Electron應用遷移到Tauri上,整體的遷移改造工作可以總結成以下圖所示:
圖片
核心內容就變成了以下四部分內容:
- 主進程的遷移:而對于商家客服來說,目前主要用的有:自定義窗口autoUpdater自動更新BrowserWindow窗口創(chuàng)建Notification消息通知Tray系統(tǒng)托盤IPC通信
而這些API在Tauri中都有對應的實現(xiàn),所以整體來看,遷移成本和技術可行性都是可控的。
- 渲染進程的遷移:渲染進程改造相對而言就少很多了,因為Tauri和Electron都可以直接使用前端框架來編寫渲染層代碼,所以幾乎可以將之前的前端代碼直接平移過來。但是還是有一些小細節(jié)需要注意,比如IPC通信、JS API的改變、兼容性... 這部分后面也會詳細介紹。
- 應用構建打包:從之前的Electron構建模式改成Tauri構建模式,并自動化整個構建流程和鏈路。
- 應用簽名&更新:簽名形式不用改,主要需要調整簽名的配置,實現(xiàn)對Tauri應用的自動簽名和自動更新能力。
最終,我們選擇了Tauri對現(xiàn)有的商家客服桌面端進行架構優(yōu)化升級。
三、技術實現(xiàn)
渲染進程代碼遷移
目錄結構調整
在聊如何調整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打包依托于兩個部分,首先是對前端頁面的構建,這塊可以根據(jù)業(yè)務需要和框架選擇(Vue、 React)進行構建腳本的執(zhí)行。一般前端構建的產物都是一個dist文件包。
然后是Tauri后端程序部分的構建,這塊主要是對Rust代碼進行編譯成binary crate。
(Tauri后端的編譯在很大程度上依賴于操作系統(tǒng)原生庫和工具鏈,因此當前無法進行有意義的交叉編譯。所以,在本地編譯我們通常需要準備一臺mac和一臺Windows電腦,以滿足在這兩個平臺上的構建。)
整體來看,和Electron是差不多的,這里,我們就直接使用了官方提供的create-tauri-app(https://github.com/tauri-apps/create-tauri-app)腳手架來創(chuàng)建項目,其目錄結構大致如下:
.
├── src // 渲染進程代碼
├── src-tauri // Rust 后端代碼
├── ... // 一些其他配置文件,vite 構建文件等等
└── package.json
所以,這里對渲染進程的目錄調整就很清晰了,直接將我們之前Electron中的renderer-process目錄中的代碼遷移到src目錄中即可。
注意:因為我們對渲染進程目錄進行了調整,所以對應的打包工具的目錄也需要進行調整。
跨域請求處理
商家客服中會有一些接口請求,這些接口請求有的是從業(yè)務中發(fā)起的,有的使用依賴的npm庫中發(fā)起的請求。但因為是客戶端引用,當從客戶端環(huán)境發(fā)起請求時,請求所攜帶的origin是這樣的:
https://tauri.localhost
那么,就會遇到一個我們熟知的一個前端跨域問題。這會導致如果不在access-ctron-allow-origin中的域名會被block掉。
圖片
如果有小伙伴對Electron比較熟悉,可能會知道在Electron實現(xiàn)跨域的方案之一是可以關閉瀏覽器的跨域安全檢測:
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進程模型很類似,但是本質上還是有區(qū)別的,最大的區(qū)別就是Electron中的渲染進程是基于Chromium魔改的,他可以在Chromium中植入一些控制器來修改Chromium的一些默認行為。但Tauri完全是基于不同平臺的內置Webview封裝,考慮的兼容性問題,并沒有對Webview進行改造(雖然Windows的Webview2支持 --disable-web-security,但是其他平臺不行)。所以他的跨域策略是Webview默認的行為,無法調整。
那么在Tauri中,如何發(fā)起一個跨域請求了?
其實社區(qū)也有幾種解決方案,接下來簡單介紹一下社區(qū)的方案和問題。
使用Tauri官方的http
既然瀏覽器會因為跨域問題block掉請求,那么就繞過瀏覽器唄,沒錯,這也是Tauri官方提供的http模塊設計的初衷和原理:https://v1.tauri.app/zh-cn/v1/api/js/http/,其設計方案就是通過JavaScript前端調用Rust后端來發(fā)請求,當請求完成后再返回給前端結果。
圖片
問題:Tauri http有一套自己的API設計和請求規(guī)范,我們必須按照他定義的格式進行請求的發(fā)送和接收。對于新項目來說問題不是很大,但對商家客服來說,這樣最大的問題是之前的所有的接口請求都得改造成Tauri http的格式,我們很多請求是基于Axios的封裝,改造成本非常大,回歸驗證也很困難,而且有很多三方npm包也依賴axios發(fā)請求,這就又增加了改造的成本和后期維護的成本。
使用axios adapter
既然使用axios改造成本大,那么就寫一個axios的適配器(adapter)在數(shù)據(jù)請求的時候不使用瀏覽器原生的xhr發(fā)請求而是使用tauri http來發(fā)請求,順便對axios的請求參數(shù)進行格式化,處理成Tauri http要求的那種各種。在請求響應后也進行類似的處理。
圖片
這種解決方案社區(qū)也有一個庫提供:https://github.com/persiliao/axios-tauri-api-adapter
問題:假設項目中依賴一個npm庫,這個庫中發(fā)起了一個axios請求,那么也需要對這個庫的axios進行適配器改造。這樣還是解決不了三方依賴使用axios的問題。我們還是需要侵入npm包進行axios改造。另外,如果其他庫使用的是xhr或者fetch來直接發(fā)請求或者,那就又無解了。
最后,不管使用方案1還是2,都有個通病,那就是請求都是走的Tauri后端來發(fā)起的,這也意味著我們將在Webview的devtools中的network看不到任何請求的信息和響應的結果,這對開發(fā)調試來說無疑是非常難以接受的。
社區(qū)對這個問題也有相關的咨詢:https://github.com/tauri-apps/tauri/issues/7882,但是官方回復也是實現(xiàn)不了:
圖片
那我們是怎么做的呢?對于Axios來說,其在瀏覽器端工作的原理是通過實例化window.XMLHttpRequest 后的xhr來發(fā)起請求,同時監(jiān)聽xhr的onreadystatechange事件來處理請求的響應。然后對于一些請求頭都是通過xhr.setRequestHeader這樣的方式設置到了xhr對象上。因此,對于axios、原生XmlHttpRequest請求來說,我們就可以重寫XmlHttpRequest中的send、onreadystatechange、setRequestHeader等方法,讓其通過Tauri的http來發(fā)請求。
但是對window.fetch這樣底層未使用XHR的請求來說,我們就需要重寫window.fetch。讓其在調用window.fetch的時候,調用xhr.send來發(fā)請求,這樣便實現(xiàn)了變相調用Tauri http的功能。
核心代碼:
class AdapterXMLHTTP extends EventTarget{
// ...
// 重寫 send 方法
async send(data: unknown) {
// 通過 TauriFetch 來發(fā)請求
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 來發(fā)請求
const xhr = new XMLHttpRequst()
})
}
// 重寫 window.XMLHttpRequest
window.XMLHttpRequest = AdapterXMLHTTP;
// 重寫 window.featch
window.fetch = fetchPollify;
那怎么解決devtools沒法調試請求的問題呢?
為了讓請求日志能出現(xiàn)在瀏覽器的webview devtools network中,我們可能需要開發(fā)一個類似于chrome plugin的方式來支持。但是很可惜,在Tauri中,webview是不支持插件開發(fā)的:https://github.com/tauri-apps/tauri/discussions/2685
所以我們只能采用新的方式來支持,那就是外接devtools。啥意思呢?就是在操作系統(tǒng)網(wǎng)絡層代理掉網(wǎng)絡請求,然后輸出到另一個控制臺中進行展示,原理類似于Charles。
到這里,我們就完成了對跨域網(wǎng)絡請求的處理改造工作。核心架構圖如下:
關鍵性API兼容
這里需要注意的是,Tauri使用的是系統(tǒng)自帶的Webview,而Electron則是直接內置了Chromium,這里有個非常大的誤區(qū)在于想當然的把Webview類比Chromium以為瀏覽器的API都可以直接使用。這其實是不對的,舉個例子:我們在發(fā)送一些消息通知的時候,可能會使用HTML5的 Notification Web API:https://developer.mozilla.org/en-US/docs/Web/API/Notification
但是,這個API是瀏覽器自行實現(xiàn)的,也就是說,你在 Electron 中可以這么用,但是,如果你在Tauri中,你會發(fā)現(xiàn)一個bug:https://github.com/tauri-apps/tauri/issues/3698,這個bug的大概含義就是Tauri中的Notification不會觸發(fā)click點擊事件。這個bug至今還未解決。究其原因:
Tauri依賴的操作系統(tǒng)webview并沒有實現(xiàn)對Notification 的支持,webview本身希望宿主應用自行實現(xiàn)對Notification的實現(xiàn),所以Tauri就重寫了JS的Notification API,當你在調用window Notification的時候,實際上你和Rust進程完成了一次通信,調用的還是tauri::Notification模塊。
在Tauri源碼里面,是這樣實現(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 后端通信,調用 Rust 發(fā)送系統(tǒng)通知
return window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Notification',
message: {
cmd: 'notification',
options:
typeof options === 'string'
? {
title: options
}
: options
}
})
}
// 這里便是對 Notification 的重寫實現(xiàn)
window.Notification = function (title, options) {
const opts = options || {}
sendNotification(
Object.assign(opts, {
title: title
})
)
}
除此之外,Tauri還分別實現(xiàn)了:
- DOM上標簽的點擊跳轉功能,使用內置的Tauri API進行打開webview。
- 差異化操作系統(tǒng)原生窗口的拖拽和最大化事件:在Windows和Linux上,當鼠標按下時拖動,雙擊時最大化;而在MacOS上,最大化應該在鼠標抬起時發(fā)生,如果雙擊后鼠標移動,應該取消最大化。
- window.alert
- window.confirm
- window.print(Macos)
所以,我們在對商家客服從Electron遷移到Tauri的過程中,還需要對這些關鍵性API進行兼容性測試和回歸。一旦發(fā)現(xiàn)相關API不符合預期,我們需要及時調整業(yè)務策略或者給嘗試進行hack。
(這里賣個關子,雖然Tauri不支持對Notification的點擊事件回調,那么我們是怎么讓他支持的呢?在下一節(jié)主進程代碼遷移中我們會詳細介紹。)
兼容性回歸
對于樣式兼容性來說,因為Electron在不同操作系統(tǒng)內都集成了Chromium所以我們完全不用擔心樣式兼容性的問題。但是對于Tauri來說,因為不同操作系統(tǒng)使用了不同的Webview,所以在樣式上,我們還是需要注意不同操作系統(tǒng)下的差異性,比如:以下分別是Linux和Windows渲染Element-Plus的界面:
圖片
圖片
可以看到在按鈕大小、文字對齊等樣式上面還是存在著不小的差距。
除了上述問題,如果你需要兼容Linux系統(tǒng),那么還有webkitgtk在非整數(shù)倍縮放下的bug,應該是陳年老問題了。當然,這些問題都是上游webkitgtk的“鍋”。
所以,社區(qū)也有關于討論Tauri是否有可能在不同平臺上使用同一個webview的可能性的討論:https://github.com/tauri-apps/tauri/discussions/4591。官方是期待能有Mac版本的Webview發(fā)布,不過大概率來看不太現(xiàn)實,一方面是因為:微軟決定不開源 Webview2的Mac和Linux版本(https://mp.weixin.qq.com/s/p6pdNI3_di7oBkv4ugDIdA),另一方面是如果要使用統(tǒng)一的webview那就又回到了Electron。
除了樣式兼容性外,對于JS代碼的兼容性也需要留意Tauri在Windows上使用的是Webview2而Webview2本身就是基于Chromium的,所以代碼兼容性倒還好,但是在MacOS 上使用的就是WebKit.WKWebview,Safari就是基于他,所以到這里,我想你也明白了,這就又回到了前端處理不同瀏覽器兼容性的問題上來了。所以這里溫馨提示一下:構建時前端代碼需要進行polyfill。
對于Electron應用的用戶來說,可能沒有這樣的煩惱,最新的API只要Chrome支持,那就可以用。
主進程代碼遷移
自定義操作欄窗口
默認情況,在構建窗口的時候,會使用系統(tǒng)自帶的原生窗口樣式,比如在MacOS下的樣式:
在有些情況下,操作系統(tǒng)的原生窗口并不能符合我們的一些視覺和交互需求。所以,在創(chuàng)建桌面應用的時候,有時候我們希望能完全掌控窗口的樣式,而隱藏掉系統(tǒng)提供的窗口邊框和標題欄等。這個時候就需要用到自定義操作欄窗口。比如在Windows中,我們希望在右上角有一排自定義的操作欄,就像是這樣:
商家客服桌面端的窗口就是一個無邊框的自定義操作欄的窗口,在Electron中,我們可以這樣操作快速創(chuàng)建一個無邊框窗口:
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中,要實現(xiàn)自定窗口首先需要在窗口創(chuàng)建的時候設置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來創(chuàng)建一個互斥體,確保只有一個實例可以運行,并在用戶嘗試啟動多個實例時,聚焦于已經(jīng)存在的實例并傳遞數(shù)據(jù),簡化后的代碼大致如下:
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,則表示已有實例在運行。
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
unsafe {
// 找到已存在窗口的句柄
let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());
if hwnd != 0 {
// ...
// 通過 SendMessageW 發(fā)送數(shù)據(jù)給該窗口
SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _);
// 最后退出當前應用
app.exit(0);
}
}
}
// ...
Ok(())
})
.build()
}
(注意:這里有坑,如果你的應用需要實現(xiàn)一個重新啟動功能,那么在單例模式下將不會生效,核心原因是因為應用重啟的邏輯是先打開一個新的實例再關閉舊的運行實例。而打開新的實例在單例模式下就被阻止了,這塊的詳細原因和解決方案我們已經(jīng)給Tauri提了PR:https://github.com/tauri-apps/tauri/pull/11684)
系統(tǒng)消息通知能力
消息通知是商家客服桌面端應用必不可少的能力,消息通知能力一般可以分為以下兩種:
- 觸達操作系統(tǒng)的消息通知
- 用戶點擊消息后的回調事件
前面我們有提到,在Electron中,我們需要顯示來自渲染進程的通知,那么可以直接使用HTML5的Web API來發(fā)送一條系統(tǒng)消息通知:
function notifyMe() {
if (!("Notification" in window)) {
// 檢查瀏覽器是否支持通知
alert("當前瀏覽器不支持桌面通知");
} else if (Notification.permission === "granted") {
// 檢查是否已授予通知權限;如果是的話,創(chuàng)建一個通知
const notification = new Notification("你好!");
// …
} else if (Notification.permission !== "denied") {
// 我們需要征求用戶的許可
Notification.requestPermission().then((permission) => {
// 如果用戶接受,我們就創(chuàng)建一個通知
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來說,只實現(xiàn)了第1個能力,也就是消息觸達。Tauri本身不支持點擊回調的功能,這就導致了用戶發(fā)來了一個消息,但是業(yè)務無法感知客服點擊消息的事件。而且原生的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 app
app.run(|_app_handle, _event| {});
Tauri的Notification Rust實現(xiàn)源碼位置在:https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/notification.rs這個文件中,其中看一下show函數(shù)的實現(xiàn):
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函數(shù)非win7環(huán)境下show函數(shù)調用的是notify_rust這個庫,而在win7環(huán)境下調用的是win7_notifications這個庫。而notify_rust這個庫,本身確實未完成實現(xiàn)對MacOS和Windows點擊回調事件。
所以我們需要自定義一個Notification的Tauri插件,實現(xiàn)對點擊回調的能力。(因為篇幅原因,這里只介紹一些核心的實現(xiàn)邏輯)
MacOS 支持消息點擊回調能力
notify_rust在Mac上實現(xiàn)消息通知是基于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,
};
// 發(fā)通過 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 函數(shù)
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操作系統(tǒng)中,notify_rust則是通過winrt_notification這個Crate來發(fā)送消息通知,winrt_notification 則是調用的windows這個crate來實現(xiàn)消息通知,windows這個crate的官方描述是:為Rust開發(fā)人員提供了一種自然和習慣的方式來調用Windows API。這里,主要會用到以下幾個方法:
- windows::UI::Notifications::ToastNotification::CreateToastNotification:這個函數(shù)的作用是根據(jù)指定的參數(shù)創(chuàng)建一個Toast通知對象,可以設置通知的標題、文本內容、圖標、音頻等屬性,并可以指定通知被點擊時的響應行為。通過調用這個函數(shù),可以在Windows應用程序中創(chuàng)建并顯示自定義的Toast通知,向用戶展示相關信息。
- windows::Data::Xml::Dom::XmlDocument:這是一個用于在Windows應用程序中創(chuàng)建和處理XML文檔的類。它主要提供了一種方便的方式來創(chuàng)建、解析和操作XML數(shù)據(jù)。
- windows::UI::Notifications::ToastNotificationManager::CreateToastNotifierWithId:通過調用CreateToastNotifierWithId函數(shù),可以創(chuàng)建一個Toast通知管理器對象,并指定一個唯一的標識符。這個標識符通常用于標識應用程序或者特定的通知渠道,以確保通知的正確分發(fā)和管理。創(chuàng)建了Toast通知管理器之后,就可以使用它來生成和發(fā)送Toast通知,向用戶展示相關信息,并且可以根據(jù)標識符進行個性化的通知管理。
- windows::Foundation::TypedEventHandler:這是Windows Runtime API中的一個委托(delegate)類型。在Windows Runtime中,委托類型用于表示事件處理程序,允許開發(fā)人員編寫事件處理邏輯并將其附加到特定的事件上。
所以,要想在> win7的操作系統(tǒng)中顯示消息同時的主要流程大致是:
- 通過XmlDocument來創(chuàng)建一個Xml消息通知模板。
- 然后將創(chuàng)建好的Xml消息模板作為CreateToastNotification的入?yún)韯?chuàng)建一個toast通知。
- 最后調用CreateToastNotifierWithId來創(chuàng)建一個Toast通知管理器對象,創(chuàng)建成功后顯示toast。
- 通過TypedEventHandler監(jiān)聽用戶點擊事件并完成回調觸發(fā)
但是winrt_notification這個庫,只完成了1-3步驟,所以我們需要手動實現(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();
// 設置消息持續(xù)狀態(tài),支持 short 和 long
// short 就是默認 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)建消息點擊監(jiān)聽捕獲
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(())
},
);
// 通過消息管理器發(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上支持消息通知點擊回調能力
在Windows 7中,Tauri調用的是win7_notifications這個庫,這個庫本身也沒有實現(xiàn)對消息點擊的回調處理,我們需要擴展win7_notifications的能力來實現(xiàn)對消息通知的回調事件。我們希望這個庫可以這樣調用:
win7_notify::Notification::new()
.appname(&app_name)
.body(&body)
.summary(&title)
.timeout(duration)
.click_event(move |str| {
// 用戶自定義的參數(shù)
let data = &sid;
// 觸發(fā)前端的回調能力
let js = tauri::api::ipc::format_callback(handle, &data)
.expect("消息點擊報錯");
let _ = window_.eval(js.as_str());
})
.show();
而我們要做的,就是為win7_notify這個庫中的Notification結構體增加一個click_event函數(shù),這個函數(shù)支持傳入一個閉包,這個閉包在點擊消息通知的時候執(zhí)行。
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 增加點擊參數(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)
}
}
}
總結:
圖片
- Tauri本身不支持Notification的點擊事件,需要自行實現(xiàn)。
- 需要對不同操作系統(tǒng)分別實現(xiàn)點擊回調能力。
- MacOS mac_notification_sys庫本來就有點擊回調,只是Tauri沒有捕獲處理,需要自定義捕獲處理邏輯就好了。
- Windows > 7中,通過windows這個crate,來完成調用Windows操作系統(tǒng)API的能力,但是winrt_notification這個庫并沒有實現(xiàn)對Windows API回調點擊的捕獲處理,所以需要重寫winrt_notification這個庫。
- Windows 7中,消息通知其實是通過繪制窗口和監(jiān)聽鼠標點擊來觸發(fā)的,但是win7_notify本身也沒有支持用戶對點擊回調的捕獲,也需要擴展這個庫的點擊捕獲能力。
應用構建打包
Windows 10
Tauri 1.3版本之前,應用程序在Windows上使用的是WiX(Windows Installer)Toolset v3工具進行構建,構建產物是Microsoft安裝程序(.msi文件)。1.3之后,使用的是NSIS來構建應用的xxx-setup.exe安裝包。
Tauri CLI默認情況下使用當前編譯機器的體系結構來編譯可執(zhí)行文件。假設當前是在64位計算機上開發(fā),CLI將生成64位應用程序。如果需要支持32位計算機,可以使用--target標志使用不同的Rust目標編譯應用程序:
tauri build --target i686-pc-windows-msvc
為了支持不同架構的編譯,需要為Rust添加對應的環(huán)境支持,比如:
rustup target add i686-pc-windows-msvc
其次,需要為構建增加不同的環(huán)境變量,以便為了在不同的環(huán)境進行代碼測試,對應到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參數(shù)指定了構建的配置文件路徑,Tauri會和src-tauri中的tarui.conf.json文件進行合并。除此之外,還可以通過tarui.{{platform}}.conf.json的形式指定不同平臺的獨特配置,優(yōu)先級關系:
-c path >> tarui.{{platform}}.conf.json >> tarui.conf.json
Windows 7
Webview 2
Tauri在Windows 7上運行有兩個東西需要注意,一個是Tauri的前端跨平臺在Windows上依托于Webview2但是Windows 7中并不會內置Webview2因此我們需要在構建時指明引入Webview的方式:
圖片
綜合比較下來,embedBootstrapper目前是比較好的方案,一方面可以減少安裝包體積,一方面減少不必要的靜態(tài)資源下載。
在Tauri中,會通過"Windows7-compat"來構建一些Win7特有的環(huán)境代碼,比如:
#[cfg(feature = "windows7-compat")]
{
// todo
}
在Tauri文檔中也有相關介紹,主要是在使用Notification的時候,需要加入Windows7-compat特性。不過,因為 Tauri 對Notification的點擊事件回調是不支持,所以我重寫了Tauri的所有Notification模塊,已經(jīng)內置了Windows7-compat能力,因此可以不用設置了。
MacOS
MacOS操作系統(tǒng)也有M1和Intel的區(qū)分,所以為了可以構建出兼容兩個版本的產物,我們需要使用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來說,應用更新的詳細配置步驟可以直接看官網(wǎng)的介紹:https://tauri.app/zh-cn/v1/guides/distribution/updater/。這里為了方便大家理解,簡單畫了個更新流程圖:
圖片
核心流程如下:
- 對于需要更新的應用,可以在渲染進程通過JS調用 installUpdate() API
- Tauri內部會發(fā)送一個更新協(xié)議事件:
pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
br
- Tauri主進程Updater模塊會響應這個事件,執(zhí)行download_and_install函數(shù)通過tauri.config.json中配置的endpoints來尋找下載地址下載endpoints服務器上的zip包內容并解壓存儲到一個臨時文件夾,Windows中大概位置在C:\Users\admin\AppData\Local\Temp這里。然后通過PowerShell來執(zhí)行下載的setup.exe文件:["-NoProfile", "-WindowStyle", "Hidden", "Start-Process"],這些參數(shù)告訴PowerShell在后臺運行,不顯示任何窗口,并啟動一個新的進程。
if found_path.extension() == Some(OsStr::new("exe")) {
// 創(chuàng)建一個新的 OsString,并將 found_path 包裹在引號中,以便在 PowerShell 中正確處理路徑
let mut installer_path = std::ffi::OsString::new();
installer_path.push("\"");
installer_path.push(&found_path);
installer_path.push("\"");
// 構造安裝程序參數(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)建一個新的命令,指向 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,安裝過程用戶無需參與。
需要注意的是:如果以為更新是增量更新,不會卸載之前已經(jīng)安裝好的應用程序只更新需要變更的部分。其實是不對的,整個安裝過程可以理解為Tauri在后臺幫你重新下載了一個最新的安裝包,然后幫你重新安裝了一下。
總結:更新的核心原理就是通過使用Windows的PowerShell來對下載后的安裝包進行open。然后由安裝包進行安裝。
為什么我要花這么大的篇幅來介紹 Tauri 的更新原理呢?
這是因為我們在更新的過程中碰到了兩個比較大的問題:
- 通過cmd調用PowerShell來安裝時,會在安裝過程中出現(xiàn)一個藍色的PowerShell控制臺一閃而過:
圖片
- 在部分開啟了病毒防護的Windows電腦上,使用PowerShell來執(zhí)行對安裝包的打開,會報錯:Permission Denied,導致安裝更新失敗:https://github.com/rust-lang/rustlings/issues/604
這些都是因為Tauri直接使用 Powershell的問題,那需要怎么改呢?很簡單,那就是使用Windows操作系統(tǒng)提供的ShellExecuteW來運行安裝程序,核心代碼如下:
windows::Win32::UI::Shell::ShellExecuteW(
0,
operation.as_ptr(),
file.as_ptr(),
parameters.as_ptr(),
std::ptr::null(),
SW_SHOW,
)
但是這塊是Tauri的源碼,我們沒法直接修改,但這個問題的解決方法我們已經(jīng)給Tauri提了PR并已合入到官方的1.6.8正式版本當中:https://github.com/tauri-apps/tauri/pull/9818
所以,你要做的就是確保Tauri升級到v1.6.8及以后版本。
應用簽名
Tauri應用程序簽名可以分成2個部分,第一部分是應用程序簽名,第二部分是安裝包程序簽名,官網(wǎng)上介紹的簽名方法需要配置tauri.config.json中如下字段:
"windows": {
// 簽名指紋
"certificateThumbprint": "xxx",
// 簽名算法
"digestAlgorithm": "sha256",
// 時間戳
"timestampUrl": "http://timestamp.comodoca.com"
}
如果你按照官方的步驟來進行簽名:https://v1.tauri.app/zh-cn/v1/guides/distribution/sign-windows/,很快就會發(fā)現(xiàn)問題所在:官網(wǎng)中簽名有一個重要的步驟就是導出一個.pfx文件,但是現(xiàn)在業(yè)界簽名工具基本上都是采用簽名狗的方式進行的,這是一個類似于U盾簽名工具,需要插入電腦中才可以進行簽名,不支持直接導出.pfx格式的文件:
圖片
所以我們需要額外處理一下:
簽名狗支持導出一個.cert證書,可以查看到證書的指紋:
圖片
這里證書的指紋對應的就是certificateThumbprint字段。
然后需要插入我們在簽名機構購買的USB key。這樣,在構建的時候,就會提示讓我們輸入密碼:
圖片
到這里就可以完成對應用程序的簽名。
不過對于我們而言,USB key簽名狗是整個公司共享的,通常不在前端開發(fā)手里(尤其是異地辦公)。一種做法是在Tauri構建的過程中,對于需要簽名的軟件提供一個signCommand命令鉤子,并為這個命令傳入文件的路徑,然后交由開發(fā)者對文件進行自行簽名(比如上傳到擁有簽名工具的電腦,上傳上去后,遠程進行簽名,簽名完成再下載)。所以這就需要讓Tauri將簽名功能暴露出來,讓我們自行進行簽名,比如這樣:
{
"signCommand": "signtool.exe --host xxxx %1"
}
該命令中包含一個%1,它只是二進制路徑的占位符,Tauri在構建的時候會將需要簽名的文件路徑替換掉%1。
圖片
這個功能官網(wǎng)上還沒有更新相關的介紹,所以你可能看不到這塊的使用方式,因為也是我們最近提交的PR:https://github.com/tauri-apps/tauri/pull/9902。不過目前,這個PR已經(jīng)被合入Tauri的主版本中,你要做的就是就是升級Tauri到1.7.0升級@tauri-apps/cli到1.6.0。
四、收益&總結
經(jīng)過我們的不懈努力(不斷地填坑)到目前,得物商家客服Tauri版本終于如期上線,基于Tauri遷移帶來的收益如下:
整體性能測試相比之前的Electron應用有比較明顯的提升:
- 包體積7M,Electron 80M下降91.25%。
- 平均內存占用249M Electron 497M下降49.9%。
- 平均CPU占用百分比20%,Electron 63.5%下降 63.19%。
整體在性能體驗上有一個非常顯著改善。但是,這里也暴露出使用Tauri的一些問題。