diff --git a/.vscode/settings.json b/.vscode/settings.json index 2f418701b..80f51cff8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } \ No newline at end of file diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 5c8da440e..ef16ea2de 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -11,3 +11,8 @@ transpiling fullstack Serverless vnode +fn +Dodrio +fc +Jemalloc +Cloudfare diff --git a/README.md b/README.md index 159574755..9e6c7df58 100644 --- a/README.md +++ b/README.md @@ -54,30 +54,34 @@ Here, the `Context` object is used to access hook state, create subscriptions, a ```rust // A very terse component! #[fc] -async fn Example(ctx: &Context, name: String) -> VNode { +fn Example(ctx: &Context, name: String) -> VNode { html! {
"Hello {name}!"
} } // or #[functional_component] -static Example: FC = |ctx, name: String| async html! {
"Hello {name}!"
}; +static Example: FC = |ctx, name: String| html! {
"Hello {name}!"
}; ``` The final output of components must be a tree of VNodes. We provide an html macro for using JSX-style syntax to write these, though, you could use any macro, DSL, templating engine, or the constructors directly. ## Concurrency -In Dioxus, components are asynchronous and can their rendering can be paused at any time by awaiting a future. Hooks can combine this functionality with the Context and Subscription APIs to craft dynamic and efficient user experiences. +In Dioxus, VNodes are asynchronous and can their rendering can be paused at any time by awaiting a future. Hooks can combine this functionality with the Context and Subscription APIs to craft dynamic and efficient user experiences. ```rust -async fn user_data(ctx: &Context<()>) -> VNode { - let Profile { name, birthday, .. } = fetch_data().await; - html! { -
- {"Hello, {name}!"} - {if birthday === std::Instant::now() {html! {"Happy birthday!"}}} -
- } +fn user_data(ctx: &Context<()>) -> VNode { + // Register this future as a task + use_suspense(ctx, async { + // Continue on with the component as usual, waiting for data to arrive + let Profile { name, birthday, .. } = fetch_data().await; + html! { +
+ {"Hello, {name}!"} + {if birthday === std::Instant::now() {html! {"Happy birthday!"}}} +
+ } + }) } ``` Asynchronous components are powerful but can also be easy to misuse as they pause rendering for the component and its children. Refer to the concurrent guide for information on how to best use async components. @@ -112,6 +116,7 @@ Dioxus LiveHost is a paid service dedicated to hosting your Dioxus Apps - whethe - Analytics - Lighthouse optimization - On-premise support (see license terms) +- Cloudfare/DDoS protection integrations For small teams, LiveHost is free. Check out the pricing page to see if Dioxus LiveHost is good your team. @@ -121,9 +126,9 @@ We use the dedicated `dioxus-cli` to build and test dioxus web-apps. This can ru Alternatively, `trunk` works but can't run examples. - tide_ssr: Handle an HTTP request and return an html body using the html! macro. `cargo run --example tide_ssr` -- doc_generator: Use dioxus ssr to generate the website and docs. `cargo run --example doc_generator` +- doc_generator: Use dioxus SSR to generate the website and docs. `cargo run --example doc_generator` - fc_macro: Use the functional component macro to build terse components. `cargo run --example fc_macro` -- hello_web: Start a simple wasm app. Requires a webpacker like dioxus-cli or trunk `cargo run --example hello` +- hello_web: Start a simple wasm app. Requires a web packer like dioxus-cli or trunk `cargo run --example hello` - router: `cargo run --example router` - tide_ssr: `cargo run --example tide_ssr` - webview: Use liveview to bridge into a webview context for a simple desktop application. `cargo run --example webview` diff --git a/SOLVEDPROBLEMS.md b/SOLVEDPROBLEMS.md new file mode 100644 index 000000000..bf9591eca --- /dev/null +++ b/SOLVEDPROBLEMS.md @@ -0,0 +1,159 @@ +# Solved problems while building Dioxus + +## FC Macro for more elegant components +Originally the syntax of the FC macro was meant to look like: + +```rust +#[fc] +fn example(ctx: &Context<{ name: String }>) -> VNode { + html! {
"Hello, {name}!"
} +} +``` + +`Context` was originally meant to be more obviously parameterized around a struct definition. However, while this works with rustc, this does not work well with Rust Analyzer. Instead, the new form was chosen which works with Rust Analyzer and happens to be more ergonomic. + +```rust +#[fc] +fn example(ctx: &Context, name: String) -> VNode { + html! {
"Hello, {name}!"
} +} +``` + +## Anonymous Components + +In Yew, the function_component macro turns a struct into a Trait `impl` with associated type `props`. Like so: + +```rust +#[derive(Properties)] +struct Props { + // some props +} + +struct SomeComponent; +impl FunctionProvider for SomeComponent { + type TProps = Props; + + fn run(&mut self, props: &Props) -> Html { + // user's functional component goes here + } +} + +pub type SomeComponent = FunctionComponent; +``` +By default, the underlying component is defined as a "functional" implementation of the `Component` trait with all the lifecycle methods. In Dioxus, we don't allow components as structs, and instead take a "hooks-only" approach. However, we still need props. To get these without dealing with traits, we just assume functional components are modules. This lets the macros assume an FC is a module, and `FC::Props` is its props and `FC::component` is the component. Yew's method does a similar thing, but with associated types on traits. + +Perhaps one day we might use traits instead. + +The FC macro needs to work like this to generate a final module signature: + +```rust +// "Example" can be used directly +// The "associated types" are just children of the module +// That way, files can just be components (yay, no naming craziness) +mod Example { + // Associated metadata important for liveview + static NAME: &'static str = "Example"; + + struct Props { + name: String + } + + fn component(ctx: &Context) -> VNode { + html! {
"Hello, {name}!"
} + } +} + +// or, Example.rs + +static NAME: &'static str = "Example"; + +struct Props { + name: String +} + +fn component(ctx: &Context) -> VNode { + html! {
"Hello, {name}!"
} +} +``` + +These definitions might be ugly, but the fc macro cleans it all up. The fc macro also allows some configuration + +```rust +#[fc] +fn example(ctx: &Context, name: String) -> VNode { + html! {
"Hello, {name}!"
} +} + +// .. expands to + +mod Example { + use super::*; + static NAME: &'static str = "Example"; + struct Props { + name: String + } + fn component(ctx: &Context) -> VNode { + html! {
"Hello, {name}!"
} + } +} +``` + + + +## Live Components +Live components are a very important part of the Dioxus ecosystem. However, the goal with live components was to constrain their implementation purely to APIs available through Context (concurrency, context, subscription). + +From a certain perspective, live components are simply server-side-rendered components that update when their props change. Here's more-or-less how live components work: + +```rust +#[fc] +static LiveFc: FC = |ctx, refresh_handler: impl FnOnce| { + // Grab the "live context" + let live_context = ctx.use_context::(); + + // Ensure this component is registered as "live" + live_context.register_scope(); + + // send our props to the live context and get back a future + let vnodes = live_context.request_update(ctx); + + // Suspend the rendering of this component until the vnodes are finished arriving + // Render them once available + ctx.suspend(async move { + let output = vnodes.await; + + // inject any listener handles (ie button clicks, views, etc) to the parsed nodes + output[1].add_listener("onclick", refresh_handler); + + // Return these nodes + // Nodes skip diffing and go straight to rendering + output + }) +} +``` + +Notice that LiveComponent receivers (the client-side interpretation of a LiveComponent) are simply suspended components waiting for updates from the LiveContext (the context that wraps the app to make it "live"). + +## Allocation Strategy (ie incorporating Dodrio research) +---- +The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like: + +```rust +static Example: FC<()> = |ctx| { + html! {
"blah"
} +}; + +// expands to... + +static Example: FC<()> = |ctx| { + // This function converts a Fn(allocator) -> VNode closure to a DomTree struct that will later be evaluated. + html_macro_to_vnodetree(move |allocator| { + let mut node0 = allocator.alloc(VElement::div); + let node1 = allocator.alloc_text("blah"); + node0.children = [node1]; + node0 + }) +}; +``` +At runtime, the new closure is created that captures references to `ctx`. Therefore, this closure can only be evaluated while `ctx` is borrowed and in scope. However, this closure can only be evaluated with an `allocator`. Currently, the global and Bumpalo allocators are available, though in the future we will add support for creating a VDom with any allocator or arena system (IE Jemalloc, wee-alloc, etc). The intention here is to allow arena allocation of VNodes (no need to box nested VNodes). Between diffing phases, the arena will be overwritten as old nodes are replaced with new nodes. This saves allocation time and enables bump allocators. + diff --git a/examples/fc_macro.rs b/examples/fc_macro.rs index cff3de0e9..682f6444c 100644 --- a/examples/fc_macro.rs +++ b/examples/fc_macro.rs @@ -20,3 +20,76 @@ fn main() { fn example(ctx: &Context, name: String) -> VNode { html! {
"Hello, {name}!"
} } + +/* +TODO + +/// The macro can also be applied to statics in order to make components less verbose +/// The FC type automatically adds the inference, and property fields are automatically added as function arguments +#[fc] +static Example: FC = |ctx, name: String| { + html! {
"Hello, {name}!"
} +}; +*/ + +// This trait is not exposed to users directly, though they could manually implement this for struct-style components +trait Comp { + type Props: Properties; + fn render(&self, ctx: &mut Context) -> VNode; + fn builder(&self) -> Self::Props; +} +trait Properties { + fn new() -> Self; +} + +impl Comp for FC { + type Props = T; + + fn render(&self, ctx: &mut Context) -> VNode { + let g = self(ctx); + g + } + + fn builder(&self) -> T { + T::new() + } +} + +// impl) -> VNode> Comp for F { +// type Props = T; + +// fn render(&self, ctx: &mut Context) -> VNode { +// let g = self(ctx); +// g +// } + +// fn builder(&self) -> T { +// T::new() +// } +// } + +impl Properties for () { + fn new() -> Self { + () + } +} +#[allow(unused, non_upper_case_globals)] +static MyComp: FC<()> = |ctx| { + html! { +
+
+ } +}; + +fn my_comp(ctx: &Context<()>) -> VNode { + todo!() +} + +fn test() { + let mut ctx = Context { props: &() }; + let f = MyComp.render(&mut ctx); + let props = MyComp.builder(); + + // let f = my_comp.render(&mut ctx); + // let props = my_comp.builder(); +} diff --git a/examples/tide_ssr.rs b/examples/tide_ssr.rs index 68cf9444f..409b86b37 100644 --- a/examples/tide_ssr.rs +++ b/examples/tide_ssr.rs @@ -8,6 +8,7 @@ //! thousands of simultaneous clients on minimal hardware. use dioxus::prelude::*; +use dioxus_ssr::TextRenderer; use rand::Rng; use tide::{Request, Response}; @@ -50,20 +51,17 @@ async fn fibsum(req: Request<()>) -> tide::Result { // Generate another random number to try let other_fib_to_try = rand::thread_rng().gen_range(1..42); - let g = html! { -
-
- }; - - let nodes = html! { + let text = TextRenderer::<()>::to_text(html! { + // Header + // Body
@@ -97,11 +95,10 @@ async fn fibsum(req: Request<()>) -> tide::Result { - }; + }); Ok(Response::builder(203) - .body(nodes.to_string()) - .header("custom-header", "value") + .body(text) .content_type(tide::http::mime::HTML) .build()) } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 1327329b0..dabd6357a 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -186,6 +186,48 @@ impl Parse for FunctionComponent { fn ensure_fn_block(item: Item) -> syn::Result { match item { Item::Fn(it) => Ok(it), + Item::Static(it) => { + let syn::ItemStatic { + attrs, + vis, + static_token, + mutability, + ident, + colon_token, + ty, + eq_token, + expr, + semi_token, + } = ⁢ + match ty.as_ref() { + Type::BareFn(bare) => {} + // Type::Array(_) + // | Type::Group(_) + // | Type::ImplTrait(_) + // | Type::Infer(_) + // | Type::Macro(_) + // | Type::Never(_) + // | Type::Paren(_) + // | Type::Path(_) + // | Type::Ptr(_) + // | Type::Reference(_) + // | Type::Slice(_) + // | Type::TraitObject(_) + // | Type::Tuple(_) + // | Type::Verbatim(_) + _ => {} + }; + + // TODO: Add support for static block + // Ensure that the contents of the static block can be extracted to a function + // TODO: @Jon + // Decide if statics should be converted to functions (under the hood) or stay as statics + // They _do_ get promoted, but also have a &'static ref + Err(syn::Error::new_spanned( + it, + "`function_component` attribute not ready for statics", + )) + } other => Err(syn::Error::new_spanned( other, "`function_component` attribute can only be applied to functions", diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json new file mode 100644 index 000000000..2f418701b --- /dev/null +++ b/packages/core/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.inlayHints.enable": true +} \ No newline at end of file diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 7e9685a5f..410bb970c 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -10,13 +10,13 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -generational-arena = "0.2.8" -once_cell = "1.5.2" dioxus-html-macro = { path = "../html-macro", version = "0.1.0" } +# Backs some static data +once_cell = "1.5.2" -# web-sys = "0.3.46" -# js-sys = "0.3.46" -# typed-arena = "2.0.1" -# virtual-dom-rs = { path = "../virtual-dom-rs" } -# virtual-node = { path = "../virtual-node" } +# Backs the scope creation and reutilization +generational-arena = "0.2.8" + +# Bumpalo backs the VNode creation +bumpalo = { version = "3.6.0", features = ["collections"] } diff --git a/packages/core/README.md b/packages/core/README.md index aa34e371e..53a934881 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,22 +1,39 @@ # Dioxus-core -In the world of Rust UI + wasm UI toolkits, Dioxus is taking a fairly opinionated stance by being functional-only with hooks/context as the primary method of interacting with state. +This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website. -To focus on building an effective user experience, +## Internals +Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts: +- React: hooks, concurrency, suspense +- Dodrio: bump allocation / custom faster allocators, stack machine for fast changes +- Percy: html! macro implementation and agnostic edit list +- Yew: passion and inspiration ❤️ +## Goals +We have big goals for Dioxus. The final implementation must: +- Be **fast**. Allocators are typically slow in WASM/Rust, so we should have a smart way of allocating. +- Be concurrent. Components should be able to pause rendering using a threading mechanism. +- Support "broadcasting". Edit lists should be separate from the Renderer implementation. +- Support SSR. VNodes should render to a string that can be served via a web server. +- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. +- Be modular. Components and hooks should be work anywhere without worrying about target platform. +## Optimizations -Sources ---- -percy: https://github.com/chinedufn/percy/blob/master/crates/virtual-dom-rs/src/patch/mod.rs -yew: -dodrio: -rsx: +- Support a pluggable allocation strategy that makes VNode creation **very** fast +- Support lazy DomTrees (ie DomTrees that are not actually created when the view fn is ran) +- Support advanced diffing strategies -react: -fre: +## Design Quirks +- Use of "Context" as a way of mitigating threading issues and the borrow checker. (JS relies on globals) +- html! is lazy - needs to be used with a partner function to actually allocate the html. + +```rust +let text = TextRenderer::render(html! {
"hello world"
}); +//
hello world
+``` diff --git a/packages/core/examples/dummy.rs b/packages/core/examples/dummy.rs new file mode 100644 index 000000000..1cc6a8706 --- /dev/null +++ b/packages/core/examples/dummy.rs @@ -0,0 +1,49 @@ +// #![allow(unused, non_upper_case_globals)] +// use bumpalo::Bump; +// use dioxus_core::nodebuilder::*; +// use dioxus_core::{nodes::DomTree, prelude::*}; +// use std::{collections::HashMap, future::Future, marker::PhantomData}; + +fn main() {} + +// struct DummyRenderer { +// alloc: Bump, +// } + +// impl DummyRenderer { +// // "Renders" a domtree by logging its children and outputs +// fn render() {} + +// // Takes a domtree, an initial value, a new value, and produces the diff list +// fn produce_diffs() {} +// } + +// struct Props<'a> { +// name: &'a str, +// } + +// /// This component does "xyz things" +// /// This is sample documentation +// static Component: FC = |ctx| { +// // This block mimics that output of the html! macro + +// DomTree::new(move |bump| { +// // parse into RSX structures +// // regurgetate as rust types + +// //
"Child 1" "Child 2"
+// div(bump) +// .attr("class", "edit") +// .child(text("Child 1")) +// .child(text("Child 2")) +// .finish() +// }) +// }; + +// /* +// source +// |> c1 -> VNode +// |> c2 -> VNode +// |> c3 -> VNode +// |> c4 -> VNode +// */ diff --git a/packages/core/examples/macrosrc.rs b/packages/core/examples/macrosrc.rs new file mode 100644 index 000000000..893744763 --- /dev/null +++ b/packages/core/examples/macrosrc.rs @@ -0,0 +1,57 @@ +#![allow(unused, non_upper_case_globals)] +use bumpalo::Bump; +use dioxus_core::prelude::*; +use dioxus_core::{nodebuilder::*, virtual_dom::DomTree}; +use std::{collections::HashMap, future::Future, marker::PhantomData}; + +fn main() {} +struct Props<'a> { + use_macro: bool, + + // todo uh not static + // incorporate lifetimes into the thing somehow + text: &'a str, +} + +fn Component(ctx: Context) -> VNode { + // Write asynchronous rendering code that immediately returns a "suspended" VNode + // The concurrent API will then progress this component when the future finishes + // You can suspend the entire component, or just parts of it + let product_list = ctx.suspend(async { + // Suspend the rendering that completes when the future is done + match fetch_data().await { + Ok(data) => html! {
}, + Err(_) => html! {
}, + } + }); + + // VNodes can be constructed via a builder or the html! macro + // However, both of these are "lazy" - they need to be evaluated (aka, "viewed") + // We can "view" them with Context for ultimate speed while inside components + if ctx.props.use_macro { + ctx.view(|bump| { + div(bump) + .attr("class", "edit") + .child(text("Hello")) + .child(text(ctx.props.text)) + .finish() + }) + } else { + // "View" indicates exactly *when* allocations take place, everything is lazy up to this point + ctx.view(html! { +
+ // TODO! + // Get all this working again + //

"Products"

+ // // Subnodes can even be suspended + // // When completely rendered, they won't cause the component itself to re-render, just their slot + //

{product_list}

+
+ }) + } +} + +/// An example of a datafetching service +async fn fetch_data() -> Result { + todo!() +} diff --git a/packages/core/examples/simple.rs b/packages/core/examples/simple.rs deleted file mode 100644 index b44d95261..000000000 --- a/packages/core/examples/simple.rs +++ /dev/null @@ -1,103 +0,0 @@ -use std::future::Future; - -use dioxus_core::{prelude::*, virtual_dom::Properties}; -// use virtual_dom_rs::Closure; - -// Stop-gap while developing -// Need to update the macro -type VirtualNode = VNode; - -pub fn main() { - let dom = VirtualDom::new_with_props(root); - // let mut renderer = TextRenderer::new(dom); - // let output = renderer.render(); -} - -#[derive(PartialEq)] -struct Props { - name: String, -} -impl Properties for Props {} - -fn root(ctx: &mut Context) -> VNode { - // the regular html syntax - - // html! { - // - // - // - //