mirror of https://github.com/yewstack/yew
Server-side Rendering (without hydration) (#2335)
* Basic render to html implementation. * Remove HtmlWriter. * Escape html content. * Add non-suspense tests. * Add Suspense tests. * Gated "ssr" feature. * Add example. * Fix tests. * Fix docs. * Fix heading size. * Remove the unused YewRenderer. * Remove extra comment. * unify naming. * Update docs. * Update docs. * Update docs. * Isolate spawn_local. * Add doc flags. * Add ssr feature to docs. * Move ServerRenderer into their own file. * Fix docs. * Update features and docs. * Fix example. * Adjust comment position. * Fix effects being wrongly called when a component is suspended. * Fix clippy. * Uuid & no double boxing. Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
This commit is contained in:
parent
fff1ffaab8
commit
d8c2550fc7
|
@ -61,6 +61,11 @@ jobs:
|
|||
continue
|
||||
fi
|
||||
|
||||
# ssr does not need trunk
|
||||
if [[ "$example" == "simple_ssr" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "building: $example"
|
||||
(
|
||||
cd "$path"
|
||||
|
|
|
@ -27,6 +27,7 @@ members = [
|
|||
"examples/password_strength",
|
||||
"examples/portals",
|
||||
"examples/router",
|
||||
"examples/simple_ssr",
|
||||
"examples/timer",
|
||||
"examples/todomvc",
|
||||
"examples/two_apps",
|
||||
|
|
|
@ -38,7 +38,7 @@ category = "Testing"
|
|||
description = "Run all tests"
|
||||
dependencies = ["tests-setup"]
|
||||
env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*", "**/packages/changelog"] }
|
||||
run_task = { name = ["test-flow", "doc-test-flow", "website-test"], fork = true }
|
||||
run_task = { name = ["test-flow", "doc-test-flow", "ssr-test", "website-test"], fork = true }
|
||||
|
||||
[tasks.benchmarks]
|
||||
category = "Testing"
|
||||
|
@ -117,3 +117,8 @@ category = "Maintainer processes"
|
|||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["run","-p","changelog", "--release", "${@}"]
|
||||
|
||||
[tasks.ssr-test]
|
||||
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/yew"] }
|
||||
private = true
|
||||
workspace = true
|
||||
|
|
|
@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
|
|||
pulldown-cmark = { version = "0.9", default-features = false }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["tokio"] }
|
||||
gloo-utils = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "simple_ssr"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
warp = "0.3"
|
||||
yew = { path = "../../packages/yew", features = ["ssr"] }
|
||||
reqwest = { version = "0.11.8", features = ["json"] }
|
||||
serde = { version = "1.0.132", features = ["derive"] }
|
||||
uuid = { version = "0.8.2", features = ["serde"] }
|
|
@ -0,0 +1,6 @@
|
|||
# Server-side Rendering Example
|
||||
|
||||
This example demonstrates server-side rendering.
|
||||
|
||||
Run `cargo run -p simple_ssr` and navigate to http://localhost:8080/ to
|
||||
view results.
|
|
@ -0,0 +1,129 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task::LocalSet;
|
||||
use tokio::task::{spawn_blocking, spawn_local};
|
||||
use uuid::Uuid;
|
||||
use warp::Filter;
|
||||
use yew::prelude::*;
|
||||
use yew::suspense::{Suspension, SuspensionResult};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UuidResponse {
|
||||
uuid: Uuid,
|
||||
}
|
||||
|
||||
async fn fetch_uuid() -> Uuid {
|
||||
// reqwest works for both non-wasm and wasm targets.
|
||||
let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap();
|
||||
let uuid_resp = resp.json::<UuidResponse>().await.unwrap();
|
||||
|
||||
uuid_resp.uuid
|
||||
}
|
||||
|
||||
pub struct UuidState {
|
||||
s: Suspension,
|
||||
value: Rc<RefCell<Option<Uuid>>>,
|
||||
}
|
||||
|
||||
impl UuidState {
|
||||
fn new() -> Self {
|
||||
let (s, handle) = Suspension::new();
|
||||
let value: Rc<RefCell<Option<Uuid>>> = Rc::default();
|
||||
|
||||
{
|
||||
let value = value.clone();
|
||||
// we use tokio spawn local here.
|
||||
spawn_local(async move {
|
||||
let uuid = fetch_uuid().await;
|
||||
|
||||
{
|
||||
let mut value = value.borrow_mut();
|
||||
*value = Some(uuid);
|
||||
}
|
||||
|
||||
handle.resume();
|
||||
});
|
||||
}
|
||||
|
||||
Self { s, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UuidState {
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
self.s == rhs.s
|
||||
}
|
||||
}
|
||||
|
||||
fn use_random_uuid() -> SuspensionResult<Uuid> {
|
||||
let s = use_state(UuidState::new);
|
||||
|
||||
let result = match *s.value.borrow() {
|
||||
Some(ref m) => Ok(*m),
|
||||
None => Err(s.s.clone()),
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Content() -> HtmlResult {
|
||||
let uuid = use_random_uuid()?;
|
||||
|
||||
Ok(html! {
|
||||
<div>{"Random UUID: "}{uuid}</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn App() -> Html {
|
||||
let fallback = html! {<div>{"Loading..."}</div>};
|
||||
|
||||
html! {
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
async fn render() -> String {
|
||||
let content = spawn_blocking(move || {
|
||||
use tokio::runtime::Builder;
|
||||
let set = LocalSet::new();
|
||||
|
||||
let rt = Builder::new_current_thread().enable_all().build().unwrap();
|
||||
|
||||
set.block_on(&rt, async {
|
||||
let renderer = yew::ServerRenderer::<App>::new();
|
||||
|
||||
renderer.render().await
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("the thread has failed.");
|
||||
|
||||
format!(
|
||||
r#"<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yew SSR Example</title>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let routes = warp::any().then(|| async move { warp::reply::html(render().await) });
|
||||
|
||||
println!("You can view the website at: http://localhost:8080/");
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
}
|
|
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["tokio"] }
|
||||
gloo-timers = { version = "0.2.2", features = ["futures"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
wasm-bindgen = "0.2"
|
||||
|
|
|
@ -24,12 +24,14 @@ indexmap = { version = "1", features = ["std"] }
|
|||
js-sys = "0.3"
|
||||
slab = "0.4"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
yew-macro = { version = "^0.19.0", path = "../yew-macro" }
|
||||
thiserror = "1.0"
|
||||
|
||||
scoped-tls-hkt = "0.1"
|
||||
|
||||
futures = { version = "0.3", optional = true }
|
||||
html-escape = { version = "0.2.9", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
|
@ -61,10 +63,19 @@ features = [
|
|||
"Window",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# we move it here so no promise-based spawn_local can present for
|
||||
# non-wasm32 targets.
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.15.0", features = ["rt"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
easybench-wasm = "0.2"
|
||||
wasm-bindgen-test = "0.3"
|
||||
gloo = { version = "0.6", features = ["futures"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
rustversion = "1"
|
||||
trybuild = "1"
|
||||
|
||||
|
@ -72,6 +83,12 @@ trybuild = "1"
|
|||
doc_test = []
|
||||
wasm_test = []
|
||||
wasm_bench = []
|
||||
ssr = ["futures", "html-escape"]
|
||||
default = []
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["doc_test"]
|
||||
features = ["doc_test", "ssr"]
|
||||
rustdoc-args = ["--cfg", "documenting"]
|
||||
|
|
|
@ -41,3 +41,7 @@ args = [
|
|||
"wasm_bench",
|
||||
"bench",
|
||||
]
|
||||
|
||||
[tasks.ssr-test]
|
||||
command = "cargo"
|
||||
args = ["test", "ssr_tests", "--features", "ssr"]
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::functional::use_hook;
|
||||
use std::{borrow::Borrow, rc::Rc};
|
||||
use std::rc::Rc;
|
||||
|
||||
struct UseEffect<Destructor> {
|
||||
runner: Option<Box<dyn FnOnce() -> Destructor>>,
|
||||
destructor: Option<Box<Destructor>>,
|
||||
}
|
||||
|
||||
|
@ -39,20 +40,27 @@ pub fn use_effect<Destructor>(callback: impl FnOnce() -> Destructor + 'static)
|
|||
where
|
||||
Destructor: FnOnce() + 'static,
|
||||
{
|
||||
let callback = Box::new(callback);
|
||||
use_hook(
|
||||
move || {
|
||||
let effect: UseEffect<Destructor> = UseEffect { destructor: None };
|
||||
let effect: UseEffect<Destructor> = UseEffect {
|
||||
runner: None,
|
||||
destructor: None,
|
||||
};
|
||||
effect
|
||||
},
|
||||
|_, updater| {
|
||||
|state, updater| {
|
||||
state.runner = Some(Box::new(callback) as Box<dyn FnOnce() -> Destructor>);
|
||||
|
||||
// Run on every render
|
||||
updater.post_render(move |state: &mut UseEffect<Destructor>| {
|
||||
if let Some(de) = state.destructor.take() {
|
||||
de();
|
||||
if let Some(callback) = state.runner.take() {
|
||||
if let Some(de) = state.destructor.take() {
|
||||
de();
|
||||
}
|
||||
|
||||
let new_destructor = callback();
|
||||
state.destructor.replace(Box::new(new_destructor));
|
||||
}
|
||||
let new_destructor = callback();
|
||||
state.destructor.replace(Box::new(new_destructor));
|
||||
false
|
||||
});
|
||||
},
|
||||
|
@ -64,9 +72,15 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
type UseEffectDepsRunnerFn<Dependents, Destructor> = Box<dyn FnOnce(&Dependents) -> Destructor>;
|
||||
|
||||
struct UseEffectDeps<Destructor, Dependents> {
|
||||
runner_with_deps: Option<(
|
||||
Rc<Dependents>,
|
||||
UseEffectDepsRunnerFn<Dependents, Destructor>,
|
||||
)>,
|
||||
destructor: Option<Box<Destructor>>,
|
||||
deps: Rc<Dependents>,
|
||||
deps: Option<Rc<Dependents>>,
|
||||
}
|
||||
|
||||
/// This hook is similar to [`use_effect`] but it accepts dependencies.
|
||||
|
@ -81,29 +95,33 @@ where
|
|||
Dependents: PartialEq + 'static,
|
||||
{
|
||||
let deps = Rc::new(deps);
|
||||
let deps_c = deps.clone();
|
||||
|
||||
use_hook(
|
||||
move || {
|
||||
let destructor: Option<Box<Destructor>> = None;
|
||||
UseEffectDeps {
|
||||
runner_with_deps: None,
|
||||
destructor,
|
||||
deps: deps_c,
|
||||
deps: None,
|
||||
}
|
||||
},
|
||||
move |_, updater| {
|
||||
move |state, updater| {
|
||||
state.runner_with_deps = Some((deps, Box::new(callback)));
|
||||
|
||||
updater.post_render(move |state: &mut UseEffectDeps<Destructor, Dependents>| {
|
||||
if state.deps != deps {
|
||||
if let Some((deps, callback)) = state.runner_with_deps.take() {
|
||||
if Some(&deps) == state.deps.as_ref() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(de) = state.destructor.take() {
|
||||
de();
|
||||
}
|
||||
let new_destructor = callback(deps.borrow());
|
||||
state.deps = deps;
|
||||
state.destructor.replace(Box::new(new_destructor));
|
||||
} else if state.destructor.is_none() {
|
||||
state
|
||||
.destructor
|
||||
.replace(Box::new(callback(state.deps.borrow())));
|
||||
|
||||
let new_destructor = callback(&deps);
|
||||
|
||||
state.deps = Some(deps);
|
||||
state.destructor = Some(Box::new(new_destructor));
|
||||
}
|
||||
false
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@ use crate::suspense::{Suspense, Suspension};
|
|||
use crate::virtual_dom::{VDiff, VNode};
|
||||
use crate::Callback;
|
||||
use crate::{Context, NodeRef};
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::channel::oneshot;
|
||||
use std::rc::Rc;
|
||||
use web_sys::Element;
|
||||
|
||||
|
@ -15,13 +17,19 @@ pub(crate) struct ComponentState<COMP: BaseComponent> {
|
|||
pub(crate) root_node: VNode,
|
||||
|
||||
context: Context<COMP>,
|
||||
parent: Element,
|
||||
|
||||
/// When a component has no parent, it means that it should not be rendered.
|
||||
parent: Option<Element>,
|
||||
|
||||
next_sibling: NodeRef,
|
||||
node_ref: NodeRef,
|
||||
has_rendered: bool,
|
||||
|
||||
suspension: Option<Suspension>,
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: Option<oneshot::Sender<VNode>>,
|
||||
|
||||
// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: u64,
|
||||
|
@ -29,12 +37,13 @@ pub(crate) struct ComponentState<COMP: BaseComponent> {
|
|||
|
||||
impl<COMP: BaseComponent> ComponentState<COMP> {
|
||||
pub(crate) fn new(
|
||||
parent: Element,
|
||||
parent: Option<Element>,
|
||||
next_sibling: NodeRef,
|
||||
root_node: VNode,
|
||||
node_ref: NodeRef,
|
||||
scope: Scope<COMP>,
|
||||
props: Rc<COMP::Properties>,
|
||||
#[cfg(feature = "ssr")] html_sender: Option<oneshot::Sender<VNode>>,
|
||||
) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
let vcomp_id = {
|
||||
|
@ -55,6 +64,9 @@ impl<COMP: BaseComponent> ComponentState<COMP> {
|
|||
suspension: None,
|
||||
has_rendered: false,
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
}
|
||||
|
@ -62,12 +74,14 @@ impl<COMP: BaseComponent> ComponentState<COMP> {
|
|||
}
|
||||
|
||||
pub(crate) struct CreateRunner<COMP: BaseComponent> {
|
||||
pub(crate) parent: Element,
|
||||
pub(crate) parent: Option<Element>,
|
||||
pub(crate) next_sibling: NodeRef,
|
||||
pub(crate) placeholder: VNode,
|
||||
pub(crate) node_ref: NodeRef,
|
||||
pub(crate) props: Rc<COMP::Properties>,
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) html_sender: Option<oneshot::Sender<VNode>>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
||||
|
@ -84,6 +98,8 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
|||
self.node_ref,
|
||||
self.scope.clone(),
|
||||
self.props,
|
||||
#[cfg(feature = "ssr")]
|
||||
self.html_sender,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -129,11 +145,13 @@ impl<COMP: BaseComponent> Runnable for UpdateRunner<COMP> {
|
|||
}
|
||||
}
|
||||
UpdateEvent::Shift(parent, next_sibling) => {
|
||||
state
|
||||
.root_node
|
||||
.shift(&state.parent, &parent, next_sibling.clone());
|
||||
state.root_node.shift(
|
||||
state.parent.as_ref().unwrap(),
|
||||
&parent,
|
||||
next_sibling.clone(),
|
||||
);
|
||||
|
||||
state.parent = parent;
|
||||
state.parent = Some(parent);
|
||||
state.next_sibling = next_sibling;
|
||||
|
||||
false
|
||||
|
@ -173,8 +191,11 @@ impl<COMP: BaseComponent> Runnable for DestroyRunner<COMP> {
|
|||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy");
|
||||
|
||||
state.component.destroy(&state.context);
|
||||
state.root_node.detach(&state.parent);
|
||||
state.node_ref.set(None);
|
||||
|
||||
if let Some(ref m) = state.parent {
|
||||
state.root_node.detach(m);
|
||||
state.node_ref.set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -194,24 +215,33 @@ impl<COMP: BaseComponent> Runnable for RenderRunner<COMP> {
|
|||
// Currently not suspended, we remove any previous suspension and update
|
||||
// normally.
|
||||
let mut root = m;
|
||||
std::mem::swap(&mut root, &mut state.root_node);
|
||||
if state.parent.is_some() {
|
||||
std::mem::swap(&mut root, &mut state.root_node);
|
||||
}
|
||||
|
||||
if let Some(ref m) = state.suspension {
|
||||
if let Some(m) = state.suspension.take() {
|
||||
let comp_scope = AnyScope::from(state.context.scope.clone());
|
||||
|
||||
let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
|
||||
let suspense = suspense_scope.get_component().unwrap();
|
||||
|
||||
suspense.resume(m.clone());
|
||||
suspense.resume(m);
|
||||
}
|
||||
|
||||
let ancestor = Some(root);
|
||||
let new_root = &mut state.root_node;
|
||||
let scope = state.context.scope.clone().into();
|
||||
let next_sibling = state.next_sibling.clone();
|
||||
if let Some(ref m) = state.parent {
|
||||
let ancestor = Some(root);
|
||||
let new_root = &mut state.root_node;
|
||||
let scope = state.context.scope.clone().into();
|
||||
let next_sibling = state.next_sibling.clone();
|
||||
|
||||
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
|
||||
state.node_ref.link(node);
|
||||
let node = new_root.apply(&scope, m, next_sibling, ancestor);
|
||||
state.node_ref.link(node);
|
||||
} else {
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(tx) = state.html_sender.take() {
|
||||
tx.send(root).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(RenderError::Suspended(m)) => {
|
||||
|
@ -236,7 +266,9 @@ impl<COMP: BaseComponent> Runnable for RenderRunner<COMP> {
|
|||
|
||||
let comp_scope = AnyScope::from(state.context.scope.clone());
|
||||
|
||||
let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
|
||||
let suspense_scope = comp_scope
|
||||
.find_parent_scope::<Suspense>()
|
||||
.expect("To suspend rendering, a <Suspense /> component is required.");
|
||||
let suspense = suspense_scope.get_component().unwrap();
|
||||
|
||||
m.listen(Callback::from(move |_| {
|
||||
|
@ -277,9 +309,11 @@ impl<COMP: BaseComponent> Runnable for RenderedRunner<COMP> {
|
|||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered");
|
||||
|
||||
let first_render = !state.has_rendered;
|
||||
state.component.rendered(&state.context, first_render);
|
||||
state.has_rendered = true;
|
||||
if state.suspension.is_none() && state.parent.is_some() {
|
||||
let first_render = !state.has_rendered;
|
||||
state.component.rendered(&state.context, first_render);
|
||||
state.has_rendered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,9 @@ use crate::virtual_dom::{insert_node, VNode};
|
|||
use gloo_utils::document;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::{fmt, iter};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// Untyped scope used for accessing parent scope
|
||||
|
@ -234,12 +232,14 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
|||
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
parent,
|
||||
parent: Some(parent),
|
||||
next_sibling,
|
||||
placeholder,
|
||||
node_ref,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: None,
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
|
@ -348,61 +348,6 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
|||
};
|
||||
closure.into()
|
||||
}
|
||||
/// This method creates a [`Callback`] which returns a Future which
|
||||
/// returns a message to be sent back to the component's event
|
||||
/// loop.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and
|
||||
/// will leak.
|
||||
pub fn callback_future<FN, FU, IN, M>(&self, function: FN) -> Callback<IN>
|
||||
where
|
||||
M: Into<COMP::Message>,
|
||||
FU: Future<Output = M> + 'static,
|
||||
FN: Fn(IN) -> FU + 'static,
|
||||
{
|
||||
let link = self.clone();
|
||||
|
||||
let closure = move |input: IN| {
|
||||
let future: FU = function(input);
|
||||
link.send_future(future);
|
||||
};
|
||||
|
||||
closure.into()
|
||||
}
|
||||
|
||||
/// This method processes a Future that returns a message and sends it back to the component's
|
||||
/// loop.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and will leak.
|
||||
pub fn send_future<F, M>(&self, future: F)
|
||||
where
|
||||
M: Into<COMP::Message>,
|
||||
F: Future<Output = M> + 'static,
|
||||
{
|
||||
let link = self.clone();
|
||||
let js_future = async move {
|
||||
let message: COMP::Message = future.await.into();
|
||||
link.send_message(message);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
|
||||
/// Registers a Future that resolves to multiple messages.
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and will leak.
|
||||
pub fn send_future_batch<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Vec<COMP::Message>> + 'static,
|
||||
{
|
||||
let link = self.clone();
|
||||
let js_future = async move {
|
||||
let messages: Vec<COMP::Message> = future.await;
|
||||
link.send_message_batch(messages);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
|
||||
/// Accesses a value provided by a parent `ContextProvider` component of the
|
||||
/// same type.
|
||||
|
@ -414,6 +359,113 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use futures::channel::oneshot;
|
||||
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc<COMP::Properties>) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
parent: None,
|
||||
next_sibling: NodeRef::default(),
|
||||
placeholder: VNode::default(),
|
||||
node_ref: NodeRef::default(),
|
||||
props,
|
||||
scope: self.clone(),
|
||||
html_sender: Some(tx),
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
RenderedRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
scheduler::start();
|
||||
|
||||
let html = rx.await.unwrap();
|
||||
|
||||
let self_any_scope = self.to_any();
|
||||
html.render_to_string(w, &self_any_scope).await;
|
||||
|
||||
scheduler::push_component_destroy(DestroyRunner {
|
||||
state: self.state.clone(),
|
||||
});
|
||||
scheduler::start();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
|
||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||
mod feat_io {
|
||||
use std::future::Future;
|
||||
|
||||
use super::*;
|
||||
use crate::io_coop::spawn_local;
|
||||
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
/// This method creates a [`Callback`] which returns a Future which
|
||||
/// returns a message to be sent back to the component's event
|
||||
/// loop.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and
|
||||
/// will leak.
|
||||
pub fn callback_future<FN, FU, IN, M>(&self, function: FN) -> Callback<IN>
|
||||
where
|
||||
M: Into<COMP::Message>,
|
||||
FU: Future<Output = M> + 'static,
|
||||
FN: Fn(IN) -> FU + 'static,
|
||||
{
|
||||
let link = self.clone();
|
||||
|
||||
let closure = move |input: IN| {
|
||||
let future: FU = function(input);
|
||||
link.send_future(future);
|
||||
};
|
||||
|
||||
closure.into()
|
||||
}
|
||||
|
||||
/// This method processes a Future that returns a message and sends it back to the component's
|
||||
/// loop.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and will leak.
|
||||
pub fn send_future<F, M>(&self, future: F)
|
||||
where
|
||||
M: Into<COMP::Message>,
|
||||
F: Future<Output = M> + 'static,
|
||||
{
|
||||
let link = self.clone();
|
||||
let js_future = async move {
|
||||
let message: COMP::Message = future.await.into();
|
||||
link.send_message(message);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
|
||||
/// Registers a Future that resolves to multiple messages.
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and will leak.
|
||||
pub fn send_future_batch<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Vec<COMP::Message>> + 'static,
|
||||
{
|
||||
let link = self.clone();
|
||||
let js_future = async move {
|
||||
let messages: Vec<COMP::Message> = future.await;
|
||||
link.send_message_batch(messages);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines a message type that can be sent to a component.
|
||||
/// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback).
|
||||
pub trait SendAsMessage<COMP: BaseComponent> {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
//! module that provides io compatibility over browser tasks and other async io tasks (e.g.: tokio)
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod io_wasm_bindgen {
|
||||
pub use wasm_bindgen_futures::spawn_local;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) use io_wasm_bindgen::*;
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
|
||||
mod io_tokio {
|
||||
use std::future::Future;
|
||||
|
||||
// spawn_local in tokio is more powerful, but we need to adjust the function signature to match
|
||||
// wasm_bindgen_futures.
|
||||
#[inline(always)]
|
||||
pub(crate) fn spawn_local<F>(f: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
tokio::task::spawn_local(f);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
|
||||
pub(crate) use io_tokio::*;
|
|
@ -1,5 +1,6 @@
|
|||
#![allow(clippy::needless_doctest_main)]
|
||||
#![doc(html_logo_url = "https://yew.rs/img/logo.png")]
|
||||
#![cfg_attr(documenting, feature(doc_cfg))]
|
||||
|
||||
//! # Yew Framework - API Documentation
|
||||
//!
|
||||
|
@ -9,9 +10,19 @@
|
|||
//! - Achieves high performance by minimizing DOM API calls for each page render and by making it easy to offload processing to background web workers.
|
||||
//! - Supports JavaScript interoperability, allowing developers to leverage NPM packages and integrate with existing JavaScript applications.
|
||||
//!
|
||||
//! ### Supported Targets
|
||||
//! ### Supported Targets (Client-Side Rendering)
|
||||
//! - `wasm32-unknown-unknown`
|
||||
//!
|
||||
//! ### Note
|
||||
//!
|
||||
//! Server-Side Rendering should work on all targets when feature `ssr` is enabled.
|
||||
//!
|
||||
//! ### Supported Features:
|
||||
//! - `ssr`: Enables Server-side Rendering support and [`ServerRenderer`].
|
||||
//! - `tokio`: Enables future-based APIs on non-wasm32 targets with tokio runtime. (You may want to
|
||||
//! enable this if your application uses future-based APIs and it does not compile / lint on
|
||||
//! non-wasm32 targets.)
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
|
@ -257,13 +268,18 @@ pub mod callback;
|
|||
pub mod context;
|
||||
pub mod functional;
|
||||
pub mod html;
|
||||
mod io_coop;
|
||||
pub mod scheduler;
|
||||
mod sealed;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod server_renderer;
|
||||
pub mod suspense;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
pub mod utils;
|
||||
pub mod virtual_dom;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use server_renderer::*;
|
||||
|
||||
/// The module that contains all events available in the framework.
|
||||
pub mod events {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
use super::*;
|
||||
|
||||
use crate::html::Scope;
|
||||
|
||||
/// A Yew Server-side Renderer.
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
|
||||
#[derive(Debug)]
|
||||
pub struct ServerRenderer<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
props: COMP::Properties,
|
||||
}
|
||||
|
||||
impl<COMP> Default for ServerRenderer<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
COMP::Properties: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::with_props(COMP::Properties::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP> ServerRenderer<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
COMP::Properties: Default,
|
||||
{
|
||||
/// Creates a [ServerRenderer] with default properties.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP> ServerRenderer<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
/// Creates a [ServerRenderer] with custom properties.
|
||||
pub fn with_props(props: COMP::Properties) -> Self {
|
||||
Self { props }
|
||||
}
|
||||
|
||||
/// Renders Yew Application.
|
||||
pub async fn render(self) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
self.render_to_string(&mut s).await;
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
/// Renders Yew Application to a String.
|
||||
pub async fn render_to_string(self, w: &mut String) {
|
||||
let scope = Scope::<COMP>::new(None);
|
||||
scope.render_to_string(w, self.props.into()).await;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
use crate::html::{Children, Component, Context, Html, Properties, Scope};
|
||||
use crate::virtual_dom::{Key, VList, VNode, VSuspense};
|
||||
|
||||
use gloo_utils::document;
|
||||
use web_sys::Element;
|
||||
|
||||
use super::Suspension;
|
||||
|
@ -29,7 +28,7 @@ pub enum SuspenseMsg {
|
|||
pub struct Suspense {
|
||||
link: Scope<Self>,
|
||||
suspensions: Vec<Suspension>,
|
||||
detached_parent: Element,
|
||||
detached_parent: Option<Element>,
|
||||
}
|
||||
|
||||
impl Component for Suspense {
|
||||
|
@ -40,7 +39,14 @@ impl Component for Suspense {
|
|||
Self {
|
||||
link: ctx.link().clone(),
|
||||
suspensions: Vec::new(),
|
||||
detached_parent: document().create_element("div").unwrap(),
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
detached_parent: web_sys::window()
|
||||
.and_then(|m| m.document())
|
||||
.and_then(|m| m.create_element("div").ok()),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
detached_parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||
use std::task::{Context, Poll};
|
||||
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
use crate::Callback;
|
||||
|
||||
|
@ -52,18 +51,6 @@ impl Suspension {
|
|||
(self_.clone(), SuspensionHandle { inner: self_ })
|
||||
}
|
||||
|
||||
/// Creates a Suspension that resumes when the [`Future`] resolves.
|
||||
pub fn from_future(f: impl Future<Output = ()> + 'static) -> Self {
|
||||
let (self_, handle) = Self::new();
|
||||
|
||||
spawn_local(async move {
|
||||
f.await;
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
self_
|
||||
}
|
||||
|
||||
/// Returns `true` if the current suspension is already resumed.
|
||||
pub fn resumed(&self) -> bool {
|
||||
self.resumed.load(Ordering::Relaxed)
|
||||
|
@ -138,3 +125,24 @@ impl Drop for SuspensionHandle {
|
|||
self.inner.resume_by_ref();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
|
||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||
mod feat_io {
|
||||
use super::*;
|
||||
use crate::io_coop::spawn_local;
|
||||
|
||||
impl Suspension {
|
||||
/// Creates a Suspension that resumes when the [`Future`] resolves.
|
||||
pub fn from_future(f: impl Future<Output = ()> + 'static) -> Self {
|
||||
let (self_, handle) = Self::new();
|
||||
|
||||
spawn_local(async move {
|
||||
f.await;
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
self_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
use super::{Key, VDiff, VNode};
|
||||
use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped};
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
use std::any::TypeId;
|
||||
use std::borrow::Borrow;
|
||||
use std::fmt;
|
||||
|
@ -41,7 +43,7 @@ pub(crate) fn get_event_log(vcomp_id: u64) -> Vec<String> {
|
|||
pub struct VComp {
|
||||
type_id: TypeId,
|
||||
scope: Option<Box<dyn Scoped>>,
|
||||
props: Option<Box<dyn Mountable>>,
|
||||
mountable: Option<Box<dyn Mountable>>,
|
||||
pub(crate) node_ref: NodeRef,
|
||||
pub(crate) key: Option<Key>,
|
||||
|
||||
|
@ -62,7 +64,7 @@ impl Clone for VComp {
|
|||
Self {
|
||||
type_id: self.type_id,
|
||||
scope: None,
|
||||
props: self.props.as_ref().map(|m| m.copy()),
|
||||
mountable: self.mountable.as_ref().map(|m| m.copy()),
|
||||
node_ref: self.node_ref.clone(),
|
||||
key: self.key.clone(),
|
||||
|
||||
|
@ -132,7 +134,7 @@ impl VComp {
|
|||
VComp {
|
||||
type_id: TypeId::of::<COMP>(),
|
||||
node_ref,
|
||||
props: Some(Box::new(PropsWrapper::<COMP>::new(props))),
|
||||
mountable: Some(Box::new(PropsWrapper::<COMP>::new(props))),
|
||||
scope: None,
|
||||
key,
|
||||
|
||||
|
@ -181,6 +183,13 @@ trait Mountable {
|
|||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped>;
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
}
|
||||
|
||||
struct PropsWrapper<COMP: BaseComponent> {
|
||||
|
@ -218,6 +227,19 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
|||
let scope: Scope<COMP> = scope.to_any().downcast();
|
||||
scope.reuse(self.props, node_ref, next_sibling);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.render_to_string(w, self.props.clone()).await;
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
impl VDiff for VComp {
|
||||
|
@ -237,7 +259,10 @@ impl VDiff for VComp {
|
|||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
let mountable = self.props.take().expect("VComp has already been mounted");
|
||||
let mountable = self
|
||||
.mountable
|
||||
.take()
|
||||
.expect("VComp has already been mounted");
|
||||
|
||||
if let Some(mut ancestor) = ancestor {
|
||||
if let VNode::VComp(ref mut vcomp) = &mut ancestor {
|
||||
|
@ -283,6 +308,22 @@ impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
|
||||
impl VComp {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
self.mountable
|
||||
.as_ref()
|
||||
.map(|m| m.copy())
|
||||
.unwrap()
|
||||
.render_to_string(w, parent_scope)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -867,3 +908,44 @@ mod layout_tests {
|
|||
diff_layouts(vec![layout]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::ServerRenderer;
|
||||
|
||||
#[test]
|
||||
async fn test_props() {
|
||||
#[derive(PartialEq, Properties, Debug)]
|
||||
struct ChildProps {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Child(props: &ChildProps) -> Html {
|
||||
html! { <div>{"Hello, "}{&props.name}{"!"}</div> }
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<Child name="Jane" />
|
||||
<Child name="John" />
|
||||
<Child name="Josh" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
"<div><div>Hello, Jane!</div><div>Hello, John!</div><div>Hello, Josh!</div></div>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,6 +284,28 @@ impl VList {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
|
||||
impl VList {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
// Concurrently render all children.
|
||||
for fragment in futures::future::join_all(self.children.iter().map(|m| async move {
|
||||
let mut w = String::new();
|
||||
|
||||
m.render_to_string(&mut w, parent_scope).await;
|
||||
|
||||
w
|
||||
}))
|
||||
.await
|
||||
{
|
||||
w.push_str(&fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VDiff for VList {
|
||||
fn detach(&mut self, parent: &Element) {
|
||||
for mut child in self.children.drain(..) {
|
||||
|
@ -1267,3 +1289,60 @@ mod layout_tests_keys {
|
|||
diff_layouts(layouts);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::ServerRenderer;
|
||||
|
||||
#[test]
|
||||
async fn test_text_back_to_back() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
let s = "world";
|
||||
|
||||
html! { <div>{"Hello "}{s}{"!"}</div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(s, "<div>Hello world!</div>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_fragment() {
|
||||
#[derive(PartialEq, Properties, Debug)]
|
||||
struct ChildProps {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Child(props: &ChildProps) -> Html {
|
||||
html! { <div>{"Hello, "}{&props.name}{"!"}</div> }
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Child name="Jane" />
|
||||
<Child name="John" />
|
||||
<Child name="Josh" />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
"<div>Hello, Jane!</div><div>Hello, John!</div><div>Hello, Josh!</div>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -296,6 +296,45 @@ impl PartialEq for VNode {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl VNode {
|
||||
// Boxing is needed here, due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html
|
||||
pub(crate) fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
match self {
|
||||
VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope).await,
|
||||
VNode::VText(vtext) => vtext.render_to_string(w).await,
|
||||
VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await,
|
||||
VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).await,
|
||||
// We are pretty safe here as it's not possible to get a web_sys::Node without DOM
|
||||
// support in the first place.
|
||||
//
|
||||
// The only exception would be to use `ServerRenderer` in a browser or wasm32 environment with
|
||||
// jsdom present.
|
||||
VNode::VRef(_) => {
|
||||
panic!("VRef is not possible to be rendered in to a string.")
|
||||
}
|
||||
// Portals are not rendered.
|
||||
VNode::VPortal(_) => {}
|
||||
VNode::VSuspense(vsuspense) => {
|
||||
vsuspense.render_to_string(w, parent_scope).await
|
||||
}
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
use super::*;
|
||||
|
|
|
@ -12,7 +12,7 @@ pub struct VSuspense {
|
|||
fallback: Box<VNode>,
|
||||
|
||||
/// The element to attach to when children is not attached to DOM
|
||||
detached_parent: Element,
|
||||
detached_parent: Option<Element>,
|
||||
|
||||
/// Whether the current status is suspended.
|
||||
suspended: bool,
|
||||
|
@ -25,7 +25,7 @@ impl VSuspense {
|
|||
pub(crate) fn new(
|
||||
children: VNode,
|
||||
fallback: VNode,
|
||||
detached_parent: Element,
|
||||
detached_parent: Option<Element>,
|
||||
suspended: bool,
|
||||
key: Option<Key>,
|
||||
) -> Self {
|
||||
|
@ -51,7 +51,9 @@ impl VDiff for VSuspense {
|
|||
fn detach(&mut self, parent: &Element) {
|
||||
if self.suspended {
|
||||
self.fallback.detach(parent);
|
||||
self.children.detach(&self.detached_parent);
|
||||
if let Some(ref m) = self.detached_parent {
|
||||
self.children.detach(m);
|
||||
}
|
||||
} else {
|
||||
self.children.detach(parent);
|
||||
}
|
||||
|
@ -74,6 +76,8 @@ impl VDiff for VSuspense {
|
|||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
let detached_parent = self.detached_parent.as_ref().expect("no detached parent?");
|
||||
|
||||
let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor {
|
||||
Some(VNode::VSuspense(mut m)) => {
|
||||
// We only preserve the child state if they are the same suspense.
|
||||
|
@ -98,7 +102,7 @@ impl VDiff for VSuspense {
|
|||
(true, true) => {
|
||||
self.children.apply(
|
||||
parent_scope,
|
||||
&self.detached_parent,
|
||||
detached_parent,
|
||||
NodeRef::default(),
|
||||
children_ancestor,
|
||||
);
|
||||
|
@ -115,13 +119,13 @@ impl VDiff for VSuspense {
|
|||
(true, false) => {
|
||||
children_ancestor.as_ref().unwrap().shift(
|
||||
parent,
|
||||
&self.detached_parent,
|
||||
detached_parent,
|
||||
NodeRef::default(),
|
||||
);
|
||||
|
||||
self.children.apply(
|
||||
parent_scope,
|
||||
&self.detached_parent,
|
||||
detached_parent,
|
||||
NodeRef::default(),
|
||||
children_ancestor,
|
||||
);
|
||||
|
@ -135,7 +139,7 @@ impl VDiff for VSuspense {
|
|||
fallback_ancestor.unwrap().detach(parent);
|
||||
|
||||
children_ancestor.as_ref().unwrap().shift(
|
||||
&self.detached_parent,
|
||||
detached_parent,
|
||||
parent,
|
||||
next_sibling.clone(),
|
||||
);
|
||||
|
@ -145,3 +149,110 @@ impl VDiff for VSuspense {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
|
||||
impl VSuspense {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
// always render children on the server side.
|
||||
self.children.render_to_string(w, parent_scope).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::task::{spawn_local, LocalSet};
|
||||
use tokio::test;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::suspense::{Suspension, SuspensionResult};
|
||||
use crate::ServerRenderer;
|
||||
|
||||
#[test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_suspense() {
|
||||
#[derive(PartialEq)]
|
||||
pub struct SleepState {
|
||||
s: Suspension,
|
||||
}
|
||||
|
||||
impl SleepState {
|
||||
fn new() -> Self {
|
||||
let (s, handle) = Suspension::new();
|
||||
|
||||
// we use tokio spawn local here.
|
||||
spawn_local(async move {
|
||||
// we use tokio sleep here.
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
Self { s }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for SleepState {
|
||||
type Action = ();
|
||||
|
||||
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
if sleep_state.s.resumed() {
|
||||
Ok(Rc::new(move || sleep_state.dispatch(())))
|
||||
} else {
|
||||
Err(sleep_state.s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties, Debug)]
|
||||
struct ChildProps {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Child(props: &ChildProps) -> HtmlResult {
|
||||
use_sleep()?;
|
||||
Ok(html! { <div>{"Hello, "}{&props.name}{"!"}</div> })
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
let fallback = html! {"loading..."};
|
||||
|
||||
html! {
|
||||
<Suspense {fallback}>
|
||||
<Child name="Jane" />
|
||||
<Child name="John" />
|
||||
<Child name="Josh" />
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
let local = LocalSet::new();
|
||||
|
||||
let s = local
|
||||
.run_until(async move {
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
renderer.render().await
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
"<div>Hello, Jane!</div><div>Hello, John!</div><div>Hello, Josh!</div>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -384,8 +384,8 @@ impl VTag {
|
|||
/// Returns `checked` property of an
|
||||
/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
|
||||
/// (Not a value of node's attribute).
|
||||
pub fn checked(&mut self) -> bool {
|
||||
match &mut self.inner {
|
||||
pub fn checked(&self) -> bool {
|
||||
match &self.inner {
|
||||
VTagInner::Input(f) => f.checked,
|
||||
_ => false,
|
||||
}
|
||||
|
@ -637,6 +637,63 @@ impl PartialEq for VTag {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::virtual_dom::VText;
|
||||
use std::fmt::Write;
|
||||
|
||||
impl VTag {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
write!(w, "<{}", self.tag()).unwrap();
|
||||
|
||||
let write_attr = |w: &mut String, name: &str, val: Option<&str>| {
|
||||
write!(w, " {}", name).unwrap();
|
||||
|
||||
if let Some(m) = val {
|
||||
write!(w, "=\"{}\"", html_escape::encode_double_quoted_attribute(m)).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
if let VTagInner::Input(_) = self.inner {
|
||||
if let Some(m) = self.value() {
|
||||
write_attr(w, "value", Some(m));
|
||||
}
|
||||
|
||||
if self.checked() {
|
||||
write_attr(w, "checked", None);
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in self.attributes.iter() {
|
||||
write_attr(w, k, Some(v));
|
||||
}
|
||||
|
||||
write!(w, ">").unwrap();
|
||||
|
||||
match self.inner {
|
||||
VTagInner::Input(_) => {}
|
||||
VTagInner::Textarea { .. } => {
|
||||
if let Some(m) = self.value() {
|
||||
VText::new(m.to_owned()).render_to_string(w).await;
|
||||
}
|
||||
|
||||
w.push_str("</textarea>");
|
||||
}
|
||||
VTagInner::Other {
|
||||
ref tag,
|
||||
ref children,
|
||||
..
|
||||
} => {
|
||||
children.render_to_string(w, parent_scope).await;
|
||||
|
||||
write!(w, "</{}>", tag).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -1439,3 +1496,81 @@ mod tests_without_browser {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::ServerRenderer;
|
||||
|
||||
#[test]
|
||||
async fn test_simple_tag() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <div></div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(s, "<div></div>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_simple_tag_with_attr() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <div class="abc"></div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(s, r#"<div class="abc"></div>"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_simple_tag_with_content() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <div>{"Hello!"}</div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(s, r#"<div>Hello!</div>"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_simple_tag_with_nested_tag_and_input() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_textarea() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <textarea value="teststring" /> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
|
||||
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,17 @@ impl VText {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
|
||||
impl VText {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String) {
|
||||
html_escape::encode_text_to_string(&self.text, w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VText {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
|
@ -180,3 +191,21 @@ mod layout_tests {
|
|||
diff_layouts(vec![layout1, layout2, layout3, layout4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
async fn test_simple_str() {
|
||||
let vtext = VText::new("abc");
|
||||
|
||||
let mut s = String::new();
|
||||
|
||||
vtext.render_to_string(&mut s).await;
|
||||
|
||||
assert_eq!("abc", s.as_str());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use yew::prelude::*;
|
|||
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gloo::timers::future::TimeoutFuture;
|
||||
|
@ -399,3 +400,178 @@ async fn suspense_nested_suspense_works() {
|
|||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn effects_not_run_when_suspended() {
|
||||
#[derive(PartialEq)]
|
||||
pub struct SleepState {
|
||||
s: Suspension,
|
||||
}
|
||||
|
||||
impl SleepState {
|
||||
fn new() -> Self {
|
||||
let (s, handle) = Suspension::new();
|
||||
|
||||
spawn_local(async move {
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
Self { s }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for SleepState {
|
||||
type Action = ();
|
||||
|
||||
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
if sleep_state.s.resumed() {
|
||||
Ok(Rc::new(move || sleep_state.dispatch(())))
|
||||
} else {
|
||||
Err(sleep_state.s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone)]
|
||||
struct Props {
|
||||
counter: Rc<RefCell<u64>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Props {
|
||||
fn eq(&self, _rhs: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Content)]
|
||||
fn content(props: &Props) -> HtmlResult {
|
||||
{
|
||||
let counter = props.counter.clone();
|
||||
|
||||
use_effect(move || {
|
||||
let mut counter = counter.borrow_mut();
|
||||
|
||||
*counter += 1;
|
||||
|
||||
|| {}
|
||||
});
|
||||
}
|
||||
|
||||
let resleep = use_sleep()?;
|
||||
|
||||
let value = use_state(|| 0);
|
||||
|
||||
let on_increment = {
|
||||
let value = value.clone();
|
||||
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
value.set(*value + 1);
|
||||
})
|
||||
};
|
||||
|
||||
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
|
||||
|
||||
Ok(html! {
|
||||
<div class="content-area">
|
||||
<div class="actual-result">{*value}</div>
|
||||
<button class="increase" onclick={on_increment}>{"increase"}</button>
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app(props: &Props) -> Html {
|
||||
let fallback = html! {<div>{"wait..."}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content counter={props.counter.clone()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let counter = Rc::new(RefCell::new(0_u64));
|
||||
|
||||
let props = Props {
|
||||
counter: counter.clone(),
|
||||
};
|
||||
|
||||
yew::start_app_with_props_in_element::<App>(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
props,
|
||||
);
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
assert_eq!(*counter.borrow(), 0); // effects not called.
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
assert_eq!(*counter.borrow(), 1); // effects ran 1 time.
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".increase")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".increase")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
assert_eq!(*counter.borrow(), 3); // effects ran 3 times.
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
assert_eq!(*counter.borrow(), 3); // effects ran 3 times.
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
assert_eq!(*counter.borrow(), 4); // effects ran 4 times.
|
||||
}
|
||||
|
|
|
@ -16,8 +16,9 @@ js-sys = "0.3"
|
|||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
weblog = "0.3.0"
|
||||
yew = { path = "../../packages/yew/" }
|
||||
yew = { path = "../../packages/yew/", features = ["ssr"] }
|
||||
yew-router = { path = "../../packages/yew-router/" }
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
|
|
@ -13,7 +13,7 @@ struct Level {
|
|||
|
||||
fn main() {
|
||||
let home = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let pattern = format!("{}/../../website/docs/**/*.mdx", home);
|
||||
let pattern = format!("{}/../../website/docs/**/*.md*", home);
|
||||
let base = format!("{}/../../website", home);
|
||||
let base = Path::new(&base).canonicalize().unwrap();
|
||||
let dir_pattern = format!("{}/../../website/docs/**", home);
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
title: "Server-side Rendering"
|
||||
description: "Render Yew on the server-side."
|
||||
---
|
||||
|
||||
# Server-side Rendering
|
||||
|
||||
By default, Yew components render at the client side. When a viewer
|
||||
visits a website, the server sends a skeleton html file without any actual
|
||||
content and a WebAssembly bundle to the browser.
|
||||
Everything is rendered at the client side by the WebAssembly
|
||||
bundle. This is known as client-side rendering.
|
||||
|
||||
This approach works fine for most websites, with some caveats:
|
||||
|
||||
1. Users will not be able to see anything until the entire WebAssembly
|
||||
bundle is downloaded and initial render has completed.
|
||||
This can result in poor user experience if the user is using a slow network.
|
||||
2. Some search engines do not support dynamically rendered web content and
|
||||
those who do usually rank dynamic websites lower in the search results.
|
||||
|
||||
To solve these problems, we can render our website on the server side.
|
||||
|
||||
## How it Works
|
||||
|
||||
Yew provides a `ServerRenderer` to render pages on the
|
||||
server-side.
|
||||
|
||||
To render Yew components at the server-side, you can create a renderer
|
||||
with `ServerRenderer::<App>::new()` and call `renderer.render().await`
|
||||
to render `<App />` into a `String`.
|
||||
|
||||
```rust
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
|
||||
#[function_component]
|
||||
fn App() -> Html {
|
||||
html! {<div>{"Hello, World!"}</div>}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let renderer = ServerRenderer::<App>::new();
|
||||
|
||||
let rendered = renderer.render().await;
|
||||
|
||||
// Prints: <div>Hello, World!</div>
|
||||
println!("{}", rendered);
|
||||
}
|
||||
```
|
||||
|
||||
## Component Lifecycle
|
||||
|
||||
The recommended way of working with server-side rendering is
|
||||
function components.
|
||||
|
||||
All hooks other than `use_effect` (and `use_effect_with_deps`)
|
||||
will function normally until a component successfully renders into `Html`
|
||||
for the first time.
|
||||
|
||||
:::caution Web APIs are not available!
|
||||
|
||||
Web APIs such as `web_sys` are not available when your component is
|
||||
rendering on the server-side.
|
||||
Your application will panic if you try to use them.
|
||||
You should isolate logics that need Web APIs in `use_effect` or
|
||||
`use_effect_with_deps` as effects are not executed during server side
|
||||
rendering.
|
||||
|
||||
:::
|
||||
|
||||
:::danger Struct Components
|
||||
|
||||
Whilst it's possible to use Struct Components with server-side rendering,
|
||||
there's no clear boundaries between client-side safe logic like the
|
||||
`use_effect` hook for function components and lifecycle events are invoked
|
||||
in a different order than client side.
|
||||
|
||||
In addition, Struct Components will continue to accept messages until all of its
|
||||
children are rendered and `destroy` method is called. Developers need to
|
||||
make sure no messages possibly passed to components would link to logic
|
||||
that makes use of Web APIs.
|
||||
|
||||
When designing an application with server-side rendering support,
|
||||
prefer function components unless you have a good reason not to.
|
||||
|
||||
:::
|
||||
|
||||
## Data Fetching during Server-side Rendering
|
||||
|
||||
Data fetching is one of the difficult point with server side rendering
|
||||
and hydration.
|
||||
|
||||
Traditionally, when a component renders, it is instantly available
|
||||
(outputs a virtual dom to be rendered). This works fine when the
|
||||
component does not want to fetch any data. But what happens if the component
|
||||
wants to fetch some data during rendering?
|
||||
|
||||
In the past, there's no mechanism for Yew to detect whether a component is still
|
||||
fetching data. The data fetching client is responsible to implement
|
||||
a solution to detect what's being requested during initial render and triggers
|
||||
a second render after requests are fulfilled. The server repeats this process until
|
||||
no more pending requests are added during a render before returning a response.
|
||||
|
||||
Not only this wastes CPU resources by repeatedly rendering components,
|
||||
but the data client also needs to provide a way to make the data fetched on
|
||||
the server-side available during hydration process to make sure that the
|
||||
virtual dom returned by initial render is consistent with the
|
||||
server-side rendered DOM tree which can be hard to implement.
|
||||
|
||||
Yew takes a different approach by trying to solve this issue with `<Suspense />`.
|
||||
|
||||
Suspense is a special component that when used on the client-side,
|
||||
provides a way to show a fallback UI while the component is fetching
|
||||
data (suspended) and resumes to normal UI when the data fetching completes.
|
||||
|
||||
When the application is rendered on the server-side, Yew waits until a
|
||||
component is no longer suspended before serializing it into the string
|
||||
buffer.
|
||||
|
||||
During the hydration process, elements within a `<Suspense />` component
|
||||
remains dehydrated until all of its child components are no longer
|
||||
suspended.
|
||||
|
||||
With this approach, developers can build a client-agnostic, SSR ready
|
||||
application with data fetching with very little effort.
|
||||
|
||||
Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr)
|
||||
|
||||
:::caution
|
||||
|
||||
Server-side rendering is experiemental and currently has no hydration support.
|
||||
However, you can still use it to generate static websites.
|
||||
|
||||
:::
|
|
@ -92,7 +92,7 @@ fn load_user() -> Option<User> {
|
|||
todo!() // implementation omitted.
|
||||
}
|
||||
|
||||
fn on_load_user_complete<F: Fn()>(_fn: F) {
|
||||
fn on_load_user_complete<F: FnOnce()>(_fn: F) {
|
||||
todo!() // implementation omitted.
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
|
@ -13,135 +11,137 @@ module.exports = {
|
|||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
// conceptsSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
sidebar: [
|
||||
// But you can create a sidebar manually
|
||||
sidebar: [
|
||||
{
|
||||
type: "category",
|
||||
label: "Getting Started",
|
||||
link: { type: "doc", id: "getting-started/introduction" },
|
||||
items: [
|
||||
"getting-started/build-a-sample-app",
|
||||
"getting-started/examples",
|
||||
"getting-started/editor-setup",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Concepts",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Yew concepts",
|
||||
description: "Learn about the important Yew concepts!",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Getting Started',
|
||||
link: { type: 'doc', id: 'getting-started/introduction' },
|
||||
items: [
|
||||
"getting-started/build-a-sample-app",
|
||||
"getting-started/examples",
|
||||
"getting-started/editor-setup",
|
||||
],
|
||||
type: "category",
|
||||
label: "Components",
|
||||
link: { type: "doc", id: "concepts/components/introduction" },
|
||||
items: [
|
||||
"concepts/components/lifecycle",
|
||||
"concepts/components/scope",
|
||||
"concepts/components/callbacks",
|
||||
"concepts/components/properties",
|
||||
"concepts/components/children",
|
||||
"concepts/components/refs",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Concepts",
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
title: 'Yew concepts',
|
||||
description: 'Learn about the important Yew concepts!',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "Components",
|
||||
link: { type: 'doc', id: 'concepts/components/introduction' },
|
||||
items: [
|
||||
"concepts/components/lifecycle",
|
||||
"concepts/components/scope",
|
||||
"concepts/components/callbacks",
|
||||
"concepts/components/properties",
|
||||
"concepts/components/children",
|
||||
"concepts/components/refs"
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "HTML",
|
||||
link: { type: 'doc', id: 'concepts/html/introduction' },
|
||||
items: [
|
||||
"concepts/html/components",
|
||||
"concepts/html/elements",
|
||||
"concepts/html/events",
|
||||
"concepts/html/classes",
|
||||
"concepts/html/fragments",
|
||||
"concepts/html/lists",
|
||||
"concepts/html/literals-and-expressions",
|
||||
"concepts/html/conditional-rendering"
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Function Components",
|
||||
items: [
|
||||
"concepts/function-components/introduction",
|
||||
"concepts/function-components/attribute",
|
||||
"concepts/function-components/pre-defined-hooks",
|
||||
"concepts/function-components/custom-hooks",
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "wasm-bindgen",
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
title: 'wasm-bindgen',
|
||||
description: 'Learn about wasm-bindgen',
|
||||
slug: '/concepts/wasm-bindgen'
|
||||
},
|
||||
items: [
|
||||
"concepts/wasm-bindgen/introduction",
|
||||
"concepts/wasm-bindgen/web-sys",
|
||||
]
|
||||
},
|
||||
"concepts/agents",
|
||||
"concepts/contexts",
|
||||
"concepts/router",
|
||||
"concepts/suspense",
|
||||
]
|
||||
type: "category",
|
||||
label: "HTML",
|
||||
link: { type: "doc", id: "concepts/html/introduction" },
|
||||
items: [
|
||||
"concepts/html/components",
|
||||
"concepts/html/elements",
|
||||
"concepts/html/events",
|
||||
"concepts/html/classes",
|
||||
"concepts/html/fragments",
|
||||
"concepts/html/lists",
|
||||
"concepts/html/literals-and-expressions",
|
||||
"concepts/html/conditional-rendering",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Advanced topics',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
title: 'Advanced topics',
|
||||
description: 'Learn about the advanced topics and inner workings of Yew!',
|
||||
},
|
||||
items: [
|
||||
"advanced-topics/how-it-works",
|
||||
"advanced-topics/optimizations",
|
||||
"advanced-topics/portals",
|
||||
]
|
||||
type: "category",
|
||||
label: "Function Components",
|
||||
items: [
|
||||
"concepts/function-components/introduction",
|
||||
"concepts/function-components/attribute",
|
||||
"concepts/function-components/pre-defined-hooks",
|
||||
"concepts/function-components/custom-hooks",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'More',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
title: 'Miscellaneous',
|
||||
},
|
||||
items: [
|
||||
"more/debugging",
|
||||
"more/external-libs",
|
||||
"more/css",
|
||||
"more/testing",
|
||||
"more/roadmap",
|
||||
]
|
||||
type: "category",
|
||||
label: "wasm-bindgen",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "wasm-bindgen",
|
||||
description: "Learn about wasm-bindgen",
|
||||
slug: "/concepts/wasm-bindgen",
|
||||
},
|
||||
items: [
|
||||
"concepts/wasm-bindgen/introduction",
|
||||
"concepts/wasm-bindgen/web-sys",
|
||||
],
|
||||
},
|
||||
"concepts/agents",
|
||||
"concepts/contexts",
|
||||
"concepts/router",
|
||||
"concepts/suspense",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Advanced topics",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Advanced topics",
|
||||
description:
|
||||
"Learn about the advanced topics and inner workings of Yew!",
|
||||
},
|
||||
items: [
|
||||
"advanced-topics/how-it-works",
|
||||
"advanced-topics/optimizations",
|
||||
"advanced-topics/portals",
|
||||
"advanced-topics/server-side-rendering",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "More",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Miscellaneous",
|
||||
},
|
||||
items: [
|
||||
"more/debugging",
|
||||
"more/external-libs",
|
||||
"more/css",
|
||||
"more/testing",
|
||||
"more/roadmap",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Migration guides",
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "yew",
|
||||
items: ["migration-guides/yew/from-0_18_0-to-0_19_0"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Migration guides",
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "yew",
|
||||
items: ["migration-guides/yew/from-0_18_0-to-0_19_0"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "yew-agent",
|
||||
items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "yew-router",
|
||||
items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
|
||||
},
|
||||
],
|
||||
type: "category",
|
||||
label: "yew-agent",
|
||||
items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
|
||||
},
|
||||
"tutorial"
|
||||
],
|
||||
{
|
||||
type: "category",
|
||||
label: "yew-router",
|
||||
items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"tutorial",
|
||||
],
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue