Rust in Everything
基于 tauri 2.12.5

一次启动

主线位置:启动主线从头到尾。本页不深入任何一层,只把「一个窗口加载出页面」经过的函数串起来:先跑一个打满日志的实验,看每一步发生在哪个线程、相隔多久;再按时间顺序逐步走一遍,每一段末尾链到详细讲它的那一章。
sequenceDiagram
  participant M as main 线程
  participant T as tao 事件循环
  participant TA as tauri
  participant W as wry
  participant K as WebKit
  participant R as tokio 线程
  M->>TA: generate_context 取 Context
  M->>TA: Builder::build
  TA->>T: Wry::new 建 EventLoop
  M->>T: App::run
  T->>TA: StartCause Init 即 Ready
  TA->>TA: setup 按配置建窗口
  TA->>W: create_webview 带协议和脚本
  W->>K: 建 WKWebView 并 loadRequest
  TA->>TA: 用户 setup 钩子 和 Ready 回调
  K->>W: 请求 tauri://localhost
  W->>R: tauri 协议 spawn 取资源
  R-->>K: index.html
  K->>K: 先跑初始化脚本 再跑页面脚本
步骤文件 · 函数关键的一两行详见
1. 编译期生成 Contexttauri-macros/src/context.rs · generate_contextdev: cfg!(not(feature = "custom-protocol"))generate_context!
2. 建 Builder,生成 invoke keyapp.rs · Builder::newlet invoke_key = crate::generate_invoke_key().unwrap();Builder 与 App
3. build:管理器、事件循环、核心插件app.rs · Builder::build;tauri-runtime-wry · Wry::newlet mut runtime = R::new(runtime_args)?;Builder 与 App、tauri-runtime-wry
4. 进事件循环,第一个事件是 Readytauri-runtime-wry · handle_event_loop;tao · AppState::launchedEvent::NewEvents(StartCause::Init) => { callback(RunEvent::Ready); }tao
5. Ready 时建配置里的窗口,再跑用户的 setupapp.rs · setupWebviewWindowBuilder::from_config(app.handle(), window_config)?.build()?;Builder 与 App
6. 补协议和初始化脚本manager/webview.rs · prepare_pending_webviewpending.register_uri_scheme_protocol("tauri", move |webview_id, request, responder| {初始化脚本
7. 交给 wry 建 WKWebViewtauri-runtime-wry · create_webview;wry · new_ns_viewconfig.setURLSchemeHandler_forURLScheme(tauri-runtime-wry、wry 在 macOS 上
8. 请求 tauri://localhost,在 tokio 线程上取资源protocol/tauri.rs · get;manager/mod.rs · get_assetcrate::async_runtime::spawn(async move {tauri:// 协议
9. 页面脚本执行时 __TAURI_INTERNALS__ 已经在了wry · initWKUserScriptInjectionTime::AtDocumentStart初始化脚本

实验:给每一步打上时间和线程

模板是 tauri 卷首页说的那个隐藏窗口应用,打开 tauri 的 tracing feature。tauri、tauri-runtime-wry 和 wry 在启动、建窗口、自定义协议、IPC 上都有 span;为了让输出短,实验自己写了一个很小的 tracing layer,每个 span 进入时打一行 >、退出时打一行 <、事件打一行 *,行首是距进程启动的毫秒数和线程名,字段只留 uri、cmd、request 这几个(layer 的代码只是格式化,从略)。main.rs 在关键位置自己也打一行(exp:):

mod trace;
use tauri::{Manager, RunEvent};
use tracing_subscriber::prelude::*;

fn step(what: &str) {
  tracing::info!(target: "exp", "{what}");
}

#[tauri::command]
fn report(app: tauri::AppHandle, msg: String) {
  step("command report");
  println!("REPORT {msg}");
  app.exit(0);
}

fn main() {
  trace::init();
  tracing_subscriber::registry().with(trace::Line).init();
  std::thread::spawn(|| {
    std::thread::sleep(std::time::Duration::from_secs(15));
    eprintln!("TIMEOUT");
    std::process::exit(2);
  });

  step("main");
  let context = tauri::generate_context!();
  let keys: Vec<String> = context.assets().iter().map(|(k, v)| format!("{k} ({} B)", v.len())).collect();
  step(&format!("generate_context! done, assets = {keys:?}, csp = {:?}", context.config().app.security.csp.as_ref().map(|c| c.to_string())));

  let builder = tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![report])
    .setup(|app| {
      step(&format!("setup hook, windows = {:?}", app.webview_windows().keys().collect::<Vec<_>>()));
      Ok(())
    })
    .on_page_load(|w, p| step(&format!("on_page_load {:?} {} ({})", p.event(), p.url(), w.label())));
  step("Builder ready, calling build");
  let app = builder.build(context).expect("build");
  step(&format!("build returned, windows = {:?}", app.webview_windows().keys().collect::<Vec<_>>()));

  let mut cleared = 0u32;
  app.run(move |_h, e| match e {
    RunEvent::MainEventsCleared => cleared += 1,
    RunEvent::Ready => step("RunEvent::Ready"),
    RunEvent::ExitRequested { .. } => step("RunEvent::ExitRequested"),
    RunEvent::Exit => step(&format!("RunEvent::Exit (MainEventsCleared x{cleared})")),
    RunEvent::WindowEvent { label, event, .. } => step(&format!("WindowEvent {label} {event:?}")),
    RunEvent::WebviewEvent { label, event, .. } => step(&format!("WebviewEvent {label} {event:?}")),
    other => step(&format!("{other:?}")),
  });
}

report 里还有一段只在设了 LSOF 环境变量时才执行的检查,tauri 解决什么问题用到它,这次运行没设,上面从略。

dist/index.html 里的脚本在第一行就记下当时的状态,等 load 之后再 fetch 一次自己的地址看响应头,最后通过 report 交回 Rust:

<!doctype html><html><head><meta charset="utf-8"></head><body><p id="out">loading</p>
<script>
const t0 = performance.now().toFixed(1);
const ti = window.__TAURI_INTERNALS__;
const info = {
  t_script_ms: t0,
  readyState: document.readyState,
  href: location.href,
  isTauri: window.isTauri,
  typeof_ipc: typeof window.ipc,
  typeof___TAURI__: typeof window.__TAURI__,
  internals: Object.getOwnPropertyNames(ti),
  plugins: Object.keys(ti.plugins),
  metadata: ti.metadata,
  pattern: ti.__TAURI_PATTERN__,
};
window.addEventListener('load', async () => {
  info.t_load_ms = performance.now().toFixed(1);
  const r = await fetch(location.href);
  info.index_headers = Object.fromEntries(r.headers.entries());
  await ti.invoke('report', { msg: JSON.stringify(info, null, 1) });
});
</script></body></html>

tauri.conf.json 只有一个隐藏窗口,没有 CSP。debug 构建、--features tauri/custom-protocol,M2 Max、macOS 27、rustc 1.99.0。输出(去掉了 tao 打出来的 validAttributesForMarkedText、viewDidMoveToWindow 之类的 trace 行和 emit 内部的两层 span,用 ... 标出;时间每次运行略有不同):

    0.160ms [main] * exp: main
    0.689ms [main] * exp: generate_context! done, assets = ["/index.html (507 B)"], csp = None
    0.788ms [main] * exp: Builder ready, calling build
    0.821ms [main] > app::build
   70.447ms [main] > app::core_plugins::register
   70.682ms [main] < app::core_plugins::register
   77.872ms [main] < app::build
   77.896ms [main] * exp: build returned, windows = []
  169.241ms [main] * log: Triggered `applicationDidFinishLaunching`
  170.801ms [main] > app::setup
  170.852ms [main] > webview::create
  171.063ms [main] > wry::webview::create
  171.075ms [main] > wry::window::draw
  171.083ms [main] > wry::window::create
  171.100ms [main] * log: Creating new window
...
  211.401ms [main] < wry::window::create
  211.451ms [main] * log: web content process terminated
  317.325ms [main] < wry::webview::create
  317.545ms [main] > app::emit
  317.601ms [main] < app::emit
  317.615ms [main] > app::emit
  317.659ms [main] < app::emit
  317.675ms [main] < webview::create
  317.713ms [main] * exp: setup hook, windows = ["main"]
  317.719ms [main] < app::setup
  317.726ms [main] * exp: RunEvent::Ready
  317.736ms [main] * log: Completed `applicationDidFinishLaunching`
  348.967ms [main] < wry::window::draw
  376.334ms [main] > wry::custom_protocol::handle
  376.400ms [main] = wry::custom_protocol::handle uri="tauri://localhost"
  376.472ms [main] > wry::custom_protocol::call_handler
  376.744ms [main] < wry::custom_protocol::call_handler
  376.766ms [main] < wry::custom_protocol::handle
  376.943ms [tokio-rt-worker] > wry::custom_protocol::call_handler
  377.140ms [tokio-rt-worker] < wry::custom_protocol::call_handler
  381.019ms [main] * exp: on_page_load Started tauri://localhost (main)
  381.130ms [main] > wry::custom_protocol::handle
  381.189ms [main] = wry::custom_protocol::handle uri="tauri://localhost"
...
  381.332ms [main] * exp: on_page_load Finished tauri://localhost (main)
  381.412ms [tokio-rt-worker] > wry::custom_protocol::call_handler
  381.839ms [tokio-rt-worker] < wry::custom_protocol::call_handler
  442.369ms [main] > wry::custom_protocol::handle
  442.442ms [main] = wry::custom_protocol::handle uri="ipc://localhost/report"
...
  442.768ms [main] * exp: command report
REPORT {
 "t_script_ms": "2.0",
 "readyState": "loading",
 "href": "tauri://localhost",
 "isTauri": true,
 "typeof_ipc": "object",
 "typeof___TAURI__": "undefined",
 "internals": [
  "plugins",
  "postMessage",
  "metadata",
  "__TAURI_PATTERN__",
  "ipc",
  "convertFileSrc",
  "transformCallback",
  "unregisterCallback",
  "runCallback",
  "callbacks",
  "invoke"
 ],
 "plugins": [],
 "metadata": {
  "currentWindow": {
   "label": "main"
  },
  "currentWebview": {
   "label": "main"
  }
 },
 "pattern": {
  "pattern": "brownfield"
 },
 "t_load_ms": "3.0",
 "index_headers": {
  "access-control-allow-origin": "tauri://localhost",
  "content-length": "810",
  "content-type": "text/html"
 }
}
...
  442.962ms [main] * exp: RunEvent::ExitRequested
...
  449.015ms [main] * exp: RunEvent::Exit (MainEventsCleared x456)

wry::window::draw 这个 span 在建窗口时进入,窗口第一次绘制时才退出,所以它跨过了 setup。下面按时间顺序把这份输出拆开。同一个程序 release 构建跑三次,从 main 到 report 到达分别是 678、598、531 毫秒,步骤和线程完全一样。

1. 编译期:Context 已经备好

generate_context! 在编译期把 tauri.conf.json 解析成 Config 的字面量,把 frontendDist 目录下的文件读进来压缩,连同 CSP 哈希、图标、解析好的 ACL 一起生成一个 Context 表达式。运行时第一行打印的 /index.html (507 B) 是压缩后的大小:磁盘上的 index.html 是 810 字节,默认的 compression feature 用 brotli 压过;同一份文件 release 构建压成 414 字节(debug 用最快的压缩级别 2,release 用 9)。后面 tauri 协议取资源时再解压,fetch 拿到的 content-length: 810 就是解压后的。

这一步在运行时几乎不花时间(0.16 到 0.69 毫秒之间包括了建 Context 和打印),工作都在编译期做完了。展开出来的结构见 generate_context!。

2. Builder::new:invoke key 在这里生成

tauri::Builder::default() 调 Builder::new。它做的事里和后面两条主线有关的只有一件:生成 invoke key(16 个随机字节的 Z85 编码),并且立刻把它渲染进 ipc-protocol.js,存成 invoke_initialization_script 字符串。这段脚本后面会作为第二段初始化脚本注入每个 webview,key 只出现在它的闭包变量里。调用主线上这个 key 怎么被检查,见一次 invoke。

3. build:建事件循环,不建窗口

app::build 这个 span 从 0.8 毫秒到 77.9 毫秒。Builder::build 依次:

  • AppManager::with_handlers 收下 Context、插件、invoke_handler、setup 之外的各种回调和托管状态,见 AppManager;
  • R::new,在 Wry 上就是 Wry::new:用 EventLoopBuilder::with_user_event() 建 tao 的 EventLoop,记下当前线程是主线程,见 tauri-runtime-wry;
  • 设菜单、托盘的事件转发,建 App 和 AppHandle;
  • register_core_plugins 注册 path、event、window、webview、app、resources、image、menu、tray 这些核心插件(输出里 0.2 毫秒),manage 几个内部状态,最后 initialize_plugins。

app::core_plugins::register 在 70 毫秒才开始,也就是说前面将近 70 毫秒花在管理器和事件循环的创建上(debug 构建)。build 返回之后实验打印 windows = []:这时一个窗口都没有。

4. App::run:第一个事件是 Ready

App::run 把运行时取出来调 Wry::run,后者 self.event_loop.run(event_handler) 把主线程交给 tao,从此不再返回。macOS 上 tao 的事件循环就是 NSApp 的 run loop,NSApplication 启动完成时调 delegate 的 applicationDidFinishLaunching:,tao 在里面发出第一个事件:

  pub fn launched(app_delegate: &Object) {
    // ...
    HANDLER.set_ready();
    HANDLER.waker().start();
    HANDLER.set_in_callback(true);
    HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(
      StartCause::Init,
    )));
    HANDLER.set_in_callback(false);
  }

tauri-runtime-wry 的 handle_event_loop 把它翻译成 RunEvent::Ready:

  match event {
    Event::NewEvents(StartCause::Init) => {
      callback(RunEvent::Ready);
    }

输出里 Triggered applicationDidFinishLaunching 在 169 毫秒,和 build 返回隔了 90 多毫秒,那是 NSApp 自己启动的时间。从这一行到 317.7 毫秒的 Completed,后面第 5 到第 7 步全部发生在这一个 delegate 回调里面。tao 怎么把 Cocoa 的回调接成 winit 风格的事件循环,见 tao。

5. Ready 的处理:先建窗口,再跑用户的 setup

App 把用户回调包了一层(make_run_event_loop_callback),Ready 来时先调内部的 setup,再把 RunEvent::Ready 交给用户:

    move |event| match event {
      RuntimeRunEvent::Ready => {
        if let Err(e) = setup(&mut self) {
          panic!("Failed to setup app: {e}");
        }
        let event = on_event_loop_event(self.handle(), RuntimeRunEvent::Ready, self.manager());
        callback(self.handle(), event);
      }

setup 本身是三步,顺序就是输出里看到的顺序:

fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
  app.ran_setup = true;

  for window_config in app.config().app.windows.iter().filter(|w| w.create) {
    WebviewWindowBuilder::from_config(app.handle(), window_config)?.build()?;
  }

  app.manager.assets.setup(app);

  if let Some(setup) = app.setup.take() {
    (setup)(app).map_err(|e| crate::Error::Setup(e.into()))?;
  }

  Ok(())
}

webview::create 这个 span(170.9 到 317.7 毫秒)就是 tauri.conf.json 里那个窗口的创建;它结束之后才是 setup hook, windows = ["main"],再之后才是 RunEvent::Ready。所以用户的 setup 钩子里 get_webview_window("main") 一定拿得到,而 build 和 run 之间拿不到。窗口创建前发出的两个 app::emit 是 tauri://webview-created 和 tauri://window-created 两个内部事件。

build 不建窗口、要等 Ready,结果是窗口创建一定发生在事件循环之内:在 macOS 上就是 NSApplication 完成启动之后的那个 delegate 回调里,用户代码不必关心这个时机。

❗IMPORTANT
动脑筋:上面实验里的两行 windows = ...,一行在 build 返回之后、一行在用户的 setup 钩子里,各打印几个窗口标签?如果把 tauri.conf.json 里窗口的 create 设成 false 呢?平台 M2 Max、macOS 27,tauri 2.12.5,debug 构建。

6. prepare_pending_webview:补上协议和脚本

WebviewWindowBuilder::build 先把 webview 的描述变成 PendingWebview(into_pending_webview),这一步里 WebviewManager::prepare_webview 先算 URL:配置里的 WebviewUrl::App("index.html") 经 get_app_url 变成 tauri://localhost(文件名就是 index.html 时不拼进路径),所以输出里的 URL 没有结尾的 /index.html。然后调 prepare_pending_webview,往 PendingWebview 上补两样东西。

初始化脚本,按这个顺序排在用户自己的脚本前面:

    all_initialization_scripts.push(main_frame_script(
      r"
        Object.defineProperty(window, 'isTauri', {
          value: true,
        });

        if (!window.__TAURI_INTERNALS__) {
          Object.defineProperty(window, '__TAURI_INTERNALS__', {
            value: {
              plugins: {}
            }
          })
        }
      "
      .to_owned(),
    ));
    all_initialization_scripts.push(main_frame_script(self.invoke_initialization_script.clone()));
    all_initialization_scripts.push(main_frame_script(format!(
      r#"
          Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{
    // ...
    all_initialization_scripts.push(main_frame_script(self.initialization_script(
      app_manager,
      &ipc_init.into_string(),
      &pattern_init.into_string(),
      use_https_scheme,
    )?));

    all_initialization_scripts.extend(plugin_init_scripts);

页面脚本报回来的 internals 正好按这个顺序排:plugins(第一段)、postMessage(第二段,ipc-protocol.js)、metadata(第三段)、__TAURI_PATTERN__、ipc、convertFileSrc、transformCallback、unregisterCallback、runCallback、callbacks、invoke(第四段:init.js 模板依次拼进 pattern.js、ipc.js、core.js 和事件分发函数)。Object.defineProperty 不写 writable、configurable 时两者默认都是 false,这些属性页面脚本改不掉也删不掉。__TAURI_PATTERN__ 是 brownfield,表示没开隔离模式。window.__TAURI__ 是 undefined,因为没配 withGlobalTauri。typeof window.ipc 是 object:那是 wry 自己注入的 postMessage 入口,调用主线的退路。每一段从哪来、freezePrototype 和插件脚本怎么插进来,见初始化脚本。

URI scheme 处理函数:用户用 register_uri_scheme_protocol 注册的先挂上,然后在用户没占用同名 scheme 的前提下挂 tauri(protocol::tauri::get)和 ipc(ipc::protocol::get);开了 protocol-asset 再挂 asset,开了隔离模式再挂隔离 iframe 用的 scheme。prepare_webview 最后还给它装上 postMessage 那条路的处理函数 ipc_handler。

7. 交给 wry:WKWebView、scheme handler、user script

build_internal 拿到运行时句柄,调 create_window。在 Wry 上这是 send_user_message:当前线程是主线程就直接处理,否则经 EventLoopProxy 发回主线程。这里已经在主线程上,所以是同步执行的,输出里 wry::window::create 和 wry::webview::create 都在 webview::create 里面。

tauri-runtime-wry 的 create_webview 把 PendingWebview 逐项翻译成 wry 的 WebViewBuilder 调用:每个 scheme 一个 with_asynchronous_custom_protocol,每段脚本一个 with_initialization_script_for_main_only,ipc_handler 变成 with_ipc_handler,debug 构建里再加 with_devtools(true)。wry 的 new_ns_view 在 WKWebViewConfiguration 上给每个 scheme 调一次 setURLSchemeHandler_forURLScheme,把每段脚本变成 AtDocumentStart 的 WKUserScript,最后 loadRequest(tauri://localhost)。这一步在 debug 构建里约 106 毫秒(wry::window::create 结束到 wry::webview::create 结束)。逐个调用的细节见 tauri-runtime-wry 和 wry 在 macOS 上。

输出里夹着一行 web content process terminated,出现在刚建 webview 的时候,进程并没有终止。这行日志在 create_webview 里的位置是:

    if let Some(on_web_content_process_terminate_handler) =
      pending.on_web_content_process_terminate_handler
    {
      webview_builder = webview_builder
        .with_on_web_content_process_terminate_handler(on_web_content_process_terminate_handler);
    } else {
      log::debug!("web content process terminated");
      let context_ = context.clone();
      let window_id_ = window_id.clone();
      webview_builder = webview_builder.with_on_web_content_process_terminate_handler(move || {
        // ...
              match webview.reload() {

用户没有设置处理函数时走 else 分支,log::debug! 写在闭包外面,于是在注册默认处理函数时就打印了;真正的处理(webview.reload())在闭包里,WebContent 进程终止时才执行。

📝NOTE
冷知识:这个默认的「进程终止就重新加载」是 2.11.0 才加的

WKWebView 的页面跑在单独的 WebContent 进程里,这个进程被系统杀掉时页面会变成空白。#14523(首次发布于 tauri 2.11.0、tauri-runtime-wry 2.11.0)之前,tauri-runtime-wry 不给 wry 注册这个回调;这个 PR 在 create_webview 里加了上面那段代码,并在 Builder 上加了 on_web_content_process_terminate,让应用自己决定怎么处理。CHANGELOG 的原话是「Add handler for web content process termination on macOS and iOS.」。日志放在闭包外,是同一个 PR 带进来的,到 2.12.5 还是这样。

8. tauri://localhost:主线程收请求,tokio 线程取资源

窗口建好、Ready 交给用户回调之后,主线程回到事件循环。约 59 毫秒后,WebKit 对 tauri://localhost 发起请求,wry 的 start_task 在主线程上收到它(wry::custom_protocol::handle uri="tauri://localhost"),把 WKURLSchemeTask 里的方法、URL、头和正文抄成一个 http::Request<Vec<u8>>,连同一个应答闭包交给 tauri 协议。tauri 协议立刻把活儿交出去:

  Box::new(move |_, request, responder| {
    let context = context.clone();
    crate::async_runtime::spawn(async move {
      match get_response(&context, request).await {
        Ok(response) => responder.respond(response),

所以主线程上的 call_handler 只用了 0.27 毫秒;紧接着在 tokio-rt-worker 线程上出现的 call_handler 是 wry 应答闭包里的 span:get_asset 把路径空串映射成 index.html、解压、按扩展名和内容定 MIME 类型,配了 CSP 时改写 HTML 并生成 Content-Security-Policy 头;应答闭包在 tokio 线程上直接调 didReceiveResponse / didReceiveData / didFinish 把结果交还 WebKit。

页面 fetch(location.href) 拿到的响应头就是这一步的产物:access-control-allow-origin: tauri://localhost(窗口自己的源)、content-type: text/html、content-length: 810。没有 content-security-policy,因为配置里没有 CSP;配了之后会多一个这个头,内联脚本的哈希会被自动加进 script-src,详见 tauri:// 协议和 CSP、原型冻结与隔离模式。

9. 页面脚本执行:初始化脚本已经跑完

页面脚本第一行记下的状态说明了初始化脚本的时机:readyState 是 loading(文档还在解析),performance.now() 只有 2 毫秒,而 window.__TAURI_INTERNALS__ 上该有的 11 个属性都已经在了,invoke 可以直接调用。这就是 WKUserScriptInjectionTime::AtDocumentStart 的含义:在文档元素创建之后、页面任何脚本之前执行。

on_page_load 的两次回调和页面脚本的先后也值得看一眼:Started 在 381.0 毫秒,第二个 tauri://localhost 请求在 381.1 毫秒,Finished 在 381.3 毫秒。第二个请求是页面在 load 事件里发的 fetch,也就是说 Finished 到达 Rust 时,页面脚本早已执行完、load 事件已经触发,它发出的请求反而先到。wry 在 didCommitNavigation 时发 Started、在 didFinishNavigation 时发 Finished,两者都由 WebKit 的导航 delegate 回调触发,wry 和 tauri 都没有让它们和页面 JS 的执行同步。想在页面脚本之前做事,要用初始化脚本,不能指望 on_page_load。

最后,页面的 invoke('report', ...) 在 442.4 毫秒到达,走的是 ipc://localhost/report:调用主线开始了,见一次 invoke。report 里的 app.exit(0) 引出 ExitRequested 和 Exit,进程退出。

动脑筋答案

上面那次运行:

   77.896ms [main] * exp: build returned, windows = []
...
  317.713ms [main] * exp: setup hook, windows = ["main"]

把窗口配置改成 "create": false,setup 钩子末尾加一行 app.handle().exit(0)(没有窗口就没有页面来调 report),其余不变:

    4.998ms [main] * exp: main
   11.718ms [main] * exp: generate_context! done, assets = ["/index.html (507 B)"], csp = None
   13.516ms [main] * exp: Builder ready, calling build
   98.366ms [main] * exp: build returned, windows = []
  198.177ms [main] * exp: setup hook, windows = []
  198.251ms [main] * exp: RunEvent::Ready
  202.914ms [main] * exp: RunEvent::ExitRequested
  203.006ms [main] * exp: Resumed
  203.128ms [main] * exp: RunEvent::Exit (MainEventsCleared x1)
  • build 返回时 0 个:Builder::build 只建管理器、事件循环和核心插件,配置里的窗口要等 Ready。
  • setup 钩子里 1 个(main):内部的 setup 先按配置建完所有 create 为真的窗口,再调用户的钩子。
  • create 设成 false 时两处都是 0 个:内部 setup 用 .filter(|w| w.create) 跳过了它。这时 Ready 和用户钩子来得更早(198 毫秒,没有建窗口的那 150 毫秒),窗口要按 WindowConfig::create 文档说的,从 app.config().app.windows 取出配置,在钩子里自己用 WebviewWindowBuilder::from_config 去建。

小结

  • generate_context! 在编译期备好 Context,前端资源以 brotli 压缩形式内嵌(实验里 810 字节压成 507 / 414 字节)。
  • Builder::new 生成 invoke key 并渲染 ipc-protocol.js;Builder::build 建 AppManager、tao 事件循环和核心插件,不建窗口。
  • App::run 把主线程交给 tao;macOS 上 applicationDidFinishLaunching 里发出 StartCause::Init,变成 Ready。
  • Ready 时先按配置建窗口,再跑用户的 setup 钩子,最后把 RunEvent::Ready 交给用户回调,三者都在同一个 delegate 回调里。
  • prepare_pending_webview 加四段初始化脚本和 tauri、ipc 两个 scheme;wry 把它们变成 WKUserScript(AtDocumentStart)和 WKURLSchemeHandler。
  • tauri://localhost 的请求在主线程收到,取资源在 tokio 线程上做;页面脚本执行时 __TAURI_INTERNALS__ 已经就绪;on_page_load 的 Finished 到达时,页面的 load 事件早已触发。

💬 关联讨论

发起讨论
加载中...