初次体验Tauri和Sycamore(3)通道实现


原创作者:庄晓立(LIIGO)
原创时间:2025年03月10日(发布时间)
原创链接:https://blog.youkuaiyun.com/liigo/article/details/146159327
版权所有,转载请注明出处。

tauri-splash


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 serializer
  private 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 order
        if (id === this.#nextMessageId) {
          this.#nextMessageId = id + 1
          this.#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_onChannel::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源码(上文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层)通过fetchCommand拉取后端缓存数据,处理乱序数据接收,执行用户层onmessage回调,完成单次数据传输。

我原来猜测通道Channel是Command之外另一种更高效的数据传输方案,但事实证明我错了。通过上述源码分析可知,Channel实际上是基于Command实现的更高层的逻辑抽象。Tauri通道发送数据,本质上还是调用Command,只是经过封装之后更适合“后端推送流式数据”应用场景。相比使用普通无通道Command传输数据,其区别在于工作模式:无通道传输,是前端单次主动拉取;有通道传输,是后端多次主动推送,且保证有序送达。

<think>好的,我需要帮助用户解决在Vue3Tauri实现桌面通知的问题。首先,我得回忆一下Tauri的相关知识。Tauri允许构建桌面应用,但桌面通知可能需要用到浏览器的Notification API或者Tauri自己的API。 用户提到的是Vue3Tauri结合,所以应该考虑在Vue组件中调用Tauri的API。记得Tauri提供了JavaScript的绑定,可以通过`@tauri-apps/api`来使用。可能需要检查Tauri的文档,看看是否有关于通知的模块。 先确定Tauri是否支持通知功能。查阅文档发现,Tauri的window模块中有个`Notification`类可以用来创建通知。但可能需要配置权限或者在tauri.conf.json中启用相关权限。 然后,在Vue3中,可以在组件的方法里调用Tauri的Notification API。比如,在按钮点击事件中触发通知。需要注意,浏览器环境下的Notification API可能Tauri的不同,所以必须使用Tauri提供的接口,而不是原生的。 接下来,代码示例部分。需要导入Notification类,然后创建实例并显示。同时,权限的问题可能需要处理,比如请求用户允许通知。不过Tauri可能默认有权限,但最好检查一下。 另外,用户可能需要自定义通知的标题、内容、图标等参数。所以在代码中要展示如何配置这些选项。还要考虑错误处理,比如权限被拒绝的情况。 然后,根据用户提供的引用,特别是引用[2]提到的创建简单示例来测试问题,建议用户先做一个最小化示例,确保通知功能本身可行,再集成到项目中。这可能帮助排除配置问题。 最后,确保代码符合Vue3的组合式API写法,比如使用setup语法糖,或者选项式API,根据用户项目情况。同时,提醒用户安装必要的依赖,比如`@tauri-apps/api`,并在Tauri配置中启用通知权限。</think>### Vue3 + Tauri 实现桌面通知的步骤及示例 #### 1. 配置 Tauri 权限 在`tauri.conf.json`中添加通知权限: ```json { "tauri": { "allowlist": { "notification": { "all": true } } } } ``` #### 2. Vue3 组件实现 ```vue <script setup> import { Notification } from &#39;@tauri-apps/api/notification&#39;; // 触发桌面通知的函数 const showNotification = async () => { try { // 检查权限(Tauri 默认授予权限) const isGranted = await Notification.isPermissionGranted(); if (isGranted) { new Notification({ title: &#39;操作成功&#39;, body: &#39;文件已保存至本地&#39;, icon: &#39;icon.png&#39; // 需将图标文件放在public目录 }).show(); } else { console.log(&#39;用户未授予通知权限&#39;); } } catch (error) { console.error(&#39;通知发送失败:&#39;, error); } }; </script> <template> <button @click="showNotification">点击通知</button> </template> ``` #### 3. 关键点说明 1. **权限管理**:Tauri 默认允许通知,但实际生产环境建议添加显式权限请求逻辑[^2] 2. **图标路径**:图标需放在`public`目录,打包时会被自动包含 3. **多平台兼容**:此代码在 Windows/macOS/Linux 均可运行,但图标格式推荐使用 PNG #### 4. 优化建议 - 添加通知点击事件处理(需结合Tauri事件系统) - 使用系统原生通知样式(通过`tauri.conf.json`配置`notification`模块) - 处理多窗口场景下的通知归属
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值