Rust in Everything
基于 tauri 2.12.5

window.TAURI is undefined

主线位置:启动主线上 prepare_pending_webview 拼初始化脚本那一步。window.__TAURI__ 是一段可选的初始化脚本(bundle.global.js)挂上去的,window.__TAURI_INTERNALS__ 才是每个页面都有的那一个。两者各由哪段脚本建立、按什么顺序注入,见初始化脚本。

flowchart TB
  cfg{"app.withGlobalTauri"} -->|false 默认| none["不读 bundle.global.js"]
  none --> undef["window.__TAURI__ undefined"]
  cfg -->|true| ctx["Context 带上脚本"]
  ctx --> inject["初始化脚本 只进主 frame"]
  inject --> main["主 frame 有 __TAURI__"]
  inject --> ifr["iframe 里没有"]

实验 crate exp-tauri/pit3-global:从模板 base 复制,页面里嵌一个同源 iframe(frame.html)。页面按教程的写法取 window.__TAURI__.core,记录结果和 iframe 里的两个全局对象,再用一定存在的 __TAURI_INTERNALS__ 把结果报回来:

window.addEventListener('load', async () => {
  const out = { typeof___TAURI__: typeof window.__TAURI__ };
  try {
    // what tutorials and copied snippets do
    const { invoke } = window.__TAURI__.core;
    out.call = 'invoke is a ' + typeof invoke;
  } catch (e) {
    out.call = String(e);
  }
  const fw = document.getElementById('f').contentWindow;
  out.iframe = {
    typeof___TAURI__: typeof fw.__TAURI__,
    typeof___TAURI_INTERNALS__: typeof fw.__TAURI_INTERNALS__,
  };
  // __TAURI_INTERNALS__ is always injected, so it can carry the report
  await window.__TAURI_INTERNALS__.invoke('report', { msg: JSON.stringify(out, null, 1) });
});

tauri =2.12.5,--features tauri/custom-protocol,隐藏窗口,debug 构建,平台 M2 Max、macOS 27、rustc 1.99.0(WKWebView)。

现象

一:默认配置。 tauri.conf.json 里没有 withGlobalTauri:

REPORT {
 "typeof___TAURI__": "undefined",
 "call": "TypeError: undefined is not an object (evaluating 'window.__TAURI__.core')",
 "iframe": {
  "typeof___TAURI__": "undefined",
  "typeof___TAURI_INTERNALS__": "undefined"
 }
}

TypeError: undefined is not an object (evaluating 'window.__TAURI__.core') 是 WebKit 的措辞,WebView2、WebKitGTK 上措辞不同(没有实测),意思一样。

二:开关写在 1.x 的位置。 从 1.x 的配置或旧教程抄来的写法放在 build 下:

  "build": { "frontendDist": "./dist", "withGlobalTauri": true },

这次不是运行时 undefined,而是构建失败,tauri-build 的输出(节选):

  unknown field `withGlobalTauri`, expected one of `runner`, `dev-url`, `devUrl`, `frontend-dist`, `frontendDist`, `before-dev-command`, `beforeDevCommand`, `before-build-command`, `beforeBuildCommand`, `before-bundle-command`, `beforeBundleCommand`, `features`, `remove-unused-commands`, `removeUnusedCommands`, `additional-watch-directories`, `additional-watch-folders`, `additionalWatchFolders`, `windows`
  found an unknown configuration field. This usually means that you are using a CLI version that is newer than `tauri-build` and is incompatible. Please try updating the Rust crates by running `cargo update` in the Tauri app folder.

第二行的建议(「CLI 比 tauri-build 新,去 cargo update」)在这里是误导:版本没问题,是字段放错了地方。BuildConfig 带 deny_unknown_fields,所以至少不会被静默忽略。

三:开关打开了,代码在 iframe 里。 见下面修法的输出:主 frame 有了 __TAURI__,iframe 里两个全局对象都没有。

出处

开关是 AppConfig 的 with_global_tauri,#[serde(default)],默认 false。generate_context! 在编译期按它决定要不要把全局 API 脚本读进 Context:

  let plugin_global_api_scripts = if config.app.with_global_tauri {
    if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
      let scripts = scripts.into_iter().map(|s| quote!(#s));
      quote!(::std::option::Option::Some(&[#(#scripts),*]))
    } else {
      quote!(::std::option::Option::None)
    }
  } else {
    quote!(::std::option::Option::None)
  };

为 false 时是 None,二进制里根本没有 bundle.global.js 这 41 KB,运行时无从注入。为 true 时,prepare_pending_webview 把每段脚本逐个包成 main_frame_script 追加到初始化脚本里,只进主 frame。__TAURI_INTERNALS__ 所在的那几段脚本同样只进主 frame,所以 iframe 里两个都没有。

__TAURI__ 是 @tauri-apps/api 打包成的 IIFE,__TAURI__.core.invoke 就是 core.ts 的 invoke,函数体只有一句 return window.__TAURI_INTERNALS__.invoke(cmd, args, options)(见初始化脚本的「withGlobalTauri」)。

📝NOTE
冷知识:这个开关在 2.0 里换过位置

1.x 的开关是 build.withGlobalTauri。#8723(「feat(core): implement new config structure」,首次发布于 tauri 2.0.0-beta.0)按 RFC 5 重排了整份配置,CHANGELOG 的 Breaking Changes 里有一行:「Moved build.withGlobalTauri to app.withGlobalTauri.」同一批改动还把 tauri 对象改名为 app、build.distDir 改名为 frontendDist。这就是上面第二种情形的来历:照 1.x 写法放在 build 下的开关,在 2.x 的 BuildConfig 里是未知字段。

修法

一:打开开关,放在 app 下。

  "app": {
    "withGlobalTauri": true, "windows": [{ "title": "global", "width": 400, "height": 300, "visible": false }]
  }
REPORT {
 "typeof___TAURI__": "object",
 "call": "invoke is a function",
 "iframe": {
  "typeof___TAURI__": "undefined",
  "typeof___TAURI_INTERNALS__": "undefined"
 }
}

主 frame 拿到了 __TAURI__;iframe 仍然没有,这是设计如此。iframe 里的代码要调命令,就让它 postMessage 给父页面,由父页面代调,并在父页面里检查消息的来源。

二:用打包器的项目不需要这个开关。 import { invoke } from '@tauri-apps/api/core' 把同一份 API 打进你自己的包,直接使用 __TAURI_INTERNALS__,不依赖 window.__TAURI__。开关是给不用打包器的纯 HTML 页面准备的;开着它,页面上任何脚本都能从全局拿到整套 API。

怎么确认没中招

  • 页面里 typeof window.__TAURI__ 和 typeof window.__TAURI_INTERNALS__ 各打一次。前者 undefined、后者 object:开关没开;两个都是 undefined:代码不在 tauri 的主 frame 里,可能是 iframe,也可能是在普通浏览器里打开了开发服务器的地址。
  • 搜 tauri.conf.json(以及 tauri.*.conf.json、TAURI_CONFIG)里的 withGlobalTauri,确认它在 app 下。
  • 用打包器的项目搜 window.__TAURI__,换成 @tauri-apps/api 的导入,就不再依赖这个开关。

小结

  • window.__TAURI__ 只在 app.withGlobalTauri 为 true 时注入,默认关闭;关闭时连脚本都不进二进制。
  • 1.x 的 build.withGlobalTauri 在 2.x 是未知字段,tauri-build 直接报错,提示里的 cargo update 是误导。
  • 打开后也只进主 frame,iframe 里 __TAURI__ 和 __TAURI_INTERNALS__ 都没有。
  • 用打包器时改用 @tauri-apps/api 的导入,不需要全局对象。

💬 关联讨论

发起讨论
加载中...