Skip to main content

yggdrasil/
navigation.rs

1//! Dioxus History 适配器。浏览器端由 JS 在旧快照完成后提交路由,SSR 使用原 History。
2
3use dioxus::history::History;
4use std::rc::Rc;
5
6pub fn transition_history(inherited: Rc<dyn History>) -> Rc<dyn History> {
7    #[cfg(target_arch = "wasm32")]
8    if let Some(history) = browser::TransitionHistory::connect(inherited.current_prefix()) {
9        return Rc::new(history);
10    }
11    inherited
12}
13
14#[cfg(target_arch = "wasm32")]
15mod browser {
16    use super::*;
17    use crate::bridges::navigation::wasm::{get_module, NavigationModule};
18    use std::{cell::RefCell, sync::Arc};
19    use wasm_bindgen::prelude::Closure;
20
21    type Updater = Arc<dyn Fn() + Send + Sync>;
22
23    pub struct TransitionHistory {
24        module: NavigationModule,
25        updater: Rc<RefCell<Option<Updater>>>,
26        // JS 持有函数引用;在 disconnect 后才释放闭包,避免悬垂调用。
27        _notify: Closure<dyn FnMut()>,
28    }
29
30    impl TransitionHistory {
31        pub fn connect(prefix: Option<String>) -> Option<Self> {
32            let module = get_module()?;
33            let updater = Rc::new(RefCell::new(None::<Updater>));
34            let notify = {
35                let updater = updater.clone();
36                Closure::wrap(Box::new(move || {
37                    let callback = updater.borrow().clone();
38                    if let Some(callback) = callback {
39                        callback();
40                    }
41                }) as Box<dyn FnMut()>)
42            };
43            if module.connect(&notify, prefix.as_deref()).is_err() {
44                let _ = module.disconnect();
45                return None;
46            }
47            Some(Self {
48                module,
49                updater,
50                _notify: notify,
51            })
52        }
53    }
54
55    impl History for TransitionHistory {
56        fn current_route(&self) -> String {
57            // Router::push 会同步请求重渲染,这里必须保持旧路由直到 VT 回调提交。
58            self.module.current_route()
59        }
60
61        fn current_prefix(&self) -> Option<String> {
62            self.module.current_prefix()
63        }
64
65        fn go_back(&self) {
66            self.module.back();
67        }
68
69        fn go_forward(&self) {
70            self.module.forward();
71        }
72
73        fn push(&self, route: String) {
74            self.module.push(&route);
75        }
76
77        fn replace(&self, route: String) {
78            self.module.replace(&route);
79        }
80
81        fn external(&self, url: String) -> bool {
82            self.module.external(&url)
83        }
84
85        fn updater(&self, callback: Updater) {
86            // HistoryProvider 只创建一次连接;Router 安装或刷新回调不重复监听事件。
87            *self.updater.borrow_mut() = Some(callback);
88        }
89    }
90
91    impl Drop for TransitionHistory {
92        fn drop(&mut self) {
93            let _ = self.module.disconnect();
94        }
95    }
96}
97
98#[cfg(all(test, not(target_arch = "wasm32")))]
99mod tests {
100    use super::*;
101    use dioxus::prelude::*;
102    use dioxus::router::components::HistoryProvider;
103    use std::{cell::RefCell, sync::Arc, time::Duration};
104
105    #[test]
106    fn native_history_retains_server_route_and_history_identity() {
107        let inherited: Rc<dyn History> = Rc::new(
108            dioxus::history::MemoryHistory::with_initial_path("/post/server-route".to_owned()),
109        );
110        let history = transition_history(inherited.clone());
111        assert!(Rc::ptr_eq(&history, &inherited));
112        assert_eq!(history.current_route(), "/post/server-route");
113        history.push("/archives".to_owned());
114        history.go_back();
115        assert_eq!(history.current_route(), "/post/server-route");
116    }
117
118    /// 模拟浏览器适配器:push 记录意图,旧快照完成之前不发布新路由。
119    struct DeferredHistory {
120        displayed: RefCell<String>,
121        pending: RefCell<Option<String>>,
122        notify: RefCell<Option<Arc<dyn Fn() + Send + Sync>>>,
123    }
124
125    impl History for DeferredHistory {
126        fn current_route(&self) -> String {
127            self.displayed.borrow().clone()
128        }
129
130        fn push(&self, route: String) {
131            *self.pending.borrow_mut() = Some(route);
132        }
133
134        fn replace(&self, route: String) {
135            self.push(route);
136        }
137
138        fn go_back(&self) {}
139        fn go_forward(&self) {}
140
141        fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>) {
142            *self.notify.borrow_mut() = Some(callback);
143        }
144    }
145
146    #[derive(Clone)]
147    struct ProbeState {
148        history: Rc<DeferredHistory>,
149        resources: Rc<RefCell<Vec<String>>>,
150        effects: Rc<RefCell<Vec<String>>>,
151        scope: Rc<RefCell<Option<ScopeId>>>,
152    }
153
154    #[derive(Clone, Routable, PartialEq)]
155    enum ProbeRoute {
156        #[route("/preview/:slug")]
157        Probe { slug: String },
158    }
159
160    #[component]
161    fn Probe(slug: String) -> Element {
162        let state: ProbeState = use_context();
163        use_hook(|| *state.scope.borrow_mut() = Some(dioxus::dioxus_core::current_scope_id()));
164        let router = dioxus::router::router();
165        let committed_slug = use_memo(move || {
166            let ProbeRoute::Probe { slug } = router.current::<ProbeRoute>();
167            slug
168        });
169        let _request = use_resource(move || {
170            let slug = committed_slug();
171            state.resources.borrow_mut().push(slug.clone());
172            async move { slug }
173        });
174        use_effect(move || state.effects.borrow_mut().push(committed_slug()));
175        rsx! { div { "{slug}" } }
176    }
177
178    fn probe_app(state: ProbeState) -> Element {
179        use_context_provider(|| state.clone());
180        rsx! {
181            HistoryProvider {
182                history: move |_| state.history.clone() as Rc<dyn History>,
183                Router::<ProbeRoute> {}
184            }
185        }
186    }
187
188    async fn settle(dom: &mut VirtualDom) {
189        for _ in 0..10 {
190            dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
191            tokio::select! {
192                biased;
193                _ = dom.wait_for_work() => {},
194                _ = tokio::time::sleep(Duration::from_millis(5)) => return,
195            }
196        }
197        panic!("navigation did not settle");
198    }
199
200    #[tokio::test]
201    async fn deferred_router_updates_only_reload_data_when_the_displayed_slug_changes() {
202        let state = ProbeState {
203            history: Rc::new(DeferredHistory {
204                displayed: RefCell::new("/preview/first".to_owned()),
205                pending: RefCell::new(None),
206                notify: RefCell::new(None),
207            }),
208            resources: Rc::new(RefCell::new(Vec::new())),
209            effects: Rc::new(RefCell::new(Vec::new())),
210            scope: Rc::new(RefCell::new(None)),
211        };
212        let mut dom = VirtualDom::new_with_props(probe_app, state.clone());
213        dom.rebuild_in_place();
214        settle(&mut dom).await;
215        assert_eq!(*state.resources.borrow(), ["first"]);
216        assert_eq!(*state.effects.borrow(), ["first"]);
217
218        // This invokes the real Router::push path, including its eager subscriber notification.
219        dom.in_scope(state.scope.borrow().unwrap(), || {
220            dioxus::router::navigator().push(ProbeRoute::Probe {
221                slug: "second".to_owned(),
222            });
223        });
224        settle(&mut dom).await;
225        assert_eq!(state.history.current_route(), "/preview/first");
226        assert_eq!(*state.resources.borrow(), ["first"]);
227        assert_eq!(*state.effects.borrow(), ["first"]);
228
229        // The browser's native update callback now publishes the actual destination.
230        *state.history.displayed.borrow_mut() = state.history.pending.borrow_mut().take().unwrap();
231        state.history.notify.borrow().as_ref().unwrap()();
232        settle(&mut dom).await;
233        assert_eq!(*state.resources.borrow(), ["first", "second"]);
234        assert_eq!(*state.effects.borrow(), ["first", "second"]);
235    }
236}