Intro
Tauri 2 应用本质上是两个独立进程的协作系统:前端进程负责用户界面渲染,后端进程(Rust 二进制)提供系统能力与业务逻辑。完整的启动流程需要先后端进程先行启动,由其托管并唤起前端页面,随后两者通过进程间通信(IPC)建立双向数据通道。只有两个进程均完成初始化并建立通信连接,应用才算真正启动就绪。
Tauri 项目分为两个进程
Tauri 应用采用双进程模型:
- Rust 核心进程(Core Process):用 Rust 编写,负责创建窗口、管理系统 API、处理 IPC(进程间通信)、安全控制等。这是整个应用的"后端大脑"。
- WebView 进程:使用操作系统原生 WebView(Windows 用 WebView2、macOS 用 WebKit、Linux 用 WebKitGTK)渲染前端界面(HTML/JS/CSS 或 React/Vue/Svelte 等)。前端不直接访问系统,只能通过 Rust 暴露的 commands(命令)进行安全 IPC 调用。
注:两个进程之间的通讯方式采用 IPC,对于此概念有疑问可以看后文『What is RPC』一节
理解这一协作模型后,下一步是观察项目文件结构:前端代码位于 src 目录,后端入口位于 src-tauri/src/main.rs,而 src-tauri/tauri.conf.json 则定义了二者如何关联及启动时的行为。后续章节将逐一拆解这些文件在启动流程中的具体作用。
Tauri 项目的文件结构
nix
my-tauri-app/
├── src-tauri/ # Tauri后端核心目录
│ ├── Cargo.toml # Rust项目配置文件
│ ├── Cargo.lock # 依赖版本锁定文件
│ ├── src/ # Rust源代码目录
│ │ ├── main.rs # 入口文件,包含Tauri命令定义
│ │ └── lib.rs # 库文件
│ ├── icons/ # 应用图标(多种尺寸和格式)
│ ├── target/ # Rust编译输出目录
│ └── tauri.conf.json # Tauri应用配置文件
│
├── src/ # 前端源代码目录(如React/Vue/Svelte)
│ ├── assets/ # 静态资源(图片、字体等)
│ ├── components/ # 前端组件
│ └── main.js # 前端入口文件
│
├── node_modules/ # 前端依赖目录
├── package.json # 前端项目配置文件
├── pnpm-lock.yaml # 包管理器锁定文件(若使用pnpm)
└── index.html # 前端HTML入口文件
main.rs 是 Rust 程序的入口文件,在 Tauri 2.x 中,一般只负责调用 run()。而位于 lib.rs 中的 run() 则实际负责初始化 Tauri 应用、配置前端窗口、定义系统托盘等核心逻辑。
阅读 Tauri 项目的后端部分一般会先从
main.rs开始入手
lib.rs 作为核心逻辑载体,通常会放:
- run() 函数(应用启动逻辑)
- command handlers(
#[tauri::command]) - 状态管理(state)
- 插件注册
- 窗口配置
注:在 Tauri v1.x 中,更多的函数被写在 main.rs 中,但这并不符合 Rust 的 Crate 设计哲学:
main.rs→ binary crate(入口)lib.rs→ library crate(逻辑)
run() 的职责
读到这里不难发现 Tauri 应用是通过在 main.rs 中调用 run() 启动的应用程序。有读者可能会好奇 main.rs 怎么知道 run() 在 lib.rs 中?
实际上,这是因为 Tauri 项目在生成时采用了库 + 二进制(lib + bin) 的标准 Rust 结构,而 main.rs 通过 crate 声明引入了 lib.rs 中导出的内容。
这样的对应关系在 src-tauri/Cargo.toml 中进行配置。不过 main.rs 和 lib.rs 的对应关系是默认的约定,可以不用管。要是改成别的对应关系就得去这里配置。
run() 函数总是从 tauri::Builder::default() 发起一个链式调用,如下所示:
rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(|app| {
#[cfg(desktop)]
setup_desktop(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
tauri::Builder::default() 是 Tauri 框架中的一个核心方法,用于创建一个默认配置的 Builder 实例。Builder 是 Tauri 应用的构建器,负责配置和启动 Tauri 应用。
Builder 实例含有一些默认配置,包括窗口设置、应用名称、图标等基础属性。还可以通过链式调用添加自定义逻辑。
在 Setup 函数中,通过条件编译完成了对不同平台的初始化操作,这是 Tauri 的常见做法
如果是 Rust 初学者,你可能好奇
|app| {...}是什么语法,实际上这是 Rust 的闭包,更通俗地讲,可以理解为 lambda 函数,详细内容参见附录『Lambda in Rust』一节
随后在 invoke_handler 中,注册 Rust 函数作为命令 ,使前端 JavaScript 代码能够调用这些 Rust 函数,这是 Tauri ++实现前后端通信的核心机制++ 。generate_handler! 和 #[tauri::command] 协作构成了 Tauri 命令系统的编译时代码生成链路
关于 Tauri 为什么要依靠
#[tauri::command]实现前后端通信的核心机制,参见附录『Invoke from Frontend』一节。更加进阶的内容,即generate_handler!和#[tauri::command]如何协作,参见附录『Code Generation Pipeline』一节
前端加载机制
开发模式:连接 Vite / Webpack 等 dev server
生产模式:加载嵌入二进制中的前端资源(通过 tauri_build 和 include_str! 或 rust-embed)
WebView 与前端的交互边界
通信机制
Tauri框架精心设计了两套强大且互补的通信机制,以应对不同的交互场景:即基于命令(invoke)的请求-响应模式,以及基于事件(listen/emit)的发布-订阅模式。
前端通过 invoke 与后端进行交互:这涉及的 #[tauri::command] 与 generate_handler! 已经在前文提到了。通过这两个宏的配合,可以在后端定义可供前端调用的函数。
这种机制主要负责前端向后端发起请求并同步获取返回结果。它适用于直接执行特定后端操作并期望立即获得处理结果的场景,例如保存文件、查询数据库等,其行为模式类似于传统的API调用。
与此不同,事件系统(listen/emit)则提供了一种双向、异步、解耦的通信范式。它允许应用中的任一部分(前端或后端)发布(emit)一个具名事件,并可携带数据;同时,另一部分则可以监听(listen)这些事件,并在事件发生时触发相应的处理函数。
显然,这个机制既可以用于后端通知前端,也可以用于前端通知后端。
这一机制完美解决了后端主动向前端推送实时更新、进度通知、广播消息等问题,也允许前端向后端发送非阻塞的"即发即忘"式通知,从而极大地增强了应用的响应性、灵活性和模块化程度。
关于Tauri 的事件系统的细节,可以参阅附录『Event System in Tauri』一节
事件监听器可以在应用的任何时刻、任何地方注册,不一定局限在 setup 中,尽管在这里是相当常见的。
在 WebView 完成页面加载后,通过 WebView 注入的 window.__TAURI__ 对象,Tauri 核心会自动注入该对象。这意味着前端 JavaScript 代码必须等待 window.__TAURI__ 可用后,才能安全使用任何 Tauri API。
实际的动态注入由 Tauri 框架完成,window.__TAURI__ 本质上是一段 JavaScript 桥接代码,其核心作用正是为前端 JavaScript 提供与 Rust 后端进行进程间通信(IPC)的能力。更进一步地说,其本质是将 Rust 与 JavaScript 之间定义好的通信协议,以 JavaScript 可直接调用的函数形式封装起来,让前端代码能以简单的方式发起跨语言调用,而无需手动处理底层序列化、消息传递和 WebView IPC 接口。
显然,在前端不论是 Invoke 还是
listen/emit都需要依靠window.__TAURI__才能实现
总结:从启动到就绪的完整时序
启动流程
-
Rust 二进制启动
用户双击 exe(或通过终端运行),操作系统加载 Rust 编译后的二进制文件,执行
src-tauri/src/main.rs中的fn main()函数。 -
Tauri Builder 初始化
tauri::Builder::default()创建构建器对象,开始配置应用的基本信息(窗口、菜单、插件等)。 -
执行 setup 钩子
.setup(|app| { ... })被调用。这是开发者可以介入的最早时机,通常在这里:- 注册全局事件监听
- 初始化插件
- 准备后端共享状态
此阶段仍在 Rust 主线程执行。
-
创建主窗口
Tauri 根据
tauri.conf.json中的窗口配置,创建 WebviewWindow(包含原生标题栏、边框等)。 -
启动 WebView 并加载前端页面
WebView(Windows 用 WebView2、macOS 用 WKWebView、Linux 用 WebKitGTK)开始加载前端资源,通常是
index.html(由 Vite、Next.js 等打包生成)。 -
前端页面加载完成
浏览器内核触发
DOMContentLoaded或load事件。此时 HTML、CSS 已解析完成。 -
Tauri 注入通信桥接代码
Tauri 核心通过 WebView 的脚本注入机制,向页面注入一段 JavaScript 代码,创建
window.__TAURI__全局对象(或通过@tauri-apps/api包间接提供)。此时前端可以安全地使用
invoke、listen、emit等 API。 -
前端框架初始化与渲染
前端 JavaScript 执行:
- React/Vue/Svelte 等框架开始挂载组件
- 执行
useEffect、onMounted等生命周期钩子 - 可能发起第一个
invoke调用或设置事件监听
用户界面逐渐变得可交互。
-
后端进入事件循环
tauri::Builder::run(tauri::generate_context!())被调用,后端主线程进入事件循环(event loop)。从这一刻起,Rust 开始持续处理:
- 来自前端的 IPC 请求(invoke)
- 事件发送与接收(emit/listen)
- 窗口事件、系统消息、托盘点击等
启动完成的明确标志
Tauri 程序真正启动完成 的标志是以下两点同时成立:
- 后端事件循环已运行 :
run()方法已执行,Rust 侧能够持续响应 IPC 消息和系统事件。后端"活起来"了。 - 前端页面完全可交互 :前端 JavaScript 执行完毕,
window.__TAURI__已注入,用户可以正常点击按钮、输入内容、看到界面响应。此时invoke、listen、emit等通信功能均可正常工作。
只有同时满足以上两点,应用才进入可用状态 。
如果只看到窗口出现但点击无反应(前端还没初始化完),或后端还没进入事件循环(无法响应 invoke),都不能算启动完成。
实际开发中的对应位置
- Rust 侧主要逻辑在
src-tauri/src/main.rs的setup和run()中。 - 前端侧初始化逻辑通常放在
src/main.tsx或App.tsx的顶层useEffect中。 - 如果需要确认启动完成,可以在前端监听
tauri://window-created或自定义一个启动完成事件,从 Rust 发出,通知前端"后端已就绪"。
这个流程在 Tauri v1 和 v2 中核心时序基本一致,v2 在多窗口和插件初始化时更加清晰有序。
附录
What is IPC
IPC(Inter-Process Communication,进程间通信)是操作系统必备且常见的功能,它用于让不同进程之间交换数据或协调工作。
基于数据传输的通信方式,有:
-
管道(Pipe)
一种最简单的通信方式,可以把它理解为"一个单向的数据通道",一个进程写数据,另一个进程读数据。常见于父子进程之间,比如命令行里的
|。 -
消息队列(Message Queue)
类似"消息收发箱",进程可以把一条一条的消息放进去,其他进程按顺序或按类型取出来。相比管道,它更灵活,可以传递结构化数据。
-
共享内存(Shared Memory)
多个进程共同使用同一块内存区域,就像"共同编辑一张白板"。它的优点是速度非常快,但缺点是需要额外机制来避免多个进程同时修改数据导致混乱。
基于同步/控制的通信方式:
-
信号(Signal)
可以理解为"提醒机制"或"通知",一个进程可以向另一个进程发送信号,比如告诉它"该停止了"或"有事情发生了"。但它只能传递很简单的信息。
-
信号量(Semaphore)
用于控制多个进程对资源的访问,可以理解为"门口的计数器"。例如限制最多只有几个进程可以同时访问某个资源,常用于避免数据冲突。
-
互斥锁(Mutex)
类似"一把锁",同一时间只允许一个进程访问某个资源。谁拿到锁谁用,用完再释放,防止多个进程同时操作同一数据。
-
条件变量(Condition Variable)
用于让进程"等待某个条件成立"。比如一个进程可以等待数据准备好,另一个进程在准备好后通知它继续执行。
IPC in Tauri
Tauri 的 IPC 本质上采用异步消息传递(Asynchronous Message Passing)模型,属于基于数据传输的通信方式中的消息传递(Message Passing)类别。它主要通过两种核心原语实现:
invoke(命令,请求-响应模式):前端调用 Rust 后端的函数,支持参数传递和返回值(类似fetchAPI)。emit / listen(事件,单向通知模式):支持双向发射(Frontend ↔ Core),适合生命周期事件、状态变更等 fire-and-forget 场景。
内部会结合必要的同步机制(如请求 ID 匹配)来保证消息有序处理和响应对应,但开发者几乎无需直接操作底层同步原语。
与传统操作系统 IPC 不同,Tauri 的 IPC 不依赖 管道(单向字节流)、共享内存或 OS 级消息队列,而是通过序列化消息 在前端 WebView 进程和 Rust 后端进程之间通信。v1 主要使用 JSON 序列化;v2 则进行了重大重构,支持更高效的二进制 payload(Raw Payloads),可直接传递 ArrayBuffer 等,避免了 JSON 在大对象或二进制数据上的开销。
这种设计优先保障了安全性 (所有消息均经过能力系统 / Capabilities 校验)和跨平台一致性,同时对开发者非常友好。异步消息传递既保留了传统消息传递的灵活性,又借助 Rust 的类型安全和 Tauri 的权限系统提供了更高的安全性,非常适合构建现代桌面和移动应用。
Tauri v2 的 IPC 实现细节
Tauri v2(当前主流版本) 对 IPC 层进行了重写,主要采用自定义 URI 协议(custom protocol) ,并在必要时回退到 postMessage。具体来说:
- 核心机制 :前端通过
fetch(或底层等价实现)向自定义协议(如ipc://或http://ipc.localhost)发起请求,WebView 将其拦截并交给 Rust 侧处理,而非真正走网络。Rust 侧通过 wry/tao 注册的register_uri_scheme_protocol(或等价内部实现)接收请求,并返回响应。 - 与 postMessage 的关系 :并非简单的"postMessage + custom protocol 混合",而是以 custom protocol 为主 (Windows、macOS 等平台优先使用,以提升性能和支持二进制数据),在 custom protocol 不可用或失败时回退到传统的
window.ipc.postMessage(由 WebView 自身提供)。Linux 等平台可能根据 WebKitGTK 版本选择合适方式。 - 底层通道 :完全在 WebView 内部完成,OS 不参与任何系统级 IPC(无管道、无共享内存、无 OS 消息队列)。它利用 WebView 的协议注册机制,将通信"伪装"成本地 HTTP 请求,但本质仍是 WebView 与宿主进程间的内部消息传递通道。
相比 v1,v2 的 custom protocol 实现显著提升了性能(支持二进制、无需全部 JSON 字符串化),同时保持了高度的安全性和跨平台一致性。开发者仍通过统一的 @tauri-apps/api 接口使用,无需关心底层差异。
Compare to Traditional IPC
-
优点:
- 安全:所有通信都经过序列化 + 能力系统(Permissions)校验,后端可以精确控制前端能调用什么。
- 简单易用:开发者无需关心底层序列化、线程安全等问题。
- 跨平台一致:无论 Windows、macOS、Linux,还是移动端,API 体验相同。
- v2 版本大幅优化了性能,支持二进制数据传输和新的 Channels 机制,适合更高频或较大 payload 的场景。
-
缺点(相对共享内存):
- 有序列化/反序列化开销(不过 v2 已显著改善)。
- 不适合极高频、超大块数据的零拷贝传输(Tauri 优先选择安全而非极致性能)。
总体而言,Tauri 的 IPC 设计在安全、性能和开发者体验之间取得了优秀的平衡,是其轻量级、现代桌面/移动应用框架的重要基石。
Lambda in Rust / Closure
Rust 习惯叫"闭包"(closure)而不是 lambda
Closure Syntax
Rust中的Lambda表达式通常被称为闭包,使用简洁的语法捕获周围环境中的变量。闭包在Rust中是匿名函数,可以像普通函数一样被调用,但支持捕获外部变量。
rust
|参数列表| -> 返回类型 { 函数体 }
例如
rust
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 输出: 3
其中,参数列表与函数参数类似,但类型通常可以省略(由编译器推断),当然也可以显式指明:
rust
let add = |a: i32, b: i32| -> i32 { a + b }; // 明确指定类型
let add: fn(i32, i32) -> i32 = |a, b| a + b; // 或者通过变量类型标注
Closures Traits
闭包自动实现以下特性之一:
Fn:不可变借用捕获。FnMut:可变借用捕获。FnOnce:所有权转移捕获(只能调用一次)。
不可变借用(默认)
rust
let x = 10;
let print_x = || println!("{}", x);
print_x(); // 输出: 10
Invoke from Frontend
invoke_handler 在 Tauri 应用中的作用是注册 Rust 函数作为命令 ,使前端 JavaScript 代码能够调用这些 Rust 函数。这是 Tauri 实现前后端通信的核心机制。
能够在 invoke_handler 中注册的函数必须由 #[tauri::command]这一宏进行标记。
这是因为#[tauri::command] 会为函数生成必要的元数据,包括:
- 函数签名信息
- 参数和返回值的类型信息
- 命令名称(默认使用函数名)
这些元数据是 generate_handler! 宏识别和注册命令的依据。
另一方面来讲,generate_handler! 宏的作用是收集所有被 #[tauri::command] 标记的函数 ,并生成一个统一的命令处理程序。如果函数没有被 #[tauri::command] 标记,generate_handler! 就无法识别它,因为:
- 函数缺少必要的元数据
- 未经过序列化/反序列化处理
- 不具备错误处理能力
进一步探究 #[tauri::command],这个宏所做的远不止是"标记"一个函数。它是在编译时执行一段代码生成程序,为你的普通函数包裹上一层 Tauri 框架能理解和调用的"外壳"。
具体来说,它会为你完成以下几项关键工作:
- 参数解析与反序列化:前端传来的数据是 JSON 格式的字符串。这个宏会生成代码,自动将 JSON 反序列化为 Rust 函数期望的具体类型(如
String、i32或自定义结构体)。这是任何"胶水代码"都必须做的基础工作。 - 依赖项自动注入:Tauri 命令可以请求一些特殊参数,比如 Window(调用者窗口)或
State<T>(全局状态)。这个宏生成的代码会识别这些特殊类型,并从上下文中自动"注入"它们,你无需手动传递。generate_handler!宏配合工作,就是负责构建这个"上下文"并建立函数名到处理函数的映射。 - 返回值的序列化与发送:函数返回的结果(比如
String或自定义结构体)需要被序列化回 JSON,并通过 IPC 通道安全地发送回前端。这部分逻辑也由宏生成的代码自动完成。 - 异步与错误处理:无论你的函数是同步还是异步的,这个宏都能生成合适的包装代码来处理
.await和Result(成功/失败)类型,确保错误能被优雅地传递回前端。
所以这并不是"既然都知道函数函数在哪了,直接通过函数名注册",就能解决的事情。
就技术上来讲,Rust 缺乏稳定的反射API。如果是 Java 或 C# 那样的语言,在运行时能保留完整的类型信息。但 Rust 的函数、结构体等信息在编译后就被"擦除"了。
即使退一步讲,为之实现了一套运行时反射,但是这种运行时的类型检查不仅会拖慢应用启动速度,还会因为无法剔除无用代码而导致最终二进制体积显著增大。
Rust 作为一门零成本抽象的静态语言,其设计哲学就是尽可能将工作从运行时转移到编译时
当然,读到这里的读者可能有疑问:那对于一个中大型项目来说,需要注册的函数数目很大,这时候在 generate_handler! 函数中岂不是参数多到爆炸?
还真是。目前社区已经有一些缓解方法,Tauri 团队也正在讨论几个解决方案,不过显然作为入门文章,这不属于本文的讨论范围。
Code Generation Pipeline
前文提到 generate_handler! 宏与 #[tauri::command] 构成了 Tauri 命令系统的编译时代码生成链路。两者分工明确:#[tauri::command] 负责函数层面的包装 ,generate_handler! 负责调度层面的注册。
| 组件 | 职责 | 缺失后果 |
|---|---|---|
#[tauri::command] |
将任意函数适配为统一签名 | generate_handler! 无法获得类型一致的函数指针 |
generate_handler! |
构建函数名到包装函数的映射 | 包装函数存在但无法被前端发现和路由 |
1. #[tauri::command] 生成的包装函数
当一个函数被 #[tauri::command] 标记时,过程宏会为原始函数生成一个符合 Tauri IPC 调用约定的包装函数。示例如下:
rust
#[tauri::command]
fn add(a: i32, b: i32) -> i32 {
a + b
}
宏展开后大致等效于:
rust
// 原始函数保持不变
fn add(a: i32, b: i32) -> i32 {
a + b
}
// 生成的包装函数
fn add_wrapper(state: tauri::State, args: tauri::ipc::Args) -> Result<tauri::ipc::Response, tauri::ipc::Error> {
// 1. 从 args 中反序列化参数 a, b
// 2. 调用原始 add
// 3. 将返回值序列化为 JSON
}
这个包装函数具有固定的函数签名 :接受 State 和 Args,返回 Result<Response, Error>。这是 Tauri 调度器能够统一调用的前提。
2. generate_handler! 生成的调度器
generate_handler! 宏接收多个函数标识符(如 add, subtract),为每个函数生成一个类型安全的调度条目:
rust
tauri::generate_handler![add, subtract]
宏展开后会生成一个函数指针数组或元组,其中每个元素包含:
- 函数名的字符串表示(用于前端匹配)
- 指向上述包装函数的指针
3. 两者的完整协作流程
text
[编译时]
原始函数 add
│
├── #[tauri::command] 展开
│ │
│ └── 生成 add_wrapper (统一签名)
│
└── generate_handler![add] 展开
│
└── 生成调度条目: ("add", add_wrapper)
[运行时]
前端调用 invoke("add", { a: 1, b: 2 })
│
▼
Tauri IPC 层接收
│
▼
在调度条目中查找 "add"
│
▼
调用对应的 add_wrapper
│
▼
add_wrapper 反序列化 → 调用原始 add → 序列化返回
Event System in Tauri
Architecture
在架构上,Tauri 的消息系统分为三个核心角色:
- 事件发射器:Rust 后端或前端都可以作为生产者,调用 emit 发送事件。
- 事件总线:Tauri 运行时(Runtime)维护着一个全局的监听器注册表。当事件被发出时,总线负责根据
EventTarget进行路由分发 。 - 事件监听器:前端或后端注册的回调函数,等待事件触发。
在这里,任何事件的内容都是由事件负载(Payload)记录的,它本质是 JSON 字符串。它可以由前端或者后端代码行程,由于规范是统一的,这在 Rust 和 JavaScript 之间架设了一座标准化的桥梁。
具体来说,Tauri 在两端使用了对等的序列化策略:
- Rust → 前端:Rust 侧的值通过
serde_json::to_string()序列化为 JSON,前端收到的event.payload已经是解析好的 JavaScript 对象 - 前端 → Rust:前端的对象通过
JSON.stringify()序列化,Rust 侧收到的是&str或可以通过serde_json::from_str()反序列化
这也是为什么 Tauri 官方不建议用事件系统发送大文件或进行高频低延迟的数据流传输------序列化和 JSON 解析的开销在极端场景下会成为瓶颈
换言之,Rust后端只能发送实现#[derive(Serialize)]的数据
进阶地,如果你的场景需要高频通信(如每秒几百次),建议:
-
聚合多个数据点后再发送
-
使用 Tauri 的 Channels 机制(基于二进制流)
-
考虑 Commands + 自定义序列化
Type of Event
Tauri v2 明确了两种事件分发范围 :
- 全局事件:发送给所有窗口和所有监听者。
- 特定 Webview 事件:只发送给指定标签(Label)的窗口。
因此,如果意图实现前端 → 前端(跨窗口)的事件,需要先发给 Rust 后端,由其代为转发
由于 Tauri 的架构是:一个 Rust 主进程 + N 个 WebView 窗口。虽然整个 Tauri 应用只有一个 Rust 主进程,但每个窗口有自己的"上下文",如
rust
#[tauri::command]
fn window_specific_command(window: tauri::Window) -> String {
// 每个窗口调用这个命令时,会传入自己的 Window 实例
format!("当前窗口标签: {}", window.label())
}
因此前端向后端发送数据时,可以通过特定 WebView 事件区分区分来源:
rust
app.listen_global("some-event", |event| {
if let Some(label) = event.window_label() {
match label {
"main" => println!("来自主窗口"),
"child" => println!("来自子窗口"),
_ => println!("其他窗口"),
}
}
});