Skip to content

Commit

Permalink
Server-side Rendering (without hydration) (#2335)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
futursolo and hamza1311 committed Jan 12, 2022
1 parent fff1ffa commit d8c2550
Show file tree
Hide file tree
Showing 30 changed files with 1,450 additions and 261 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/publish-examples.yml
Expand Up @@ -61,6 +61,11 @@ jobs:
continue
fi
# ssr does not need trunk
if [[ "$example" == "simple_ssr" ]]; then
continue
fi
echo "building: $example"
(
cd "$path"
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -27,6 +27,7 @@ members = [
"examples/password_strength",
"examples/portals",
"examples/router",
"examples/simple_ssr",
"examples/timer",
"examples/todomvc",
"examples/two_apps",
Expand Down
7 changes: 6 additions & 1 deletion Makefile.toml
Expand Up @@ -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"
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion examples/futures/Cargo.toml
Expand Up @@ -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]
Expand Down
14 changes: 14 additions & 0 deletions examples/simple_ssr/Cargo.toml
@@ -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"] }
6 changes: 6 additions & 0 deletions examples/simple_ssr/README.md
@@ -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.
129 changes: 129 additions & 0 deletions examples/simple_ssr/src/main.rs
@@ -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;
}
2 changes: 1 addition & 1 deletion examples/suspense/Cargo.toml
Expand Up @@ -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"
Expand Down
21 changes: 19 additions & 2 deletions packages/yew/Cargo.toml
Expand Up @@ -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 = [
Expand Down Expand Up @@ -61,17 +63,32 @@ 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"

[features]
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"]
4 changes: 4 additions & 0 deletions packages/yew/Makefile.toml
Expand Up @@ -41,3 +41,7 @@ args = [
"wasm_bench",
"bench",
]

[tasks.ssr-test]
command = "cargo"
args = ["test", "ssr_tests", "--features", "ssr"]
58 changes: 38 additions & 20 deletions packages/yew/src/functional/hooks/use_effect.rs
@@ -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>>,
}

Expand Down Expand Up @@ -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
});
},
Expand All @@ -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.
Expand All @@ -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
});
Expand Down

1 comment on commit d8c2550

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yew master branch benchmarks (Lower is better)

Benchmark suite Current: d8c2550 Previous: fff1ffa Ratio
yew-struct-keyed 01_run1k 246.7085 172.567 1.43
yew-struct-keyed 02_replace1k 265.42049999999995 192.423 1.38
yew-struct-keyed 03_update10th1k_x16 468.3485 421.674 1.11
yew-struct-keyed 04_select1k 79.83699999999999 86.3475 0.92
yew-struct-keyed 05_swap1k 99.389 99.2515 1.00
yew-struct-keyed 06_remove-one-1k 32.952 30.7395 1.07
yew-struct-keyed 07_create10k 2826.7855 2114.381 1.34
yew-struct-keyed 08_create1k-after1k_x2 556.9110000000001 407.3905 1.37
yew-struct-keyed 09_clear1k_x8 261.108 204.85 1.27
yew-struct-keyed 21_ready-memory 0.9196929931640624 0.9196929931640624 1
yew-struct-keyed 22_run-memory 1.4166336059570312 1.4500503540039062 0.98
yew-struct-keyed 23_update5-memory 1.4559898376464844 1.47119140625 0.99
yew-struct-keyed 24_run5-memory 1.476593017578125 1.4699134826660156 1.00
yew-struct-keyed 25_run-clear-memory 1.0852279663085938 1.0886917114257812 1.00
yew-struct-keyed 31_startup-ci 1730.022 1858.56725 0.93
yew-struct-keyed 32_startup-bt 39.25799999999998 29.76999999999999 1.32
yew-struct-keyed 34_startup-totalbytes 363.7998046875 363.7998046875 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.