1
0

lib.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #![deny(missing_docs)]
  2. //! Dioxus WebSys
  3. //!
  4. //! ## Overview
  5. //! ------------
  6. //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for
  7. //! Dioxus is one of the more advanced renderers, supporting:
  8. //! - idle work
  9. //! - animations
  10. //! - jank-free rendering
  11. //! - noderefs
  12. //! - controlled components
  13. //! - re-hydration
  14. //! - and more.
  15. //!
  16. //! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate.
  17. //!
  18. //! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
  19. //! validation of websys-specific features and not the general use of Dioxus.
  20. // ## RequestAnimationFrame and RequestIdleCallback
  21. // ------------------------------------------------
  22. // React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
  23. // running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
  24. // main thread.
  25. //
  26. // React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
  27. // the diff phase is non-blocking, using "yield_now" to allow the browser to process other events. When the diff phase
  28. // is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
  29. //
  30. // Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
  31. // setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
  32. //
  33. // From Google's guide on rAF and rIC:
  34. // -----------------------------------
  35. //
  36. // If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
  37. // which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
  38. // of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
  39. // frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
  40. // which is a potential performance bottleneck.
  41. //
  42. // Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
  43. // and as such we could easily go past the deadline the browser provided.
  44. //
  45. // The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
  46. // browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
  47. // be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
  48. // to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
  49. //
  50. // Essentially:
  51. // ------------
  52. // - Do the VDOM work during the idlecallback
  53. // - Do DOM work in the next requestAnimationFrame callback
  54. use std::rc::Rc;
  55. pub use crate::cfg::WebConfig;
  56. use dioxus::SchedulerMsg;
  57. use dioxus::VirtualDom;
  58. pub use dioxus_core as dioxus;
  59. use dioxus_core::prelude::Component;
  60. use futures_util::FutureExt;
  61. pub use crate::util::use_eval;
  62. mod cache;
  63. mod cfg;
  64. mod dom;
  65. mod rehydrate;
  66. mod ric_raf;
  67. mod util;
  68. /// Launch the VirtualDOM given a root component and a configuration.
  69. ///
  70. /// This function expects the root component to not have root props. To launch the root component with root props, use
  71. /// `launch_with_props` instead.
  72. ///
  73. /// This method will block the thread with `spawn_local` from wasm_bindgen_futures.
  74. ///
  75. /// If you need to run the VirtualDOM in its own thread, use `run_with_props` instead and await the future.
  76. ///
  77. /// # Example
  78. ///
  79. /// ```rust, ignore
  80. /// fn main() {
  81. /// dioxus_web::launch(App);
  82. /// }
  83. ///
  84. /// static App: Component = |cx| {
  85. /// rsx!(cx, div {"hello world"})
  86. /// }
  87. /// ```
  88. pub fn launch(root_component: Component) {
  89. launch_with_props(root_component, (), |c| c);
  90. }
  91. /// Launch your app and run the event loop, with configuration.
  92. ///
  93. /// This function will start your web app on the main web thread.
  94. ///
  95. /// You can configure the WebView window with a configuration closure
  96. ///
  97. /// ```rust
  98. /// use dioxus::prelude::*;
  99. ///
  100. /// fn main() {
  101. /// dioxus_web::launch_with_props(App, |config| config.pre_render(true));
  102. /// }
  103. ///
  104. /// fn app(cx: Scope) -> Element {
  105. /// cx.render(rsx!{
  106. /// h1 {"hello world!"}
  107. /// })
  108. /// }
  109. /// ```
  110. pub fn launch_cfg(root: Component, config_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig) {
  111. launch_with_props(root, (), config_builder)
  112. }
  113. /// Launches the VirtualDOM from the specified component function and props.
  114. ///
  115. /// This method will block the thread with `spawn_local`
  116. ///
  117. /// # Example
  118. ///
  119. /// ```rust, ignore
  120. /// fn main() {
  121. /// dioxus_web::launch_with_props(
  122. /// App,
  123. /// RootProps { name: String::from("joe") },
  124. /// |config| config
  125. /// );
  126. /// }
  127. ///
  128. /// #[derive(ParitalEq, Props)]
  129. /// struct RootProps {
  130. /// name: String
  131. /// }
  132. ///
  133. /// static App: Component<RootProps> = |cx| {
  134. /// rsx!(cx, div {"hello {cx.props.name}"})
  135. /// }
  136. /// ```
  137. pub fn launch_with_props<T>(
  138. root_component: Component<T>,
  139. root_properties: T,
  140. configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig,
  141. ) where
  142. T: Send + 'static,
  143. {
  144. if cfg!(feature = "panic_hook") {
  145. console_error_panic_hook::set_once();
  146. }
  147. let mut config = WebConfig::default();
  148. configuration_builder(&mut config);
  149. wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
  150. }
  151. /// Runs the app as a future that can be scheduled around the main thread.
  152. ///
  153. /// Polls futures internal to the VirtualDOM, hence the async nature of this function.
  154. ///
  155. /// # Example
  156. ///
  157. /// ```ignore
  158. /// fn main() {
  159. /// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") });
  160. /// wasm_bindgen_futures::spawn_local(app_fut);
  161. /// }
  162. /// ```
  163. pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T, cfg: WebConfig) {
  164. let mut dom = VirtualDom::new_with_props(root, root_props);
  165. for s in crate::cache::BUILTIN_INTERNED_STRINGS {
  166. wasm_bindgen::intern(s);
  167. }
  168. for s in &cfg.cached_strings {
  169. wasm_bindgen::intern(s);
  170. }
  171. let tasks = dom.get_scheduler_channel();
  172. let sender_callback: Rc<dyn Fn(SchedulerMsg)> =
  173. Rc::new(move |event| tasks.unbounded_send(event).unwrap());
  174. let should_hydrate = cfg.hydrate;
  175. let mut websys_dom = dom::WebsysDom::new(cfg, sender_callback);
  176. log::trace!("rebuilding app");
  177. if should_hydrate {
  178. // todo: we need to split rebuild and initialize into two phases
  179. // it's a waste to produce edits just to get the vdom loaded
  180. let _ = dom.rebuild();
  181. if let Err(err) = websys_dom.rehydrate(&dom) {
  182. log::error!(
  183. "Rehydration failed {:?}. Rebuild DOM into element from scratch",
  184. &err
  185. );
  186. websys_dom.root.set_text_content(None);
  187. // errrrr we should split rebuild into two phases
  188. // one that initializes things and one that produces edits
  189. let edits = dom.rebuild();
  190. websys_dom.apply_edits(edits.edits);
  191. }
  192. } else {
  193. let edits = dom.rebuild();
  194. websys_dom.apply_edits(edits.edits);
  195. }
  196. let mut work_loop = ric_raf::RafLoop::new();
  197. loop {
  198. log::trace!("waiting for work");
  199. // if virtualdom has nothing, wait for it to have something before requesting idle time
  200. // if there is work then this future resolves immediately.
  201. dom.wait_for_work().await;
  202. log::trace!("working..");
  203. // wait for the mainthread to schedule us in
  204. let mut deadline = work_loop.wait_for_idle_time().await;
  205. // run the virtualdom work phase until the frame deadline is reached
  206. let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
  207. // wait for the animation frame to fire so we can apply our changes
  208. work_loop.wait_for_raf().await;
  209. for edit in mutations {
  210. // actually apply our changes during the animation frame
  211. websys_dom.apply_edits(edit.edits);
  212. }
  213. }
  214. }