?
原創作者:莊曉立(LIIGO)
原創時間:2025年03月10日(發布時間)
原創鏈接:https://blog.csdn.net/liigo/article/details/146159327
版權所有,轉載請注明出處。
20250310 LIIGO備注:本文源自系列文章第1篇《初次體驗Tauri和Sycamore (1)》,從中抽取出來獨立成文(但并無更新和修訂),專注于探究Tauri通道的底層實現(實際上也沒有足夠底層)。理由:1.原文已經很長,需要精簡;2.原文主體是初級技術內容,僅這一節相對深入,顯得格格不入。(如無意外,這將是本系列文章的終結。)
20241118 LIIGO補記:出于好奇,簡單研究一下Tauri通道的底層實現。
在JS層,創建Channel對象生成通道ID,并關聯onmessage處理函數;在傳輸層,通過invoke()
調用后端Command,傳入Channel對象作為參數(實質上是傳入通道ID);在Rust層,根據通道ID構造后端Channel對象,向客戶端指定的Channel發送Message。如何向通道發送Message是后續關注的重點。
JS層創建Channel的源碼如下:
class Channel<T = unknown> {id: number// @ts-expect-error field used by the IPC serializerprivate readonly __TAURI_CHANNEL_MARKER__ = true#onmessage: (response: T) => void = () => {// no-op}#nextMessageId = 0#pendingMessages: Record<string, T> = {}constructor() {this.id = transformCallback(({ message, id }: { message: T; id: number }) => {// the id is used as a mechanism to preserve message orderif (id === this.#nextMessageId) {this.#nextMessageId = id + 1this.#onmessage(message) // 前端用戶收到此message// process pending messages// ...} else {this.#pendingMessages[id.toString()] = message}});}// ...
}function transformCallback<T = unknown>(callback?: (response: T) => void, once = false): number {return window.__TAURI_INTERNALS__.transformCallback(callback, once)
}
JS層Channel構造函數內部,調用transformCallback
為一個回調函數生成唯一ID(它基于Crypto.getRandomValues()
的實現能保證ID唯一嗎我存疑),并將二者關聯至window對象:window['_回調ID'] = ({message, id})=>{ /*...*/};
。此處生成的ID也稱為通道ID,將被invoke函數傳遞給Rust層(參見上文前端調用Command)。后端數據通過通道到達前端后,可通過通道ID反查并調用該回調函數接收后端數據。注意區分通道ID、消息ID和后文的數據ID。
Rust層通過JavaScriptChannelId::channel_on
和Channel::new_with_id
構造Channel對象實例。
impl JavaScriptChannelId {/// Gets a [`Channel`] for this channel ID on the given [`Webview`].pub fn channel_on<R: Runtime, TSend>(&self, webview: Webview<R>) -> Channel<TSend> {let callback_id = self.0;let counter = AtomicUsize::new(0);Channel::new_with_id(callback_id.0, move |body| {let i = counter.fetch_add(1, Ordering::Relaxed);if let Some(interceptor) = &webview.manager.channel_interceptor {if interceptor(&webview, callback_id, i, &body) {return Ok(());}}let data_id = CHANNEL_DATA_COUNTER.fetch_add(1, Ordering::Relaxed);webview.state::<ChannelDataIpcQueue>().0.lock().unwrap().insert(data_id, body);webview.eval(&format!("window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window['_' + {}]({{ message: response, id: {i} }})).catch(console.error)",callback_id.0))?;Ok(())})}
}
Channel::new_with_id
有兩個參數,一個是通道ID(或稱callback_id),一個是向前端發送數據的on_message函數。這個on_message的命名有誤導性,讓人以為是接收函數,但看Channel::send()
函數源碼可以確認on_message是發送函數。
impl<TSend> Channel<TSend> {fn new_with_id<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(id: u32,on_message: F,) -> Self {// ...}/// Sends the given data through the channel.pub fn send(&self, data: TSend) -> crate::Result<()> where TSend: IpcResponse, {(self.on_message)(data.body()?)}
}
Rust層Channel發送數據的實現代碼就在上面JavaScriptChannelId::channel_on(webview)
函數內部,即new_with_id()
的on_message參數閉包函數內,它主要干了如下幾件事:
- 生成數據ID(data_id):
let data_id = CHANNEL_DATA_COUNTER.fetch_add(1, Ordering::Relaxed);
- 將要數據存入發送緩存隊列并關聯data_id:
webview.state::<ChannelDataIpcQueue>()...insert(data_id, body)
- 生成JS代碼并提交給前端執行(分兩步):
webview.eval(JSCODE)
- fetch:
invoke('plugin:__TAURI_CHANNEL__|fetch', null, ...data_id...)
- callback:
window['_通道ID']({ message: response, id: {i} })
(調用JS端回調函數,{i}
為此通道內消息ID,即序號)
- fetch:
再看一下fetch
源碼(上文invoke('plugin:__TAURI_CHANNEL__|fetch', ...)
將調用此后端Command):
#[command(root = "crate")]
fn fetch(request: Request<'_>,cache: State<'_, ChannelDataIpcQueue>,
) -> Result<Response, &'static str> {if let Some(id) = request.headers().get(CHANNEL_ID_HEADER_NAME).and_then(|v| v.to_str().ok()).and_then(|id| id.parse().ok()){if let Some(data) = cache.0.lock().unwrap().remove(&id) {Ok(Response::new(data))} else {Err("data not found")}} else {Err("missing channel id header")}
}
fetch
命令的作用是從發送緩存隊列中取出與參數data_id關聯的數據返回給前端,同時從發送緩存隊列中移除。fetch執行后,通過通道發送的數據就從后端到了前端。注意時序,是后端主動提交JS代碼讓前端執行,前端才被動發起fetch調用,Tauri正是通過這種方式實現后端向前端“推送”數據。數據被推送至前端后,可能還要經歷緩存階段才提交給Channel用戶,確保用戶有序接收。
調用鏈:(JS層)創建Channel,發起調用后端某Command(傳入通道ID),(Rust層)把通道ID反序列化為Channel,將待發送數據緩存,調度前端執行JS代碼(webview.eval()
),(JS層)通過fetch
Command拉取后端緩存數據,處理亂序數據接收,執行用戶層onmessage回調,完成單次數據傳輸。
我原來猜測通道Channel是Command之外另一種更高效的數據傳輸方案,但事實證明我錯了。通過上述源碼分析可知,Channel實際上是基于Command實現的更高層的邏輯抽象。Tauri通道發送數據,本質上還是調用Command,只是經過封裝之后更適合“后端推送流式數據”應用場景。相比使用普通無通道Command傳輸數據,其區別在于工作模式:無通道傳輸,是前端單次主動拉取;有通道傳輸,是后端多次主動推送,且保證有序送達。
?