lib.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
  2. #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
  3. #![deny(missing_docs)]
  4. //! Dioxus WebSys
  5. //!
  6. //! ## Overview
  7. //! ------------
  8. //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for
  9. //! Dioxus is one of the more advanced renderers, supporting:
  10. //! - idle work
  11. //! - animations
  12. //! - jank-free rendering
  13. //! - controlled components
  14. //! - hydration
  15. //! - and more.
  16. //!
  17. //! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate.
  18. //!
  19. //! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
  20. //! validation of websys-specific features and not the general use of Dioxus.
  21. // ## RequestAnimationFrame and RequestIdleCallback
  22. // ------------------------------------------------
  23. // React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
  24. // running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
  25. // main thread.
  26. //
  27. // React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
  28. // the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase
  29. // is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
  30. //
  31. // Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
  32. // setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
  33. //
  34. // From Google's guide on rAF and rIC:
  35. // -----------------------------------
  36. //
  37. // If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
  38. // which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
  39. // of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
  40. // frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
  41. // which is a potential performance bottleneck.
  42. //
  43. // Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
  44. // and as such we could easily go past the deadline the browser provided.
  45. //
  46. // The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
  47. // browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
  48. // be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
  49. // to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
  50. //
  51. // Essentially:
  52. // ------------
  53. // - Do the VDOM work during the idlecallback
  54. // - Do DOM work in the next requestAnimationFrame callback
  55. pub use crate::cfg::Config;
  56. #[cfg(feature = "file_engine")]
  57. pub use crate::file_engine::WebFileEngineExt;
  58. use dioxus_core::{Element, Scope, VirtualDom};
  59. use futures_util::{
  60. future::{select, Either},
  61. pin_mut, FutureExt, StreamExt,
  62. };
  63. mod cache;
  64. mod cfg;
  65. mod dom;
  66. #[cfg(feature = "eval")]
  67. mod eval;
  68. #[cfg(feature = "file_engine")]
  69. mod file_engine;
  70. #[cfg(all(feature = "hot_reload", debug_assertions))]
  71. mod hot_reload;
  72. #[cfg(feature = "hydrate")]
  73. mod rehydrate;
  74. // Currently disabled since it actually slows down immediate rendering
  75. // todo: only schedule non-immediate renders through ric/raf
  76. // mod ric_raf;
  77. // mod rehydrate;
  78. /// Launch the VirtualDOM given a root component and a configuration.
  79. ///
  80. /// This function expects the root component to not have root props. To launch the root component with root props, use
  81. /// `launch_with_props` instead.
  82. ///
  83. /// This method will block the thread with `spawn_local` from wasm_bindgen_futures.
  84. ///
  85. /// If you need to run the VirtualDOM in its own thread, use `run_with_props` instead and await the future.
  86. ///
  87. /// # Example
  88. ///
  89. /// ```rust, ignore
  90. /// fn main() {
  91. /// dioxus_web::launch(App);
  92. /// }
  93. ///
  94. /// static App: Component = |cx| {
  95. /// render!(div {"hello world"})
  96. /// }
  97. /// ```
  98. pub fn launch(root_component: fn(Scope) -> Element) {
  99. launch_with_props(root_component, (), Config::default());
  100. }
  101. /// Launch your app and run the event loop, with configuration.
  102. ///
  103. /// This function will start your web app on the main web thread.
  104. ///
  105. /// You can configure the WebView window with a configuration closure
  106. ///
  107. /// ```rust, ignore
  108. /// use dioxus::prelude::*;
  109. ///
  110. /// fn main() {
  111. /// dioxus_web::launch_with_props(App, Config::new().pre_render(true));
  112. /// }
  113. ///
  114. /// fn app(cx: Scope) -> Element {
  115. /// cx.render(rsx!{
  116. /// h1 {"hello world!"}
  117. /// })
  118. /// }
  119. /// ```
  120. pub fn launch_cfg(root: fn(Scope) -> Element, config: Config) {
  121. launch_with_props(root, (), config)
  122. }
  123. /// Launches the VirtualDOM from the specified component function and props.
  124. ///
  125. /// This method will block the thread with `spawn_local`
  126. ///
  127. /// # Example
  128. ///
  129. /// ```rust, ignore
  130. /// fn main() {
  131. /// dioxus_web::launch_with_props(
  132. /// App,
  133. /// RootProps { name: String::from("joe") },
  134. /// Config::new()
  135. /// );
  136. /// }
  137. ///
  138. /// #[derive(ParitalEq, Props)]
  139. /// struct RootProps {
  140. /// name: String
  141. /// }
  142. ///
  143. /// static App: Component<RootProps> = |cx| {
  144. /// render!(div {"hello {cx.props.name}"})
  145. /// }
  146. /// ```
  147. pub fn launch_with_props<T: 'static>(
  148. root_component: fn(Scope<T>) -> Element,
  149. root_properties: T,
  150. config: Config,
  151. ) {
  152. wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
  153. }
  154. /// Runs the app as a future that can be scheduled around the main thread.
  155. ///
  156. /// Polls futures internal to the VirtualDOM, hence the async nature of this function.
  157. ///
  158. /// # Example
  159. ///
  160. /// ```ignore
  161. /// fn main() {
  162. /// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") });
  163. /// wasm_bindgen_futures::spawn_local(app_fut);
  164. /// }
  165. /// ```
  166. pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_props: T, cfg: Config) {
  167. tracing::info!("Starting up");
  168. let mut dom = VirtualDom::new_with_props(root, root_props);
  169. #[cfg(feature = "eval")]
  170. {
  171. // Eval
  172. let cx = dom.base_scope();
  173. eval::init_eval(cx);
  174. }
  175. #[cfg(feature = "panic_hook")]
  176. if cfg.default_panic_hook {
  177. console_error_panic_hook::set_once();
  178. }
  179. #[cfg(all(feature = "hot_reload", debug_assertions))]
  180. let mut hotreload_rx = hot_reload::init();
  181. for s in crate::cache::BUILTIN_INTERNED_STRINGS {
  182. wasm_bindgen::intern(s);
  183. }
  184. for s in &cfg.cached_strings {
  185. wasm_bindgen::intern(s);
  186. }
  187. let (tx, mut rx) = futures_channel::mpsc::unbounded();
  188. #[cfg(feature = "hydrate")]
  189. let should_hydrate = cfg.hydrate;
  190. #[cfg(not(feature = "hydrate"))]
  191. let should_hydrate = false;
  192. let mut websys_dom = dom::WebsysDom::new(cfg, tx);
  193. tracing::info!("rebuilding app");
  194. if should_hydrate {
  195. #[cfg(feature = "hydrate")]
  196. {
  197. // todo: we need to split rebuild and initialize into two phases
  198. // it's a waste to produce edits just to get the vdom loaded
  199. let templates = dom.rebuild().templates;
  200. websys_dom.load_templates(&templates);
  201. if let Err(err) = websys_dom.rehydrate(&dom) {
  202. tracing::error!(
  203. "Rehydration failed {:?}. Rebuild DOM into element from scratch",
  204. &err
  205. );
  206. websys_dom.root.set_text_content(None);
  207. let edits = dom.rebuild();
  208. websys_dom.load_templates(&edits.templates);
  209. websys_dom.apply_edits(edits.edits);
  210. }
  211. }
  212. } else {
  213. let edits = dom.rebuild();
  214. websys_dom.load_templates(&edits.templates);
  215. websys_dom.apply_edits(edits.edits);
  216. }
  217. // the mutations come back with nothing - we need to actually mount them
  218. websys_dom.mount();
  219. loop {
  220. tracing::trace!("waiting for work");
  221. // if virtualdom has nothing, wait for it to have something before requesting idle time
  222. // if there is work then this future resolves immediately.
  223. let (mut res, template) = {
  224. let work = dom.wait_for_work().fuse();
  225. pin_mut!(work);
  226. #[cfg(all(feature = "hot_reload", debug_assertions))]
  227. // futures_util::select! {
  228. // _ = work => (None, None),
  229. // new_template = hotreload_rx.next() => {
  230. // (None, new_template)
  231. // }
  232. // evt = rx.next() =>
  233. // }
  234. match select(work, select(hotreload_rx.next(), rx.next())).await {
  235. Either::Left((_, _)) => (None, None),
  236. Either::Right((Either::Left((new_template, _)), _)) => (None, new_template),
  237. Either::Right((Either::Right((evt, _)), _)) => (evt, None),
  238. }
  239. #[cfg(not(all(feature = "hot_reload", debug_assertions)))]
  240. match select(work, rx.next()).await {
  241. Either::Left((_, _)) => (None, None),
  242. Either::Right((evt, _)) => (evt, None),
  243. }
  244. };
  245. if let Some(template) = template {
  246. dom.replace_template(template);
  247. }
  248. // Dequeue all of the events from the channel in send order
  249. // todo: we should re-order these if possible
  250. while let Some(evt) = res {
  251. dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles);
  252. res = rx.try_next().transpose().unwrap().ok();
  253. }
  254. // Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks
  255. // Jank free rendering
  256. //
  257. // 1. wait for the browser to give us "idle" time
  258. // 2. During idle time, diff the dom
  259. // 3. Stop diffing if the deadline is exceded
  260. // 4. Wait for the animation frame to patch the dom
  261. // wait for the mainthread to schedule us in
  262. // let deadline = work_loop.wait_for_idle_time().await;
  263. // run the virtualdom work phase until the frame deadline is reached
  264. let edits = dom.render_immediate();
  265. // wait for the animation frame to fire so we can apply our changes
  266. // work_loop.wait_for_raf().await;
  267. websys_dom.load_templates(&edits.templates);
  268. websys_dom.apply_edits(edits.edits);
  269. }
  270. }