Rust in Everything
基于 tauri 2.12.5

把本站装进 tauri

主线位置:启动主线上「窗口加载哪个地址」这一步,和调用主线上「页面的 JS 能不能走到 on_message 的 ACL 那一步」。两种装法分别从这两处出岔子:直接打开站点时,加载的是远程地址,invoke 被 ACL 拦下;内嵌前端时,加载的是 tauri://localhost,站点的 server function 请求根本到不了站点。

flowchart TB
  site["本站 dioxus 应用"] --> wayA["装法一 打开站点 URL"]
  site --> wayB["装法二 内嵌前端"]
  wayA --> hyd["SSR 加水合 照常"]
  wayA --> acl["invoke 走 ACL"]
  acl --> deny["无能力 拒绝"]
  acl --> cap["remote.urls 放行"]
  acl --> local["frontendDist 当本地"]
  wayB --> rel["相对路径 回 index.html"]
  wayB --> cors["绝对路径 CORS 拦下"]
  wayB --> nocookie["跨站 不带 cookie"]

本站(开源,仓库 yuxuetr/rustineverything)是 dioxus 全栈应用,不用 tauri。这一页回答一个具体的问题:要把它做成桌面应用,用 tauri 包一层,会撞上什么。站点源码链接指向提交 89a61b1,本页引用的代码到写作时的 eefd161 没有变(security.rs 只改了 CSP_POLICY 的解析,csp_for 没动)。站点怎么起服务、页面怎么取数据,见 dioxus 卷的 在本站中。

实验 crate 是 exp-tauri/site-lab:一个隐藏窗口的 tauri 应用,MODE 环境变量选装法,窗口用 incognito(true),每次运行的 cookie 不留到下一次。对面是本地 dx serve 起的站点(debug,eefd161);只发 GET 和只读的 server function,不登录。依赖 tauri =2.12.5,--features tauri/custom-protocol,debug 构建,平台 M2 Max、macOS 27、rustc 1.99.0(WKWebView)。Windows、Linux 没有实测。

先说清:8080 端口上的不是站点本身

dx serve 把站点的服务端二进制起在一个随机端口上(这次是 127.0.0.1:59077),自己在 8080 上做反向代理,代理外面包了一层 CORS(dioxus CLI 的 build_devserver_router):

    // Setup cors
    router = router.layer(
        tower_http::cors::CorsLayer::new()
            // allow `GET` and `POST` when accessing the resource
            .allow_methods([Method::GET, Method::POST])
            // allow requests from any origin
            .allow_origin(Any)
            .allow_headers(Any),
    );

同一个只读的 server function list_available_themes(POST /api/theme/list,按 site_theme cookie 标出当前主题),分别对两个端口发预检:

---OPTIONS inner (127.0.0.1:59077)
HTTP/1.1 405 Method Not Allowed
...(CSP、nosniff 等六个安全头)
allow: POST
content-length: 0
...
---OPTIONS dx (localhost:8080)
HTTP/1.1 200 OK
vary: origin, access-control-request-method, access-control-request-headers
access-control-allow-methods: GET,POST
access-control-allow-headers: *
access-control-allow-origin: *
content-length: 0
...

站点本身没有 CORS 层(全仓库包括网关 crates/gateway 都没有 CorsLayer 或 Access-Control-*),OPTIONS 落到 axum 的方法路由上,405;access-control-allow-origin: * 是开发代理加的。线上跑的是站点本身,下文说「站点不回 CORS 头」指的是 59077 这一侧,8080 的结果只代表 dx serve。

装法一:窗口直接打开站点

      let url = match mode.as_str() {
        "remote" | "remote-cap" => {
          WebviewUrl::External(format!("http://localhost:8080{path}").parse().unwrap())
        }
        "appurl" => WebviewUrl::App(path.trim_start_matches('/').into()),
        _ => WebviewUrl::App("index.html".into()),
      };
      // ...
      WebviewWindowBuilder::new(app, "main", url)
        .visible(false)
        .incognito(std::env::var("INCOGNITO").map(|v| v != "0").unwrap_or(true))
        .initialization_script(format!("window.__INNER = {inner:?};"))
        .initialization_script(PROBE)
        .on_navigation(move |u| { /* /__rie_report?r=... 打印后退出 */ })

远程页面调不了 report,结果只能从别的路交回来:PROBE 是一段初始化脚本,tauri 把它和自己的脚本一起注入主 frame,不看来源(初始化脚本)。它在文档开始时挂上 securitypolicyviolation 监听、包一层 window.fetch 记下站点 wasm 发出的请求,等 #wasm-theme-style 里有了内容(主题 CSS 由水合后的 use_resource 取回,见 本站的页面怎么取数据)再做几次 invoke,最后导航到 /__rie_report?r=<json>,Rust 在 on_navigation 里打印、取消这次导航、退出。核心几行:

  const tryInvoke = async (cmd, args) => {
    try {
      return 'ok ' + JSON.stringify(await withTimeout(window.__TAURI_INTERNALS__.invoke(cmd, args), 5000));
    } catch (e) {
      return 'err ' + String(e).split('\n')[0];
    }
  };
  // ...
    out.greet = await tryInvoke('greet', { name: 'rie' });
    out.whoami = await tryInvoke('whoami');
    out.appVersion = await tryInvoke('plugin:app|version');
    out.themeBefore = await themeActive();
    document.cookie = 'site_theme=sunset; path=/; max-age=60; samesite=lax';
    out.themeAfterCookie = await themeActive();

应用声明了应用清单(greet、report、note、whoami 四个命令),capabilities/default.json 是一个普通应用都会有的本地能力:windows: ["main"],core:default 加四个 allow-*。whoami 返回它收到的 Origin 头。MODE=remote,不给远程来源任何能力:

NAV http://localhost:8080/
REPORT {
 "url": "http://localhost:8080/",
 "hydrated": true,
 "waitedMs": 22413,
 "internals": "object",
 "apiCallsAfterLoad": [
  "/api/theme/aggregated-css",
  "/api/auth/providers",
  "/api/layout/active",
  "/api/modules/enabled",
  "/api/theme/list",
  "/api/modules/enabled",
  "/api/theme/list",
  "/api/topics/list",
  "/api/auth/me"
 ],
 "wasm": "20047ms",
 "greet": "err greet not allowed on window \"main\", webview \"main\", URL: http://localhost:8080/",
 "whoami": "err whoami not allowed on window \"main\", webview \"main\", URL: http://localhost:8080/",
 "appVersion": "err app.version not allowed on window \"main\", webview \"main\", URL: http://localhost:8080/",
 "themeBefore": "200 active=ocean",
 "themeAfterCookie": "200 active=sunset",
 "csp": [
  "connect-src blocked ipc://localhost/greet"
 ],
 "sent": [
  "POST /api/theme/aggregated-css credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/auth/providers credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/layout/active credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/modules/enabled credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/theme/list credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/modules/enabled credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/theme/list credentials=same-origin [x-request-client: dioxus] body=0B",
  "POST /api/topics/list credentials=same-origin [content-type: application/json, x-request-client: dioxus] body=21B",
  "POST /api/auth/me credentials=same-origin [x-request-client: dioxus] body=0B"
 ]
}

站点这一侧什么都没变:SSR 的页面出来、wasm 加载(debug 构建的 wasm 有 164 MB,取到加编译用了 20 秒)、水合、水合后的 9 个 server function 调用都是同源请求,site_theme cookie 写进去,下一次 theme/list 就标出 sunset。主题、布局、ModuleGate 这些在客户端决定的东西照常工作,因为对站点来说,WKWebView 只是又一个浏览器。

tauri 这一侧有三点:

  • __TAURI_INTERNALS__ 在站点页面上,invoke key 也在。拦住 invoke 的是 on_message 里的 ACL:来源不是本地,必须有匹配的远程能力(运行时检查)。上面是 debug 的拒绝原文,只截了第一行。
  • 站点自己的 CSP 把 IPC 赶到了 postMessage 上。 csp_for 给的是 connect-src 'self'(debug 多一个 ws: wss:),fetch('ipc://localhost/greet') 不在其中,被拦下一次,ipc-protocol.js 退回 postMessage,之后的调用不再试 fetch,所以 csp 里只有一条(退路的条件见 JS 侧:invoke 与回调)。下面 whoami 收到的 Origin 是 None,就是 postMessage 路的特征。
  • 站点的 server function 请求全是 credentials=same-origin,带 x-request-client: dioxus,没有参数的函数 body 为空。这一行在装法二里会用到。

MODE=remote-cap 在 setup 里给 http://localhost:8080/* 加一个远程能力,只含 allow-greet 和 allow-whoami:

        app.add_capability(
          tauri::ipc::CapabilityBuilder::new("site-remote")
            .remote("http://localhost:8080/*".into())
            .local(false)
            .window("main")
            .permission("allow-greet")
            .permission("allow-whoami"),
        )?;
 "greet": "ok \"hello, rie\"",
 "whoami": "ok \"origin header: None\"",
 "appVersion": "err app.version not allowed on window \"main\", webview \"main\", URL: http://localhost:8080/",

(其余字段和上一次相同。)给什么才有什么,core:default 里的 app.version 仍然被拒。

还有一种更常见的写法:不写 WebviewUrl::External,而是把站点地址写进 build.frontendDist,窗口照常用 WebviewUrl::App。实验里用 Context::config_mut 在运行时改,效果和写在 tauri.conf.json 里一样(MODE=appurl):

  let mut context = tauri::generate_context!();
  if mode == "appurl" {
    context.config_mut().build.frontend_dist =
      Some(FrontendDist::Url("http://localhost:8080/".parse().unwrap()));
  }
❗IMPORTANT
动脑筋:appurl 模式下没有加任何远程能力,capabilities/default.json 也没动。站点页面上调 greet 和 plugin:app|version,各得到什么?

答案取决于 tauri 怎么判断「本地」。is_local_url 的第二条:

  fn is_local_url(&self, current_url: &Url) -> bool {
    let uses_https = current_url.scheme() == "https";

    // if from `tauri://` custom protocol
    ({
      let protocol_url = self.manager().tauri_protocol_url(uses_https);
      current_url.scheme() == protocol_url.scheme()
      && current_url.domain() == protocol_url.domain()
    }) ||

    // or if relative to `devUrl` or `frontendDist`
      self
          .manager()
          .get_app_url(uses_https)
          .make_relative(current_url)
          .is_some()
    // ...

非开发模式(带 custom-protocol)下 get_app_url 在 frontendDist 是 URL 时就返回它。Url::make_relative 只要求 scheme、主机、端口相同,路径不限,所以站点这个源下的每一个页面都算本地,套用的是本地能力,不需要 remote.urls。

📝NOTE
冷知识:外部地址算不算本地,1.0.2 之前只看 scheme

#4536(「fix(core): check whether external url is local」,首次发布于 tauri 1.0.2)之前,1.x 的窗口管理器对 WindowUrl::External 的判断是 url.scheme() == "tauri":一个外部地址只有走 tauri 协议才算本地。CHANGELOG 原文:「Fixes check for local URL when an external URL is provided to the window and it is based on the configured devPath.」改成了 config_url.make_relative(url).is_some(),也就是现在 is_local_url 第二条的写法:开发时 devPath(2.x 的 devUrl)上的页面即使以外部地址打开,也要拿到本地页面的待遇。代价就是上面这个结论:只要地址和配置里的 URL 同源,就是本地。

为什么不该把本站这样装

本站是公开的内容站点,装法一等于把本机能力交给「能在 localhost:8080(线上就是站点域名)这个源上执行 JS 的任何东西」:

  • 授权的最小单位是一个源。 remote.urls 经 ipc:// 匹配的是 Origin 头,没有路径(运行时检查)。本站的情况特殊一点:站点 CSP 把 IPC 赶到了 postMessage 上,那条路用 frame 的完整 URL,带路径的模式这时能匹配上;但同一个源下的页面之间没有隔离,按路径分权在这里也不是一道边界。
  • frontendDist 写站点地址时没有这道门。 应用的本地能力(通常就是 core:default 加应用自己的命令)整套给了站点,不需要任何 remote 配置,也不会出现在能力文件里提醒人。
  • 站点的 CSP 不是为这件事设计的。 script-src 带 'unsafe-inline'(为了 Dioxus 的内联样式和 mermaid、Prism 的引导脚本,见 security.rs 的模块注释),这条 CSP 防的是站点数据被外泄,不是「页面上的 JS 能不能调本机命令」。
  • 不设 on_navigation 时整页跳转不受限。 站点的登录是整页跳转到 OAuth 提供方再跳回来(auth_routes.rs),提供方的页面也会在这个 webview 里加载、同样被注入 __TAURI_INTERNALS__(ipc 协议的「来自别的源的页面」)。它不在能力里就调不了命令,前提是能力只写了站点自己的源。

所以装法一只适合「不给任何命令」:窗口就是一个浏览器,tauri 只提供窗口、菜单和打包。一旦要调 Rust,就该把调用 Rust 的那部分页面做成内嵌的本地页面,而不是给站点授权。

装法二:内嵌前端,跨源调后端

另一种思路是把 dioxus 的 web 构建产物当 frontendDist 内嵌,页面是 tauri://localhost,数据还从站点取。这条路在本站上走不到 CORS 那一步,前面先有两道坎。

第一道坎:dioxus 在 web 上只发相对路径

dioxus 0.7.10 的客户端在 wasm 上用 gloo-net 发 server function 请求,new_gloo_request 只取 URL 的路径和查询串:

    pub fn new_gloo_request(&self) -> gloo_net::http::RequestBuilder {
        let mut builder = gloo_net::http::RequestBuilder::new(
            format!(
                "{path}{query_string}",
                path = self.url.path(),
                query_string = self
                    .url
                    .query()
                    .map(|query| format!("?{query}"))
                    .unwrap_or_default()
            )
            .as_str(),
        )
        .header("X-Request-Client", "dioxus")
        .method(self.method.clone());

fetch_inner 的注释写的就是这件事:「On wasm, this doesn't matter since we always use relative URLs when making requests anyways」。set_server_url 在 web 上只能改路径前缀(launch 用它加 base_path),改不了主机。本站也没有调用它。没有设置 credentials,按 fetch 的默认值是 same-origin,装法一里记下的 credentials=same-origin 就是这个。

于是内嵌之后,站点的 wasm 会把 POST /api/theme/list 发给 tauri://localhost。实验的内嵌页面 dist/index.html 照 dioxus 的样子发这个请求:

  // what dioxus 0.7 sends for a server fn without arguments: POST, empty body, X-Request-Client
  const dx = { method: 'POST', headers: { 'X-Request-Client': 'dioxus' } };
  // ...
    out.push(await attempt('relative  /api/theme/list', '/api/theme/list', dx));
relative  /api/theme/list: 200 text/html <!doctype html> <html> <body> <script> // Embedded page (t

200,正文是内嵌的 index.html:tauri 协议找不到资源时回退到 index.html,不看方法(tauri:// 协议与 CSP)。dioxus 拿到的是一页 HTML,按 server function 的编码去解,失败。

第二道坎:没有水合数据

内嵌的是静态文件,没有服务端渲染,页面里也就没有 window.initial_dioxus_hydration_data。本站的客户端开着 fullstack,它会打开 dioxus-web/hydrate,dioxus-web 的 launch 一开始就经 get_initial_hydration_data 读这个变量:

                export function get_initial_hydration_data() {
                    const decoded = atob(window.initial_dioxus_hydration_data);
                    return Uint8Array.from(decoded, (c) => c.charCodeAt(0))
                }

变量不存在时就是 atob(undefined)。在同一个内嵌页面里执行它:

atob(undefined): InvalidCharacterError: The string contains invalid characters.

这个 extern 声明没有 catch,JS 的异常不会变成 Rust 的 Err,客户端在第一次渲染之前就停下。要内嵌,得另出一个不开 hydrate 的纯客户端构建。这一点只读了源码,本页没有构建本站的 wasm。

就算请求发对了地方

假设前两道坎都绕过去(比如客户端不用 dioxus 的 server function,自己拼绝对地址发请求),内嵌页面能不能跨源调站点?同一个页面接着发,app 是站点本身(59077),dx-proxy 是 8080 上的开发代理,echo 是本机 127.0.0.1:19020 上的一个 Python 服务,打印收到的头,/nocors 不回任何 CORS 头(和站点本身一样),/echo 对请求的 Origin 回 Access-Control-Allow-Origin 并允许带凭据:

NAV tauri://localhost
NOTE 
origin tauri://localhost
relative  /api/theme/list: 200 text/html <!doctype html> <html> <body> <script> // Embedded page (t
app       /api/theme/list: TypeError: Load failed
app simple /api/theme/list: TypeError: Load failed
dx-proxy  /api/theme/list: 200 application/json [{"id":"ocean","label":"Ocean","is_active":true},{"id":"suns
dx-proxy  +credentials: TypeError: Load failed
echo nocors simple: TypeError: Load failed
echo nocors dioxus: TypeError: Load failed
atob(undefined): InvalidCharacterError: The string contains invalid characters.

echo 服务这一侧看到的(同一次运行):

ECHO POST /nocors {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhost", "Sec-Fetch-Mode": "cors"}
ECHO OPTIONS /nocors {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhost", "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "x-request-client", "Sec-Fetch-Mode": "cors"}
请求结果原因
站点本身,dioxus 的样子TypeError: Load failedX-Request-Client 不是 CORS 安全头,先发预检;站点对 OPTIONS 回 405,请求没发出去
站点本身,不带自定义头TypeError: Load failed简单请求不预检,直接发到服务端;响应没有 Access-Control-Allow-Origin,脚本读不到
开发代理,不带凭据200,读到 JSON代理回 *
开发代理,credentials: 'include'TypeError: Load failed带凭据时 * 不算数

Load failed 是 WebKit 给脚本的全部信息,具体原因只在 Web Inspector 的控制台里。第二行要单独说:echo 的日志证明简单的跨源 POST 确实发到了服务端,只是结果对脚本不可见;站点对这样一个不带头、不带 body 的 POST(用 curl 发)照常回 200 和主题列表。站点的 server function 由 dioxus 发出时总带 X-Request-Client,预检在前,405 挡住了处理函数;手写的简单请求没有这层保护,站点只在登出路由上按 Sec-Fetch-Site: cross-site 拒绝(auth_routes.rs),而 tauri://localhost 发出的请求带的正是 cross-site。

📝NOTE
没有蠢问题

问: 本地 curl localhost:8080/api/theme/list 明明回了 access-control-allow-origin: *,在内嵌页面里也真读到了 JSON,为什么还说站点不回 CORS 头?

答: 那个头是 dx serve 的开发代理加的,站点的 Router 里没有 CORS 层(上面 59077 的 OPTIONS 回 405)。开发时装法二「能用」,上线就失效,而且失效的样子只是一个 Load failed。另外 * 本来就不允许带 cookie,即使开发时,登录态也传不过去。

cookie:一个都不带

站点的登录态是一个 session cookie,auth_routes.rs 在 OAuth 回调里下发:

              let session_cookie = format!(
                "session={}; HttpOnly; Path=/; Max-Age=604800; SameSite=Lax{}",
                jwt_token, secure_flag
              );

SameSite=Lax 的 cookie 在跨站的 fetch 里本来就不带。为了看 WKWebView 在 tauri://localhost 上实际怎么做,echo 服务的 /set 在一次顶层导航里下发三个 cookie:一个照抄站点格式的 session(SameSite=Lax)、一个不写 SameSite 的 plain、一个 SameSite=None; Secure 的 none;页面先同源请求一次作对照,再跳回 tauri://localhost/index.html?phase=2,从那里带 credentials: 'include' 请求 /echo:

NAV http://127.0.0.1:19020/set
NAV tauri://localhost/index.html?phase=2
REPORT 
back at tauri://localhost/index.html?phase=2
echo include : 200 application/json {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhos
echo default : 200 application/json {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhos
ECHO GET /set {"Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "navigate"}
ECHO POST /nocors-same {"Sec-Fetch-Site": "same-origin", "Origin": "http://127.0.0.1:19020", "Sec-Fetch-Mode": "cors", "Cookie": "none=1; plain=1; session=test"}
ECHO OPTIONS /echo {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhost", "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "x-request-client", "Sec-Fetch-Mode": "cors"}
ECHO POST /echo {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhost", "X-Request-Client": "dioxus", "Sec-Fetch-Mode": "cors"}
ECHO POST /echo {"Sec-Fetch-Site": "cross-site", "Origin": "tauri://localhost", "X-Request-Client": "dioxus", "Sec-Fetch-Mode": "cors"}

三个 cookie 都存下了(同源的对照请求全带上了),但从 tauri://localhost 发出的两次请求一个都没带,包括 SameSite=None 的那个;credentials: 'include' 和默认值没有区别。把 incognito 关掉、用持久的数据存储再跑一次,结果相同。所以对 WKWebView 来说,问题不只是站点的 cookie 写了 Lax:在 macOS 上,内嵌页面跨站请求时带不出 cookie,靠 cookie 的登录态这条路走不通,要换成页面自己保存、自己放进请求头的令牌,站点也要相应改认证方式并加上 CORS 层。

和 dioxus-desktop 比

本站的 Cargo.toml 已经有一个 desktop = ["dioxus/desktop"] 特性,Cargo.lock 里锁着 dioxus-desktop 用的 wry 0.53.5、tao 0.34.8(本卷是 wry 0.57.1、tao 0.37.2)。同一份组件编成 dioxus-desktop 应用,UI 逻辑在 Rust 进程里,webview 只接收 DOM 指令,没有 invoke、没有 ACL(tauri 解决什么问题的「和 dioxus-desktop 比」)。server function 那一侧也不一样,按 dioxus 0.7.10 的源码(本页没有构建本站的桌面版):

  • 请求由 Rust 发。 非 wasm 目标走 reqwest,LaunchBuilder::launch 在桌面上把服务端地址默认设成 http://127.0.0.1:8080(可由 DIOXUS_DEVSERVER_IP / DIOXUS_DEVSERVER_PORT 改,或自己调 set_server_url)。这不是浏览器里的请求,没有 CORS,也没有 WebKit 的 cookie 规则。
  • cookie 在进程里。 new_reqwest_client 给非 wasm 目标配了一个进程级的 reqwest::cookie::Jar,服务端下发的 Set-Cookie 存在这里,和 webview 的 cookie 存储是两回事。
  • 本站的浏览器操作在桌面上什么也不做。 widgets::browser 的函数只在 wasm32 上有实现,navigate、set_cookie 在别的目标上是空函数。登录弹窗的按钮靠 navigate 整页跳到 /api/auth/login/{provider},桌面版里点了没有反应;主题选择用 set_cookie 兜底写的 cookie 也写不进去,只剩 set_user_theme 响应里的 Set-Cookie 进了 reqwest 的 Jar,ThemePicker 的注释写的正是这条「桌面端走 reqwest cookie jar,跨平台兜底」。就算把登录改成在 webview 里跳转,session cookie 也会落进 webview 的存储,而 server function 用的是 reqwest 的 Jar,两边拿不到同一份登录态。

所以对本站来说,三条路的难点各在一处:tauri 装法一难在授权,装法二难在跨源和 cookie,dioxus-desktop 难在把浏览器里的登录流程搬进 Rust。

动脑筋答案

MODE=appurl,debug:

NAV http://localhost:8080/
REPORT {
 "url": "http://localhost:8080/",
 "hydrated": true,
 "waitedMs": 22866,
 "internals": "object",
 ...
 "wasm": "20371ms",
 "greet": "ok \"hello, rie\"",
 "whoami": "ok \"origin header: None\"",
 "appVersion": "ok \"0.1.0\"",
 "themeBefore": "200 active=ocean",
 "themeAfterCookie": "200 active=sunset",
 "csp": [
  "connect-src blocked ipc://localhost/greet"
 ]
}
  • greet 成功:http://localhost:8080/ 能相对于 frontendDist 表示,is_local_url 返回真,main 窗口的本地能力 default 生效,里面有 allow-greet。
  • app.version 也成功,返回应用版本 0.1.0:core:default 同样来自本地能力。远程能力那一次它是被拒的,这里一行远程配置都没写,站点拿到的反而更多。
  • whoami 的 Origin 仍是 None,csp 里仍有那一条:本地与否只影响 ACL,站点自己的 CSP 照样把 IPC 赶到 postMessage 上。

小结

  • 装法一(窗口打开站点):SSR、水合、水合后的 server function、cookie 都照常,主题和模块开关正常;__TAURI_INTERNALS__ 和 invoke key 在站点页面上,站点的 connect-src 'self' 让 IPC 退回 postMessage。
  • 没有远程能力时命令全部被拒;给了 remote.urls 就按给的权限放行;把站点写进 frontendDist 时站点算本地,整套本地能力(含 core:default)直接给了它。本站是公开站点,CSP 带 'unsafe-inline',不该给它任何命令。
  • 装法二(内嵌前端):dioxus 在 web 上只发相对路径,请求落到 tauri://localhost 拿回 index.html;没有水合数据时 atob(undefined) 抛错,客户端起不来。
  • 即使发到站点:站点本身没有 CORS 层,预检 405;dx serve 的 8080 代理回 *,开发时看起来能用。WKWebView 从 tauri://localhost 跨站请求不带任何 cookie,SameSite=None 的也不带。
  • dioxus-desktop 由 reqwest 发 server function,没有 CORS 和浏览器的 cookie 规则,但 cookie 在进程里的 Jar,本站的 widgets::browser 在非 wasm 上是空操作,登录流程要重做。

💬 关联讨论

发起讨论
加载中...