123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- #![deny(missing_docs)]
- //! Dioxus WebSys
- //!
- //! ## Overview
- //! ------------
- //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for
- //! Dioxus is one of the more advanced renderers, supporting:
- //! - idle work
- //! - animations
- //! - jank-free rendering
- //! - controlled components
- //! - hydration
- //! - and more.
- //!
- //! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate.
- //!
- //! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
- //! validation of websys-specific features and not the general use of Dioxus.
- // ## RequestAnimationFrame and RequestIdleCallback
- // ------------------------------------------------
- // React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
- // running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
- // main thread.
- //
- // React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
- // the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase
- // is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
- //
- // Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
- // setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
- //
- // From Google's guide on rAF and rIC:
- // -----------------------------------
- //
- // If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
- // which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
- // of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
- // frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
- // which is a potential performance bottleneck.
- //
- // Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
- // and as such we could easily go past the deadline the browser provided.
- //
- // The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
- // browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
- // be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
- // to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
- //
- // Essentially:
- // ------------
- // - Do the VDOM work during the idlecallback
- // - Do DOM work in the next requestAnimationFrame callback
- pub use crate::cfg::Config;
- use dioxus_core::{Element, Scope, VirtualDom};
- use futures_util::{
- future::{select, Either},
- pin_mut, FutureExt, StreamExt,
- };
- mod cache;
- mod cfg;
- mod dom;
- #[cfg(feature = "eval")]
- mod eval;
- #[cfg(feature = "file_engine")]
- mod file_engine;
- #[cfg(all(feature = "hot_reload", debug_assertions))]
- mod hot_reload;
- #[cfg(feature = "hydrate")]
- mod rehydrate;
- // Currently disabled since it actually slows down immediate rendering
- // todo: only schedule non-immediate renders through ric/raf
- // mod ric_raf;
- // mod rehydrate;
- /// Launch the VirtualDOM given a root component and a configuration.
- ///
- /// This function expects the root component to not have root props. To launch the root component with root props, use
- /// `launch_with_props` instead.
- ///
- /// This method will block the thread with `spawn_local` from wasm_bindgen_futures.
- ///
- /// If you need to run the VirtualDOM in its own thread, use `run_with_props` instead and await the future.
- ///
- /// # Example
- ///
- /// ```rust, ignore
- /// fn main() {
- /// dioxus_web::launch(App);
- /// }
- ///
- /// static App: Component = |cx| {
- /// render!(div {"hello world"})
- /// }
- /// ```
- pub fn launch(root_component: fn(Scope) -> Element) {
- launch_with_props(root_component, (), Config::default());
- }
- /// Launch your app and run the event loop, with configuration.
- ///
- /// This function will start your web app on the main web thread.
- ///
- /// You can configure the WebView window with a configuration closure
- ///
- /// ```rust, ignore
- /// use dioxus::prelude::*;
- ///
- /// fn main() {
- /// dioxus_web::launch_with_props(App, Config::new().pre_render(true));
- /// }
- ///
- /// fn app(cx: Scope) -> Element {
- /// cx.render(rsx!{
- /// h1 {"hello world!"}
- /// })
- /// }
- /// ```
- pub fn launch_cfg(root: fn(Scope) -> Element, config: Config) {
- launch_with_props(root, (), config)
- }
- /// Launches the VirtualDOM from the specified component function and props.
- ///
- /// This method will block the thread with `spawn_local`
- ///
- /// # Example
- ///
- /// ```rust, ignore
- /// fn main() {
- /// dioxus_web::launch_with_props(
- /// App,
- /// RootProps { name: String::from("joe") },
- /// Config::new()
- /// );
- /// }
- ///
- /// #[derive(ParitalEq, Props)]
- /// struct RootProps {
- /// name: String
- /// }
- ///
- /// static App: Component<RootProps> = |cx| {
- /// render!(div {"hello {cx.props.name}"})
- /// }
- /// ```
- pub fn launch_with_props<T: 'static>(
- root_component: fn(Scope<T>) -> Element,
- root_properties: T,
- config: Config,
- ) {
- wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
- }
- /// Runs the app as a future that can be scheduled around the main thread.
- ///
- /// Polls futures internal to the VirtualDOM, hence the async nature of this function.
- ///
- /// # Example
- ///
- /// ```ignore
- /// fn main() {
- /// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") });
- /// wasm_bindgen_futures::spawn_local(app_fut);
- /// }
- /// ```
- pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_props: T, cfg: Config) {
- log::info!("Starting up");
- let mut dom = VirtualDom::new_with_props(root, root_props);
- #[cfg(feature = "eval")]
- {
- // Eval
- let cx = dom.base_scope();
- eval::init_eval(cx);
- }
- #[cfg(feature = "panic_hook")]
- if cfg.default_panic_hook {
- console_error_panic_hook::set_once();
- }
- #[cfg(all(feature = "hot_reload", debug_assertions))]
- let mut hotreload_rx = hot_reload::init();
- for s in crate::cache::BUILTIN_INTERNED_STRINGS {
- wasm_bindgen::intern(s);
- }
- for s in &cfg.cached_strings {
- wasm_bindgen::intern(s);
- }
- let (tx, mut rx) = futures_channel::mpsc::unbounded();
- #[cfg(feature = "hydrate")]
- let should_hydrate = cfg.hydrate;
- #[cfg(not(feature = "hydrate"))]
- let should_hydrate = false;
- let mut websys_dom = dom::WebsysDom::new(cfg, tx);
- log::info!("rebuilding app");
- if should_hydrate {
- #[cfg(feature = "hydrate")]
- {
- // todo: we need to split rebuild and initialize into two phases
- // it's a waste to produce edits just to get the vdom loaded
- let templates = dom.rebuild().templates;
- websys_dom.load_templates(&templates);
- if let Err(err) = websys_dom.rehydrate(&dom) {
- log::error!(
- "Rehydration failed {:?}. Rebuild DOM into element from scratch",
- &err
- );
- websys_dom.root.set_text_content(None);
- let edits = dom.rebuild();
- websys_dom.load_templates(&edits.templates);
- websys_dom.apply_edits(edits.edits);
- }
- }
- } else {
- let edits = dom.rebuild();
- websys_dom.load_templates(&edits.templates);
- websys_dom.apply_edits(edits.edits);
- }
- // the mutations come back with nothing - we need to actually mount them
- websys_dom.mount();
- loop {
- log::trace!("waiting for work");
- // if virtualdom has nothing, wait for it to have something before requesting idle time
- // if there is work then this future resolves immediately.
- let (mut res, template) = {
- let work = dom.wait_for_work().fuse();
- pin_mut!(work);
- #[cfg(all(feature = "hot_reload", debug_assertions))]
- // futures_util::select! {
- // _ = work => (None, None),
- // new_template = hotreload_rx.next() => {
- // (None, new_template)
- // }
- // evt = rx.next() =>
- // }
- match select(work, select(hotreload_rx.next(), rx.next())).await {
- Either::Left((_, _)) => (None, None),
- Either::Right((Either::Left((new_template, _)), _)) => (None, new_template),
- Either::Right((Either::Right((evt, _)), _)) => (evt, None),
- }
- #[cfg(not(all(feature = "hot_reload", debug_assertions)))]
- match select(work, rx.next()).await {
- Either::Left((_, _)) => (None, None),
- Either::Right((evt, _)) => (evt, None),
- }
- };
- if let Some(template) = template {
- dom.replace_template(template);
- }
- // Dequeue all of the events from the channel in send order
- // todo: we should re-order these if possible
- while let Some(evt) = res {
- dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles);
- res = rx.try_next().transpose().unwrap().ok();
- }
- // Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks
- // Jank free rendering
- //
- // 1. wait for the browser to give us "idle" time
- // 2. During idle time, diff the dom
- // 3. Stop diffing if the deadline is exceded
- // 4. Wait for the animation frame to patch the dom
- // wait for the mainthread to schedule us in
- // let deadline = work_loop.wait_for_idle_time().await;
- // run the virtualdom work phase until the frame deadline is reached
- let edits = dom.render_immediate();
- // wait for the animation frame to fire so we can apply our changes
- // work_loop.wait_for_raf().await;
- websys_dom.load_templates(&edits.templates);
- websys_dom.apply_edits(edits.edits);
- }
- }
|