精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

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

一、背景

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

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

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

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

圖片圖片

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

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

我們也發(fā)現(xiàn),之前調(diào)研過的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

除此之外,因為Tauri是基于操作系統(tǒng)自帶的Webview + Rust的框架。首先,因為不用打包一個Chromium,所以包體積非常的小:

圖片圖片

其次Rust作為一門系統(tǒng)級編程語言,具有以下特點:

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

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

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

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

Electron的核心模塊

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

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

圖片圖片

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

圖片圖片

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

打包一個Electron應(yīng)用程序簡單來說就是通過構(gòu)建工具創(chuàng)建一個桌面安裝程序(.dmg、.exe、.deb 等)。在Electron早期作為 Atom 編輯器的一部分時,應(yīng)用程序開發(fā)者通常通過手動編輯Electron二進制文件來為應(yīng)用程序做分發(fā)準備。隨著時間的推移,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)建特定平臺的安裝程序,例如https://github.com/electron/windows-installer或https://github.com/electron-userland/electron-installer-dmg
  • 本地Node.js原生擴展模塊重新構(gòu)建https://github.com/electron/rebuild
  • 通用MacOS構(gòu)建https://github.com/electron/universal

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

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

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

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

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

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

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

圖片圖片

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

Tauri的核心模塊

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

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

圖片圖片

一些核心模塊的解釋:

WRY

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

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

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

TAO

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

JS API

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

看起來有點復(fù)雜,其實核心也是分成了主進程和渲染進程兩個部分。

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

Tauri

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

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

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

yarn tauri build

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

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

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

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

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

后面我們會詳細介紹該能力的使用方式。

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

選型總結(jié)

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

圖片圖片

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

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

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

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

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

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

渲染進程代碼遷移

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

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

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

其中文件說明如下:

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

有的時候你可能需要劃分目錄來編寫不同功能的代碼,但是,不管功能目錄怎么改,最終的渲染進程和主進程的構(gòu)建產(chǎn)物都是期望符合類似于上面的結(jié)構(gòu)。

圖片圖片

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

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

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

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

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

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

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

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

注意:因為我們對渲染進程目錄進行了調(diào)整,所以對應(yīng)的打包工具的目錄也需要進行調(diào)整。

跨域請求處理

商家客服中會有一些接口請求,這些接口請求有的是從業(yè)務(wù)中發(fā)起的,有的使用依賴的npm庫中發(fā)起的請求。但因為是客戶端引用,當從客戶端環(huán)境發(fā)起請求時,請求所攜帶的origin是這樣的:

https://tauri.localhost

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

圖片圖片

如果有小伙伴對Electron比較熟悉,可能會知道在Electron實現(xiàn)跨域的方案之一是可以關(guā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進程模型很類似,但是本質(zhì)上還是有區(qū)別的,最大的區(qū)別就是Electron中的渲染進程是基于Chromium魔改的,他可以在Chromium中植入一些控制器來修改Chromium的一些默認行為。但Tauri完全是基于不同平臺的內(nèi)置Webview封裝,考慮的兼容性問題,并沒有對Webview進行改造(雖然Windows的Webview2支持 --disable-web-security,但是其他平臺不行)。所以他的跨域策略是Webview默認的行為,無法調(diào)整。

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

其實社區(qū)也有幾種解決方案,接下來簡單介紹一下社區(qū)的方案和問題。

使用Tauri官方的http

既然瀏覽器會因為跨域問題block掉請求,那么就繞過瀏覽器唄,沒錯,這也是Tauri官方提供的http模塊設(shè)計的初衷和原理:https://v1.tauri.app/zh-cn/v1/api/js/http/,其設(shè)計方案就是通過JavaScript前端調(diào)用Rust后端來發(fā)請求,當請求完成后再返回給前端結(jié)果。

圖片圖片

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

使用axios adapter

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

圖片圖片

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

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

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

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

圖片圖片

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

但是對window.fetch這樣底層未使用XHR的請求來說,我們就需要重寫window.fetch。讓其在調(diào)用window.fetch的時候,調(diào)用xhr.send來發(fā)請求,這樣便實現(xiàn)了變相調(diào)用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沒法調(diào)試請求的問題呢?

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

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

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

關(guān)鍵性API兼容

這里需要注意的是,Tauri使用的是系統(tǒng)自帶的Webview,而Electron則是直接內(nèi)置了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本身希望宿主應(yīng)用自行實現(xiàn)對Notification的實現(xiàn),所以Tauri就重寫了JS的Notification API,當你在調(diào)用window  Notification的時候,實際上你和Rust進程完成了一次通信,調(diào)用的還是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 后端通信,調(diào)用 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上標簽的點擊跳轉(zhuǎn)功能,使用內(nèi)置的Tauri API進行打開webview。
  • 差異化操作系統(tǒng)原生窗口的拖拽和最大化事件:在Windows和Linux上,當鼠標按下時拖動,雙擊時最大化;而在MacOS上,最大化應(yīng)該在鼠標抬起時發(fā)生,如果雙擊后鼠標移動,應(yīng)該取消最大化。
  • window.alert
  • window.confirm
  • window.print(Macos)

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

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

兼容性回歸

對于樣式兼容性來說,因為Electron在不同操作系統(tǒng)內(nèi)都集成了Chromium所以我們完全不用擔心樣式兼容性的問題。但是對于Tauri來說,因為不同操作系統(tǒng)使用了不同的Webview,所以在樣式上,我們還是需要注意不同操作系統(tǒng)下的差異性,比如:以下分別是Linux和Windows渲染Element-Plus的界面:

圖片圖片

圖片圖片

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

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

所以,社區(qū)也有關(guān)于討論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就是基于他,所以到這里,我想你也明白了,這就又回到了前端處理不同瀏覽器兼容性的問題上來了。所以這里溫馨提示一下:構(gòu)建時前端代碼需要進行polyfill。

對于Electron應(yīng)用的用戶來說,可能沒有這樣的煩惱,最新的API只要Chrome支持,那就可以用。

主進程代碼遷移

自定義操作欄窗口

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

在有些情況下,操作系統(tǒng)的原生窗口并不能符合我們的一些視覺和交互需求。所以,在創(chuàng)建桌面應(yī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)建的時候設(shè)置decoration無裝飾樣式,比如這樣:(也可以在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類似,自己畫一個控制欄,詳細的代碼可以參考這里: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>

單例模式

通過使用窗口單例模式,可以確保應(yīng)用程序在用戶嘗試多次打開時只會有一個主窗口實例,從而提高用戶體驗并避免不必要的資源占用。在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 _);
                        // 最后退出當前應(yīng)用
                        app.exit(0);
                    }
                }
            }
            // ...
            Ok(())
        })
        .build()
}

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

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

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

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

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

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

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

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本身不支持點擊回調(diào)的功能,這就導(dǎo)致了用戶發(fā)來了一個消息,但是業(yè)務(wù)無法感知客服點擊消息的事件。而且原生的Web API也是Tauri自己寫的,原理還是調(diào)用了Rust的通知能力。接下來,我也會詳細介紹一下我們是如何擴展消息點擊回調(diào)能力的。

Tauri在Rust層,我們可以通過下面這段代碼來調(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實現(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 構(gòu)造 notification 實例
      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這個庫,而在win7環(huán)境下調(diào)用的是win7_notifications這個庫。而notify_rust這個庫,本身確實未完成實現(xiàn)對MacOS和Windows點擊回調(diào)事件。

所以我們需要自定義一個Notification的Tauri插件,實現(xiàn)對點擊回調(diào)的能力。(因為篇幅原因,這里只介紹一些核心的實現(xiàn)邏輯)

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

notify_rust在Mac上實現(xiàn)消息通知是基于Mac_notification_sys這個庫的,這個庫本身是支持對點擊action的response,只是notify_rust沒有處理而已,所以我們可以為notify_rust增加對Mac上點擊回調(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ā)通過 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à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("點擊 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上支持消息點擊回調(diào)能力

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

  • windows::UI::Notifications::ToastNotification::CreateToastNotification:這個函數(shù)的作用是根據(jù)指定的參數(shù)創(chuàng)建一個Toast通知對象,可以設(shè)置通知的標題、文本內(nèi)容、圖標、音頻等屬性,并可以指定通知被點擊時的響應(yīng)行為。通過調(diào)用這個函數(shù),可以在Windows應(yīng)用程序中創(chuàng)建并顯示自定義的Toast通知,向用戶展示相關(guān)信息。
  • windows::Data::Xml::Dom::XmlDocument:這是一個用于在Windows應(yīng)用程序中創(chuàng)建和處理XML文檔的類。它主要提供了一種方便的方式來創(chuàng)建、解析和操作XML數(shù)據(jù)。
  • windows::UI::Notifications::ToastNotificationManager::CreateToastNotifierWithId:通過調(diào)用CreateToastNotifierWithId函數(shù),可以創(chuàng)建一個Toast通知管理器對象,并指定一個唯一的標識符。這個標識符通常用于標識應(yīng)用程序或者特定的通知渠道,以確保通知的正確分發(fā)和管理。創(chuàng)建了Toast通知管理器之后,就可以使用它來生成和發(fā)送Toast通知,向用戶展示相關(guān)信息,并且可以根據(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通知。
  • 最后調(diào)用CreateToastNotifierWithId來創(chuàng)建一個Toast通知管理器對象,創(chuàng)建成功后顯示toast。
  • 通過TypedEventHandler監(jiān)聽用戶點擊事件并完成回調(diào)觸發(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();
  // 設(shè)置消息持續(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上支持消息通知點擊回調(diào)能力

在Windows 7中,Tauri調(diào)用的是win7_notifications這個庫,這個庫本身也沒有實現(xiàn)對消息點擊的回調(diào)處理,我們需要擴展win7_notifications的能力來實現(xiàn)對消息通知的回調(diào)事件。我們希望這個庫可以這樣調(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("消息點擊報錯");
      let _ = window_.eval(js.as_str());
    })
    .show();

而我們要做的,就是為win7_notify這個庫中的Notification結(jié)構(gòu)體增加一個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 的調(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 {
       // ....
       // 增加對點擊事件的調(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 增加點擊參數(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的點擊事件,需要自行實現(xiàn)。
  • 需要對不同操作系統(tǒng)分別實現(xiàn)點擊回調(diào)能力。
  • MacOS  mac_notification_sys庫本來就有點擊回調(diào),只是Tauri沒有捕獲處理,需要自定義捕獲處理邏輯就好了。
  • Windows > 7中,通過windows這個crate,來完成調(diào)用Windows操作系統(tǒng)API的能力,但是winrt_notification這個庫并沒有實現(xiàn)對Windows API回調(diào)點擊的捕獲處理,所以需要重寫winrt_notification這個庫。
  • Windows 7中,消息通知其實是通過繪制窗口和監(jiān)聽鼠標點擊來觸發(fā)的,但是win7_notify本身也沒有支持用戶對點擊回調(diào)的捕獲,也需要擴展這個庫的點擊捕獲能力。

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

Windows 10

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

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

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

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

rustup target add i686-pc-windows-msvc

其次,需要為構(gòu)建增加不同的環(huán)境變量,以便為了在不同的環(huán)境進行代碼測試,對應(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會和src-tauri中的tarui.conf.json文件進行合并。除此之外,還可以通過tarui.{{platform}}.conf.json的形式指定不同平臺的獨特配置,優(yōu)先級關(guān)系:

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

Windows 7

Webview 2

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

圖片圖片

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

Windows 7一些特性

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

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

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

MacOS

MacOS操作系統(tǒng)也有M1和Intel的區(qū)分,所以為了可以構(gòu)建出兼容兩個版本的產(chǎn)物,我們需要使用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

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

應(yīng)用更新

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

圖片圖片

核心流程如下:

  • 對于需要更新的應(yīng)用,可以在渲染進程通過JS調(diào)用 installUpdate() API
  • Tauri內(nèi)部會發(fā)送一個更新協(xié)議事件:
pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
br
  • Tauri主進程Updater模塊會響應(yīng)這個事件,執(zhí)行download_and_install函數(shù)通過tauri.config.json中配置的endpoints來尋找下載地址下載endpoints服務(wù)器上的zip包內(nèi)容并解壓存儲到一個臨時文件夾,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("\"");
      
      // 構(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)建一個新的命令,指向 PowerShell 的路徑。
      // 使用 Start-Process 命令來啟動安裝程序,
      // 并設(shè)置 -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啟動應(yīng)用安裝程序的時候,就會使用到tauri.config.json中配置的updater.windows.installMode功能:"basicUi":指定安裝過程中包括最終對話框在內(nèi)的基本用戶界面,需要用戶手動點擊下一步。"quiet":安靜模式表示無需用戶交互。如果安裝程序需要管理員權(quán)限(WiX),則需要管理員權(quán)限。"passive":會顯示一個只有安裝進度條的UI,安裝過程用戶無需參與。

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

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

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

這是因為我們在更新的過程中碰到了兩個比較大的問題:

  • 通過cmd調(diào)用PowerShell來安裝時,會在安裝過程中出現(xiàn)一個藍色的PowerShell控制臺一閃而過:

圖片圖片

  • 在部分開啟了病毒防護的Windows電腦上,使用PowerShell來執(zhí)行對安裝包的打開,會報錯:Permission Denied,導(dǎo)致安裝更新失敗: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及以后版本。

應(yīng)用簽名

Tauri應(yīng)用程序簽名可以分成2個部分,第一部分是應(yīng)用程序簽名,第二部分是安裝包程序簽名,官網(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)中簽名有一個重要的步驟就是導(dǎo)出一個.pfx文件,但是現(xiàn)在業(yè)界簽名工具基本上都是采用簽名狗的方式進行的,這是一個類似于U盾簽名工具,需要插入電腦中才可以進行簽名,不支持直接導(dǎo)出.pfx格式的文件:

圖片圖片

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

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

圖片圖片

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

然后需要插入我們在簽名機構(gòu)購買的USB key。這樣,在構(gòu)建的時候,就會提示讓我們輸入密碼:

圖片圖片

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

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

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

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

圖片圖片

這個功能官網(wǎng)上還沒有更新相關(guān)的介紹,所以你可能看不到這塊的使用方式,因為也是我們最近提交的PR:https://github.com/tauri-apps/tauri/pull/9902。不過目前,這個PR已經(jīng)被合入Tauri的主版本中,你要做的就是就是升級Tauri到1.7.0升級@tauri-apps/cli到1.6.0。

四、收益&總結(jié)

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

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

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

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

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

2023-02-01 18:33:44

得物商家客服

2023-11-27 18:38:57

得物商家測試

2022-12-02 18:45:06

SOP機器人技術(shù)

2022-12-09 18:58:10

2022-10-20 14:35:48

用戶畫像離線

2025-03-20 10:47:15

2023-03-30 18:39:36

2025-11-11 01:55:00

2022-08-27 21:31:04

Tauri框架二進制

2023-02-06 18:35:05

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

2023-12-27 18:46:05

云原生容器技術(shù)

2023-10-09 18:35:37

得物Redis架構(gòu)

2025-03-13 06:48:22

2022-06-03 09:30:31

店鋪W3C體系渲染

2022-12-14 18:40:04

得物染色環(huán)境

2022-05-12 11:41:16

開發(fā)框架程序

2023-02-08 18:33:49

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

2010-08-20 11:18:49

Exchange Se

2023-07-07 19:26:50

自建DTS平臺

2013-03-19 09:56:36

云計算遷移
點贊
收藏

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

久久九九99视频| 亚洲天堂一区二区三区四区| 婷婷六月综合亚洲| 精品卡一卡二| 国产一区二区视频网站| 国产99久久久国产精品成人免费| 欧美性xxxx极品hd满灌| 国产伦精品一区二区三区照片91| 日日夜夜综合网| japanese国产精品| 制服视频三区第一页精品| 日本xxxxx18| 五月婷婷综合久久| 蜜臀国产一区二区三区在线播放 | 中文字幕日韩av综合精品| 少妇一级淫免费放| 婷婷色在线资源| 91一区一区三区| 国产精品成人va在线观看| 欧美性x x x| 神马午夜久久| 91麻豆精品国产91久久久久久| 精品人妻人人做人人爽| 欧美色图另类| 国产二区国产一区在线观看| 91av在线影院| 成人在线观看免费完整| 亚洲免费福利一区| 91精品国产综合久久精品| 成人免费aaa| 超碰公开在线| 日本一区二区成人在线| 粉嫩高清一区二区三区精品视频 | 国产精品久久久久久69| 国产午夜精品一区二区三区欧美| 中文字幕视频在线免费欧美日韩综合在线看 | 欧美日韩一区二区三区四区| 黄色片免费在线观看视频| 日韩欧美电影在线观看| 毛片av一区二区| 4438全国成人免费| 午夜精品一区二区三区视频| 国产最新精品| 亚洲激情成人网| 三级黄色片免费观看| 456亚洲精品成人影院| 亚洲国产美国国产综合一区二区| 亚洲激情一区二区| 欧美午夜黄色| 不卡的看片网站| 亚洲一区免费网站| 一卡二卡在线观看| 久久国产免费| 97精品视频在线观看| 久久免费看少妇高潮v片特黄| 九九精品久久| 亚洲少妇激情视频| 三级视频网站在线观看| 无码国模国产在线观看| 欧美另类高清zo欧美| 爆乳熟妇一区二区三区霸乳| 亚洲男人av| 五月天激情小说综合| 亚洲中文字幕无码一区二区三区| 欧美日韩在线看片| 国产精品久久久久久久第一福利| 日韩精品最新在线观看| 免费一级在线观看| 久久婷婷一区二区三区| 免费久久久一本精品久久区| 外国精品视频在线观看| 99热99精品| 久久99导航| 熟妇人妻中文av无码| 成人一级视频在线观看| 成人欧美一区二区三区视频| 成人福利小视频| 成人免费黄色在线| 激情五月综合色婷婷一区二区| 国产女人18毛片水18精| 国产精品白丝jk黑袜喷水| 91精品天堂| 亚洲国产精品久久久久爰性色| 国产激情一区二区三区四区| 99在线视频首页| 丰满熟女一区二区三区| av在线免费不卡| 欧美日韩精品免费观看视一区二区| 天堂在线资源库| 国产ts人妖一区二区| 国产精品yjizz| 三级在线电影| 欧美国产精品久久| 中文字幕一区二区中文字幕| 亚洲91av| 狠狠躁18三区二区一区| 激情婷婷综合网| 国产一区二区| 亚洲国产精品久久| 亚欧洲乱码视频| 欧美高清视频手机在在线| 久久精品夜夜夜夜夜久久| 国模无码国产精品视频| 亚洲综合激情| 国产男人精品视频| 亚洲欧美另类综合| 久久九九全国免费| 亚洲 欧洲 日韩| caoprom在线| 欧美亚洲综合在线| 91小视频在线播放| 林ゆな中文字幕一区二区| 亚洲欧美自拍一区| 国内毛片毛片毛片毛片毛片| 国产综合婷婷| 97国产精品久久| 中文字幕日日夜夜| 不卡在线观看av| 亚洲福利av在线| www.综合网.com| 欧美日韩色综合| 午夜男人的天堂| 日韩电影免费网址| 国色天香2019中文字幕在线观看| 日本精品入口免费视频| 国产成人99久久亚洲综合精品| 欧美日韩三区四区| 日本片在线看| 欧美中文字幕一区| 国产精品久久久久久亚洲av| 欧美电影免费| 青青在线视频一区二区三区| 99在线精品视频免费观看软件| 久久久久久久电影| 潘金莲一级淫片aaaaaa播放1| 日韩免费电影| 亚洲国产精品字幕| 九九九久久久久| 久久99这里只有精品| 玛丽玛丽电影原版免费观看1977| 黄色成人影院| 欧美视频一区在线观看| 国产精品无码毛片| 国产精品magnet| 国产精品视频永久免费播放| 亚洲欧美日本在线观看| 亚洲国产精品一区二区尤物区| 国产成年人视频网站| 精品国产精品久久一区免费式| 亚州欧美日韩中文视频| 888奇米影视| 中文字幕免费不卡在线| 99精品视频在线看| 美女午夜精品| 国外成人在线播放| 黑人乱码一区二区三区av| 亚洲三级在线免费| 手机版av在线| 国产精品伦理久久久久久| 国产精品男人的天堂| 国产主播福利在线| 一本一本大道香蕉久在线精品| 国产视频久久久久久| 欧美激情麻豆| 99视频免费观看| 伊人222成人综合网| 91精品一区二区三区久久久久久| 午夜国产福利视频| 久久国产福利国产秒拍| 亚洲综合网中心| 国产精品久久久免费观看| 精品视频在线一区| 久久久国产一区二区三区| 91麻豆成人精品国产免费网站| 国产精品久久久久久久久晋中| 九色91popny| 日韩一区自拍| 亚洲综合自拍一区| 在线heyzo| 欧美精品一区二区三区一线天视频| 久久国产在线观看| 成人中文字幕在线| 黄色www网站| 亚洲理论电影| 国产精品久久久久久久7电影 | 国产伦精一区二区三区| 天堂а√在线中文在线| 亚洲国产精品免费视频| 国产做受高潮69| 三级在线观看网站| 一本色道久久加勒比精品| b站大片免费直播| 免费观看成人av| 51xx午夜影福利| 北条麻妃一区二区三区在线观看 | 国产一卡二卡在线| 国产精品乡下勾搭老头1| 加勒比成人在线| 九一精品国产| 成人中文字幕+乱码+中文字幕| 日本h片在线观看| 精品亚洲一区二区三区| 中文字幕一区2区3区| 亚洲精品成人悠悠色影视| 国产xxxx视频| 蜜乳av一区二区| 日韩小视频网站| 婷婷综合一区| 成人免费福利在线| 天堂√8在线中文| 久久视频在线观看免费| 欧美自拍偷拍第一页| 在线影院国内精品| 久久影院一区二区| 国产欧美日韩不卡| 日韩精品xxx| 日产国产高清一区二区三区| 屁屁影院ccyy国产第一页| 成人av动漫| 国产日韩在线免费| jizzjizz中国精品麻豆| 色老头一区二区三区在线观看| 99国产精品久久久久久久成人| 精品国产91乱高清在线观看| 天堂网av2018| 91影院在线免费观看| 国内av一区二区| 久久精品三级| 日韩视频在线视频| 国产精品99一区二区三区| 开心色怡人综合网站| 成人国产激情在线| 97色伦亚洲国产| 色呦呦在线视频| 久久精品影视伊人网| 国产www.大片在线| 亚洲黄在线观看| www.精品视频| 777奇米四色成人影色区| 丁香社区五月天| 亚洲一二三四久久| 天天操天天操天天操天天操天天操| 久久久久久亚洲综合| 精品伦一区二区三区| 天堂资源在线中文精品| 僵尸世界大战2 在线播放| 久久精品国内一区二区三区水蜜桃 | 日韩av首页| 日韩免费观看在线观看| aa级大片免费在线观看| 欧美裸体男粗大视频在线观看| 91精品国产综合久久久久久豆腐| 亚洲开心激情网| 国产叼嘿视频在线观看| 制服丝袜中文字幕亚洲| 一本色道久久综合无码人妻| 欧美性一区二区| 中文在线免费看视频| 欧美午夜理伦三级在线观看| 波多野结衣电车痴汉| 在线亚洲+欧美+日本专区| 久久精品视频1| 色婷婷综合久久久中文字幕| 国产91国语对白在线| 色婷婷综合视频在线观看| 国产成人精品亚洲男人的天堂| 亚洲综合在线观看视频| 欧美xxxx黑人xyx性爽| 一区二区三区精品在线| 朝桐光av在线| 亚洲最大成人综合| 欧美成人精品欧美一级乱黄| 精品高清美女精品国产区| 亚洲天堂一区在线| 一本色道**综合亚洲精品蜜桃冫 | 欧美亚洲国产一区二区三区| 18国产免费视频| 7777女厕盗摄久久久| 国产手机视频在线| 日韩午夜激情电影| 国产精品香蕉国产| 天堂网在线最新版www中文网| 国产成人综合精品| 精品中文在线| 欧美中日韩免费视频| 911久久香蕉国产线看观看| 欧美亚洲精品一区二区| 久久精品国产成人一区二区三区| 亚洲熟女一区二区三区| 久久蜜臀精品av| 亚洲国产精品久| 在线观看中文字幕不卡| 国产高清视频免费| 亚洲国产婷婷香蕉久久久久久| 视频三区在线| 久久久久久成人精品| 97精品国产综合久久久动漫日韩| 国产99午夜精品一区二区三区 | 亚洲一区二区在线看| 亚洲东热激情| 亚洲妇熟xx妇色黄蜜桃| 26uuu成人网一区二区三区| 国产精品白丝喷水在线观看| 欧美性生活大片免费观看网址| 国产伦一区二区| 亚洲香蕉成人av网站在线观看 | 在线精品视频一区二区三四| 亚洲成人第一区| 日韩在线欧美在线| 在线毛片观看| 国产精品视频免费一区二区三区| 日韩欧美精品综合| 亚洲精品乱码久久久久久自慰 | 91激情视频在线观看| 亚洲va韩国va欧美va精品| 国产精品爽爽久久久久久| 亚洲视频在线免费看| 麻豆网站免费在线观看| 91视频在线免费观看| 国产精品毛片久久| 在线观看免费黄网站| 26uuu色噜噜精品一区| 黄色小视频在线免费看| 欧美一区二区三区视频在线观看| 成人高潮成人免费观看| 欧美做受高潮1| 嫩草国产精品入口| 丁香花在线影院观看在线播放| 国产精品自拍三区| 秋霞欧美一区二区三区视频免费| 在线影院国内精品| 黄色电影免费在线看| 青青草原成人在线视频| 亚洲bt欧美bt精品777| 国产h视频在线播放| 99久久er热在这里只有精品15| 久操视频免费在线观看| 日韩一级欧美一级| a视频在线播放| 亚洲综合社区网| 一区二区中文| 久久久久国产免费| 亚洲香肠在线观看| 乱精品一区字幕二区| 欧美激情在线狂野欧美精品| 国产伦精品一区二区三区免费优势| 日韩中文字幕在线不卡| 国产白丝精品91爽爽久久| 国产一级片免费观看| 亚洲高清福利视频| 国产拍在线视频| 欧美久久久久久一卡四| 石原莉奈一区二区三区在线观看| 337人体粉嫩噜噜噜| 欧美三级视频在线观看| 免费黄色在线| 不卡一卡2卡3卡4卡精品在| 国语精品一区| 一区二区三区少妇| 欧美亚洲一区二区在线观看| 免费观看成人高潮| 91嫩草在线视频| 亚洲午夜av| 亚洲欧美色图视频| 日本乱码高清不卡字幕| 欧美18hd| 国产精品免费观看高清| 性久久久久久| 美国一级片在线观看| 日韩欧美黄色影院| 校园春色亚洲| 五月天色婷婷综合| 成人午夜免费电影| 四虎影院在线免费播放| 久久中文字幕国产| 久久中文资源| 性欧美videossex精品| 一二三区精品视频| 男人天堂亚洲二区| 91久久国产综合久久91精品网站| 国语自产精品视频在线看8查询8| 好吊日免费视频| 欧美一区三区四区| 亚洲插插视频| 男同互操gay射视频在线看| av不卡在线播放| 夜夜嗨aⅴ一区二区三区| 欧美黑人视频一区| 精品国产一区二区三区四区| 男人操女人下面视频| 色天天综合久久久久综合片| а天堂中文在线官网| 欧美黑人xxxxx| 国产麻豆精品在线| 日韩 国产 欧美| 欧美猛交免费看| 成人同人动漫免费观看| 一级少妇精品久久久久久久| 欧美区视频在线观看| 在线中文字幕播放|