把本站装进 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()));
}
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。
#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 failed | X-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。
问: 本地 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 里跳转,sessioncookie 也会落进 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 上是空操作,登录流程要重做。