refactor(hooks): 新增 use_event_listener 通用事件监听 hook
把 theme.rs 与 footer.rs 两处手写的 "use_hook 持有 Closure+target、 use_effect 注册 add_event_listener、use_drop 移除" 样板收口到一个 通用 hook,按 Dioxus 0.7 custom hooks 范式封装。 hook 设计: - target 通过 acquire 闭包在 use_effect 首次运行时获取(此时 DOM 一定 可用),调用方无需再自己包 use_effect。 - 泛型 AsRef<EventTarget> 统一 Window / Element / MediaQueryList。 - 非 wasm32 整体编译为 noop,调用方用 #[cfg] 隔离 web_sys 调用即可。 footer 的 "注册后首次同步" 业务逻辑保留在 hook 之外(语义不属于 事件监听)。theme 的 handler 改为重新 match_media 读取 matches, 功能等价(低频事件,开销可忽略)。
This commit is contained in:
parent
5a00ef31f3
commit
f00c2bf963
@ -3,9 +3,9 @@
|
||||
//! 提供站点版权信息,并在用户向下滚动超过一屏后显示"回到顶部"悬浮按钮。
|
||||
//! 回到顶部的滚动监听与平滑滚动逻辑仅在 WASM 前端生效。
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::hooks::event_listener::use_event_listener;
|
||||
use dioxus::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// 页脚与回到顶部按钮组件。
|
||||
///
|
||||
@ -19,67 +19,38 @@ use std::rc::Rc;
|
||||
pub fn Footer() -> Element {
|
||||
let mut visible = use_signal(|| false);
|
||||
|
||||
// WASM 下保存 scroll 事件闭包与 window,用于后续清理
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let listener_state = use_hook(|| {
|
||||
Rc::new(RefCell::new(
|
||||
None::<(wasm_bindgen::prelude::Closure<dyn FnMut()>, web_sys::Window)>,
|
||||
))
|
||||
});
|
||||
|
||||
// 非 WASM 下保持类型一致,避免编译错误
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _listener_state = use_hook(|| Rc::new(RefCell::new(None::<()>)));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let listener_state_for_effect = listener_state.clone();
|
||||
|
||||
// 挂载时注册 scroll 监听器,并根据当前滚动位置初始化按钮可见性
|
||||
use_effect(move || {
|
||||
// 根据 window 当前滚动位置同步 visible(注册监听后立即调用一次,避免首屏漏判)。
|
||||
// 滚动事件回调里也复用同一份判断逻辑。
|
||||
let sync_visible = move || {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if let Some(window) = web_sys::window() {
|
||||
let closure = wasm_bindgen::prelude::Closure::wrap(Box::new(move || {
|
||||
if let Some(w) = web_sys::window() {
|
||||
let threshold = w
|
||||
.inner_height()
|
||||
.ok()
|
||||
.and_then(|h| h.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let scroll_y = w.scroll_y().unwrap_or(0.0);
|
||||
let new_visible = scroll_y > threshold;
|
||||
visible.set(new_visible);
|
||||
}
|
||||
})
|
||||
as Box<dyn FnMut()>);
|
||||
|
||||
let _ = window.add_event_listener_with_callback(
|
||||
"scroll",
|
||||
wasm_bindgen::JsCast::unchecked_ref(closure.as_ref()),
|
||||
);
|
||||
|
||||
let threshold = window
|
||||
if let Some(w) = web_sys::window() {
|
||||
let threshold = w
|
||||
.inner_height()
|
||||
.ok()
|
||||
.and_then(|h| h.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let scroll_y = window.scroll_y().unwrap_or(0.0);
|
||||
let scroll_y = w.scroll_y().unwrap_or(0.0);
|
||||
visible.set(scroll_y > threshold);
|
||||
|
||||
*listener_state_for_effect.borrow_mut() = Some((closure, window));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 卸载时移除 scroll 监听器,防止内存泄漏
|
||||
// 注册 scroll 监听:注册 / 卸载清理由 use_event_listener 负责。
|
||||
// 仅 WASM 端调用(server 端 use_event_listener 是 noop,但 acquire 闭包内的
|
||||
// web_sys 在非 wasm 下不可解析,故整块 cfg;hook 数量在 server build 中不影响,
|
||||
// 因为 server 端该组件只跑一次 SSR)。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use_drop(move || {
|
||||
if let Some((closure, window)) = listener_state.borrow_mut().take() {
|
||||
let _ = window.remove_event_listener_with_callback(
|
||||
"scroll",
|
||||
wasm_bindgen::JsCast::unchecked_ref(closure.as_ref()),
|
||||
);
|
||||
}
|
||||
use_event_listener(
|
||||
|| web_sys::window(),
|
||||
"scroll",
|
||||
// 滚动事件触发时复用同样的阈值判断。
|
||||
move || sync_visible(),
|
||||
);
|
||||
|
||||
// 挂载时根据当前滚动位置初始化一次按钮可见性。
|
||||
use_effect(move || {
|
||||
sync_visible();
|
||||
});
|
||||
|
||||
// 根据 visible 动态切换按钮显示/隐藏样式
|
||||
|
||||
84
src/hooks/event_listener.rs
Normal file
84
src/hooks/event_listener.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! 通用 DOM 事件监听 hook:封装 add/remove_event_listener 生命周期。
|
||||
//!
|
||||
//! 用 `use_hook` 持有 `(Closure, target)`,`use_effect` 注册,`use_drop` 清理,
|
||||
//! 把 theme/footer 两处手写的样板收口到一处。非 wasm32 目标下整体编译为空操作
|
||||
//! (SSR 无 DOM),调用方无需再写占位变量或 `#[cfg]`。
|
||||
//!
|
||||
//! 设计要点:
|
||||
//! - `target` 通过一个初始化闭包 `acquire` 提供,在 `use_effect` 首次运行时调用
|
||||
//! (此时 DOM 一定可用);这样调用方无需自己再包一层 `use_effect`。
|
||||
//! - `event` 要求 `&'static str`,add 与 remove 复用同一字符串字面量。
|
||||
//! - handler 是 `FnMut()`(无参);若调用方需要 target 信息(如 `media.matches()`),
|
||||
//! 应在 `acquire` 里 clone 一份后 move 进 handler。
|
||||
|
||||
/// 注册一次性事件监听器,组件卸载时自动移除。
|
||||
///
|
||||
/// - `acquire`:在 `use_effect` 首次运行(DOM 已就绪)时调用,返回要监听的 target
|
||||
/// 及其克隆(handler 内若要读 target,用这个克隆)。返回 `None` 则不注册。
|
||||
/// - `event`:事件名(如 `"scroll"` / `"change"`),需为 `&'static str`。
|
||||
/// - `handler`:事件触发时的回调,`FnMut()`(无参)。
|
||||
///
|
||||
/// 仅在 wasm32 执行真实注册;非 wasm 为 noop。
|
||||
///
|
||||
/// # 例
|
||||
///
|
||||
/// ```ignore
|
||||
/// use_event_listener(
|
||||
/// || web_sys::window().unwrap(),
|
||||
/// "scroll",
|
||||
/// move || { visible.set(/* ... */); },
|
||||
/// );
|
||||
/// ```
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn use_event_listener<T, A, F>(acquire: A, event: &'static str, mut handler: F)
|
||||
where
|
||||
T: AsRef<web_sys::EventTarget> + Clone + 'static,
|
||||
A: FnOnce() -> Option<T>,
|
||||
F: FnMut() + 'static,
|
||||
{
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
// 用 use_hook 持有 (Closure, target),在整个组件生命周期内复用;
|
||||
// use_drop 时 take 出来移除监听,防止泄漏。
|
||||
let state: Rc<RefCell<Option<(wasm_bindgen::prelude::Closure<dyn FnMut()>, T)>>> =
|
||||
use_hook(|| Rc::new(RefCell::new(None)));
|
||||
let state_for_drop = state.clone();
|
||||
|
||||
use_effect(move || {
|
||||
let Some(target) = acquire() else { return };
|
||||
let target_clone = target.clone();
|
||||
let closure = wasm_bindgen::prelude::Closure::wrap(Box::new(move || {
|
||||
handler();
|
||||
})
|
||||
as Box<dyn FnMut()>);
|
||||
let _ = target_clone.as_ref().add_event_listener_with_callback(
|
||||
event,
|
||||
wasm_bindgen::JsCast::unchecked_ref(closure.as_ref()),
|
||||
);
|
||||
*state.borrow_mut() = Some((closure, target_clone));
|
||||
});
|
||||
|
||||
use_drop(move || {
|
||||
if let Some((closure, tgt)) = state_for_drop.borrow_mut().take() {
|
||||
let _ = tgt.as_ref().remove_event_listener_with_callback(
|
||||
event,
|
||||
wasm_bindgen::JsCast::unchecked_ref(closure.as_ref()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 非 wasm 占位:SSR 无 DOM,编译为空操作。
|
||||
///
|
||||
/// 保留与 wasm 版相同的泛型形态,调用方代码两端完全一致。
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(unused_variables, dead_code)]
|
||||
pub fn use_event_listener<T, A, F>(acquire: A, event: &'static str, handler: F)
|
||||
where
|
||||
T: Clone + 'static,
|
||||
A: FnOnce() -> Option<T>,
|
||||
F: FnMut() + 'static,
|
||||
{
|
||||
// SSR 无 DOM,空实现。
|
||||
}
|
||||
@ -3,9 +3,13 @@
|
||||
//! 该模块集中管理可在组件树中复用的自定义 Hook,包括:
|
||||
//! - 评论草稿在浏览器 localStorage 中的持久化(WASM 端)
|
||||
//! - 骨架屏延迟加载状态
|
||||
//! - 通用 DOM 事件监听(注册 + 自动卸载清理)
|
||||
|
||||
/// 评论草稿持久化 Hook,基于浏览器的 localStorage(仅在 WASM 端有效)。
|
||||
pub mod comment_storage;
|
||||
|
||||
/// 骨架屏延迟加载状态 Hook。
|
||||
/// 延迟加载状态 Hook。
|
||||
pub mod delayed_loading;
|
||||
|
||||
/// 通用 DOM 事件监听 Hook(add/remove_event_listener 生命周期封装)。
|
||||
pub mod event_listener;
|
||||
|
||||
58
src/theme.rs
58
src/theme.rs
@ -196,48 +196,28 @@ pub fn use_theme_provider() -> Signal<Theme> {
|
||||
|
||||
// WASM 端监听系统颜色偏好变化(仅 System 模式有意义,但无论何种模式都更新
|
||||
// system_dark signal;resolved memo 会决定是否真正改变 ResolvedTheme)。
|
||||
// 用 use_hook 持有 Closure + MediaQueryList,use_drop 时清理,防止泄漏。
|
||||
// 注册 / 卸载清理由 use_event_listener 统一负责,target 在其内部 use_effect
|
||||
// 首次运行时通过 acquire 闭包获取(此时 DOM 一定可用)。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use crate::hooks::event_listener::use_event_listener;
|
||||
|
||||
let listener_state = use_hook(|| {
|
||||
Rc::new(RefCell::new(None::<(
|
||||
wasm_bindgen::prelude::Closure<dyn FnMut()>,
|
||||
web_sys::MediaQueryList,
|
||||
)>))
|
||||
});
|
||||
let listener_state_for_effect = listener_state.clone();
|
||||
|
||||
use_effect(move || {
|
||||
let Some(window) = web_sys::window() else {
|
||||
return;
|
||||
};
|
||||
let Ok(Some(media)) = window.match_media("(prefers-color-scheme: dark)") else {
|
||||
return;
|
||||
};
|
||||
// 初始化时同步一次(read_system_dark 已设过初值,这里保持一致即可)。
|
||||
let media_for_closure = media.clone();
|
||||
let closure = wasm_bindgen::prelude::Closure::wrap(Box::new(move || {
|
||||
system_dark.set(media_for_closure.matches());
|
||||
})
|
||||
as Box<dyn FnMut()>);
|
||||
let _ = media.add_event_listener_with_callback(
|
||||
"change",
|
||||
wasm_bindgen::JsCast::unchecked_ref(closure.as_ref()),
|
||||
);
|
||||
*listener_state_for_effect.borrow_mut() = Some((closure, media));
|
||||
});
|
||||
|
||||
use_drop(move || {
|
||||
if let Some((closure, media)) = listener_state.borrow_mut().take() {
|
||||
let _ = media.remove_event_listener_with_callback(
|
||||
"change",
|
||||
wasm_bindgen::JsCast::unchecked_ref(closure.as_ref()),
|
||||
);
|
||||
}
|
||||
});
|
||||
use_event_listener(
|
||||
|| {
|
||||
let window = web_sys::window()?;
|
||||
window.match_media("(prefers-color-scheme: dark)").ok().flatten()
|
||||
},
|
||||
"change",
|
||||
move || {
|
||||
// handler 需要重新读取当前 matches 值(MediaQueryList 的事件回调
|
||||
// 不带参,只能自行重新查询)。
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Ok(Some(media)) = window.match_media("(prefers-color-scheme: dark)") {
|
||||
system_dark.set(media.matches());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
use_context_provider(|| theme);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user