refactor(wasm): 消除 DOM 互操作中的 js_sys::eval 字符串求值

对照 Dioxus "Breaking Out" 文档审查后,将 3 处 js_sys::eval 改为
类型化调用,与 tiptap_bridge/codemirror_bridge 风格统一:

- utils/time.rs sleep_ms:eval("new Promise(r=>setTimeout(r,n))")
  改为 js_sys::Promise + web_sys::Window::set_timeout_*,复用项目内
  system.rs::wasm_sleep 同款写法。
- theme.rs 主题切换:format!-into-eval 拼 __startThemeTransition(x,y)
  改为 Reflect::get 取函数 + Function::call2,消除字符串注入面
  (原坐标虽为 f64 无实际风险,但属不必要 eval)。
- post_content.rs:__initPostContent / __initLightbox 两处 eval 字面量
  改为 invoke_optional_global 辅助函数(Reflect::get + Function::apply),
  消除旧版 "eval 字符串粘贴" 模式;__lightboxSelectors 直接复用 JsValue
  传入,省去一次回读。
This commit is contained in:
xfy 2026-07-02 17:15:18 +08:00
parent d11385e3ee
commit fee5b7ba94
3 changed files with 60 additions and 22 deletions

View File

@ -4,6 +4,27 @@
use dioxus::prelude::*;
/// 读取 `window` 上的可选全局函数并调用;函数未定义/为 null 时静默跳过。
///
/// 替代 `js_sys::eval("if(window.__x) window.__x(...)")` 字符串拼贴模式:用
/// `Reflect::get` 取属性 + `Function::apply` 调用,无字符串求值,与 `tiptap_bridge`
/// 的类型化 extern 风格一致。
#[cfg(target_arch = "wasm32")]
fn invoke_optional_global(window: &web_sys::Window, name: &str, args: &[wasm_bindgen::JsValue]) {
use wasm_bindgen::JsCast;
if let Ok(fn_val) = js_sys::Reflect::get(window, &name.into()) {
if !fn_val.is_undefined() && !fn_val.is_null() {
let arr = js_sys::Array::new();
for a in args {
arr.push(a);
}
let _ = fn_val
.unchecked_into::<js_sys::Function>()
.apply(window, &arr);
}
}
}
/// 文章内容组件。
///
/// Props
@ -18,14 +39,21 @@ use dioxus::prelude::*;
pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")]
use_effect(move || {
let _ = js_sys::eval("window.__initPostContent('.post-content')");
let window = web_sys::window().unwrap();
// 调用 window.__initPostContent('.post-content'):函数不存在时静默跳过
// (与旧 eval 中的 if 守卫语义一致)。
invoke_optional_global(&window, "__initPostContent", &[".post-content".into()]);
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
// 否则 lightbox.js 加载完后其 IIFE 尾部读到配置自启动。
let _ = js_sys::eval(
"window.__lightboxSelectors = ['.post-content', '.entry-cover']; \
if (window.__initLightbox) window.__initLightbox(window.__lightboxSelectors);",
);
let selectors = js_sys::Array::new();
selectors.push(&".post-content".into());
selectors.push(&".entry-cover".into());
let selectors_val = js_sys::Object::from(selectors).into();
let _ = js_sys::Reflect::set(&window, &"__lightboxSelectors".into(), &selectors_val);
invoke_optional_global(&window, "__initLightbox", &[selectors_val]);
});
rsx! {

View File

@ -115,7 +115,7 @@ fn detect_initial_theme() -> Theme {
}
}
// 无持久化值:跟随系统。
return Theme::System;
Theme::System
}
#[cfg(feature = "server")]
@ -304,25 +304,27 @@ pub fn ThemeToggle() -> Element {
let prev_resolved = resolved();
let new_resolved = next.resolve(system_dark());
if prev_resolved != new_resolved {
use wasm_bindgen::JsCast;
// 实际明暗翻转 → 触发圆形展开 VT 动画(颜色过渡)。
// JS 从 DOM 现状推导目标主题(不传 isDark),避免与 Signal 状态不同步。
// 用 Reflect::get 取 window.__startThemeTransition 再 call2 调用,
// 替代旧版 format!-into-eval 字符串拼贴(无注入面、与 bridge 风格一致)。
let coords = evt.client_coordinates();
let x = coords.x;
let y = coords.y;
let _ = js_sys::eval(
&format!(
"if (window.__startThemeTransition) \
window.__startThemeTransition({x}, {y});",
x = x,
y = y,
),
);
let window = web_sys::window().unwrap();
let key = "__startThemeTransition".into();
if let Ok(fn_val) = js_sys::Reflect::get(&window, &key) {
if !fn_val.is_undefined() && !fn_val.is_null() {
let fn_obj = fn_val.unchecked_into::<js_sys::Function>();
let _ = fn_obj.call2(&window, &x.into(), &y.into());
}
}
}
// 立即切换图标theme.set 触发重渲染换 SVG配合 key 触发 CSS 进入动画。
// VT 的伪元素快照在 __startThemeTransition 内已同步拍好,不受后续真实
// DOM 变化影响,故无需推迟 theme.set。
theme.set(next);
return;
}
#[cfg(not(target_arch = "wasm32"))]
{

View File

@ -10,15 +10,23 @@
use chrono::DateTime;
/// 异步睡眠指定毫秒数。
///
/// 用 `js_sys::Promise` + `web_sys::Window::set_timeout_*` 构造,与
/// `pages/admin/system.rs::wasm_sleep` 同款写法,避免 `js_sys::eval` 字符串求值。
#[cfg(target_arch = "wasm32")]
pub async fn sleep_ms(ms: u32) {
use wasm_bindgen::JsCast;
let js_code = format!("new Promise(r => setTimeout(r, {}))", ms);
if let Ok(promise_val) = js_sys::eval(&js_code) {
if let Ok(promise) = promise_val.dyn_into::<js_sys::Promise>() {
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
}
}
use wasm_bindgen_futures::JsFuture;
let promise = js_sys::Promise::new(&mut |resolve, _| {
web_sys::window()
.expect("no window")
.set_timeout_with_callback_and_timeout_and_arguments_0(
&resolve.unchecked_into(),
ms.try_into().unwrap(),
)
.expect("set_timeout failed");
});
let _ = JsFuture::from(promise).await;
}
/// 异步睡眠指定毫秒数(原生 tokio 版本)。