Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server-side Rendering (without hydration) #2335

Merged
merged 31 commits into from
Jan 12, 2022
Merged
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
65af3a2
Basic render to html implementation.
futursolo Jan 6, 2022
667796d
Remove HtmlWriter.
futursolo Jan 6, 2022
820d3ac
Escape html content.
futursolo Jan 6, 2022
1b2d823
Add non-suspense tests.
futursolo Jan 6, 2022
5b3f5e3
Add Suspense tests.
futursolo Jan 6, 2022
c7c9d2d
Gated "ssr" feature.
futursolo Jan 6, 2022
2bab219
Add example.
futursolo Jan 6, 2022
90deefc
merge master into ssr
futursolo Jan 6, 2022
482865e
Fix tests.
futursolo Jan 6, 2022
10f4ac4
Fix docs.
futursolo Jan 6, 2022
77b48c0
Fix heading size.
futursolo Jan 6, 2022
476300a
Remove the unused YewRenderer.
futursolo Jan 6, 2022
95e1d39
Remove extra comment.
futursolo Jan 6, 2022
655afd4
unify naming.
futursolo Jan 6, 2022
26bebb5
Update docs.
futursolo Jan 7, 2022
61f44d1
Update docs.
futursolo Jan 7, 2022
826b431
Update docs.
futursolo Jan 7, 2022
ed138b1
Isolate spawn_local.
futursolo Jan 7, 2022
bd26db4
Add doc flags.
futursolo Jan 7, 2022
f003aab
Add ssr feature to docs.
futursolo Jan 7, 2022
e56fcef
Move ServerRenderer into their own file.
futursolo Jan 7, 2022
ebe8c68
Fix docs.
futursolo Jan 7, 2022
10d7295
Update features and docs.
futursolo Jan 8, 2022
8635d63
Fix example.
futursolo Jan 8, 2022
3599240
Adjust comment position.
futursolo Jan 8, 2022
39a3d06
Fix effects being wrongly called when a component is suspended.
futursolo Jan 9, 2022
49566a0
Fix clippy.
futursolo Jan 9, 2022
17fd3b9
Uuid & no double boxing.
futursolo Jan 11, 2022
c67de39
merge master into ssr
futursolo Jan 11, 2022
0f24ae6
Merge branch 'master' into ssr
futursolo Jan 11, 2022
3567689
Merge branch 'master' into fursolo-ssr
ranile Jan 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Basic render to html implementation.
futursolo committed Jan 6, 2022
commit 65af3a24df69b77af563c915c60f7b335792a874
2 changes: 2 additions & 0 deletions packages/yew/Cargo.toml
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
yew-macro = { version = "^0.19.0", path = "../yew-macro" }
thiserror = "1.0"
async-trait = "0.1"
futures = "0.3"

scoped-tls-hkt = "0.1"

62 changes: 43 additions & 19 deletions packages/yew/src/html/component/lifecycle.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ use crate::suspense::{Suspense, Suspension};
use crate::virtual_dom::{VDiff, VNode};
use crate::Callback;
use crate::{Context, NodeRef};
use futures::channel::oneshot;
use std::rc::Rc;
use web_sys::Element;

@@ -15,26 +16,32 @@ 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>,

html_sender: Option<oneshot::Sender<VNode>>,

// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: u64,
}

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>,
html_sender: Option<oneshot::Sender<VNode>>,
) -> Self {
#[cfg(debug_assertions)]
let vcomp_id = {
@@ -55,19 +62,22 @@ impl<COMP: BaseComponent> ComponentState<COMP> {
suspension: None,
has_rendered: false,

html_sender,

#[cfg(debug_assertions)]
vcomp_id,
}
}
}

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>,
pub(crate) html_sender: Option<oneshot::Sender<VNode>>,
}

impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
@@ -84,6 +94,7 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
self.node_ref,
self.scope.clone(),
self.props,
self.html_sender,
));
}
}
@@ -129,11 +140,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 +186,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,7 +210,9 @@ 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 {
let comp_scope = AnyScope::from(state.context.scope.clone());
@@ -205,13 +223,17 @@ impl<COMP: BaseComponent> Runnable for RenderRunner<COMP> {
suspense.resume(m.clone());
}

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 if let Some(tx) = state.html_sender.take() {
tx.send(root).unwrap();
}
}

Err(RenderError::Suspended(m)) => {
@@ -277,9 +299,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.parent.is_some() {
let first_render = !state.has_rendered;
state.component.rendered(&state.context, first_render);
state.has_rendered = true;
}
}
}
}
45 changes: 44 additions & 1 deletion packages/yew/src/html/component/scope.rs
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ use crate::context::{ContextHandle, ContextProvider};
use crate::html::NodeRef;
use crate::scheduler::{self, Shared};
use crate::virtual_dom::{insert_node, VNode};
use futures::channel::oneshot;
use gloo_utils::document;
use std::any::{Any, TypeId};
use std::cell::{Ref, RefCell};
@@ -234,12 +235,13 @@ impl<COMP: BaseComponent> Scope<COMP> {

scheduler::push_component_create(
CreateRunner {
parent,
parent: Some(parent),
next_sibling,
placeholder,
node_ref,
props,
scope: self.clone(),
html_sender: None,
},
RenderRunner {
state: self.state.clone(),
@@ -414,6 +416,47 @@ impl<COMP: BaseComponent> Scope<COMP> {
}
}

mod feat_ssr {
use super::*;
use crate::html_writer::HtmlWriter;

impl<COMP: BaseComponent> Scope<COMP> {
/// Renders Into a [`HtmlWrite`].
pub(crate) async fn render_to_html(&self, w: &HtmlWriter, 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_html(w, &self_any_scope).await;

scheduler::push_component_destroy(DestroyRunner {
state: self.state.clone(),
});
scheduler::start();
}
}
}

/// 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> {
22 changes: 22 additions & 0 deletions packages/yew/src/html_writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! module to provide an html writer that can be written without mutable borrowing.

mod feat_ssr {
use std::cell::RefCell;

#[derive(Debug, Default)]
pub(crate) struct HtmlWriter {
inner: RefCell<String>,
}

impl HtmlWriter {
pub fn push_str(&self, s: &str) {
self.inner.borrow_mut().push_str(s);
}

pub fn into_inner(self) -> String {
self.inner.into_inner()
}
}
}

pub use feat_ssr::*;
120 changes: 120 additions & 0 deletions packages/yew/src/lib.rs
Original file line number Diff line number Diff line change
@@ -260,6 +260,7 @@ pub mod callback;
pub mod context;
pub mod functional;
pub mod html;
mod html_writer;
pub mod scheduler;
mod sealed;
pub mod suspense;
@@ -304,6 +305,125 @@ fn set_default_panic_hook() {
}
}

// /// A Yew Renderer.
// #[derive(Debug)]
// pub struct YewRenderer<COMP>
// where
// COMP: BaseComponent,
// {
// root: Element,
// props: COMP::Properties,
// }

// impl<COMP> YewRenderer<COMP>
// where
// COMP: BaseComponent,
// {
// /// Creates a [`YewRenderer`] with a custom root and properties.
// pub fn with_root_and_props(root: Element, props: COMP::Properties) -> Self {
// Self { root, props }
// }

// /// Creates a [`YewRenderer`] with document body as root and custom properties.
// pub fn with_props(props: COMP::Properties) -> Self {
// Self::with_root_and_props(
// gloo_utils::document()
// .body()
// .expect("no body node found")
// .into(),
// props,
// )
// }

// /// Renders a Yew application.
// pub fn render(self) -> AppHandle<COMP> {
// set_default_panic_hook();

// AppHandle::<COMP>::mount_with_props(self.root, Rc::new(self.props))
// }

// /// Hydrates a Yew application.
// pub fn hydrate(self) -> AppHandle<COMP> {
// set_default_panic_hook();
// todo!()
// }
// }

// impl<COMP> YewRenderer<COMP>
// where
// COMP: BaseComponent,
// COMP::Properties: Default,
// {
// /// Creates a [`YewRenderer`] with a custom root.
// pub fn with_root(root: Element) -> Self {
// Self::with_root_and_props(root, COMP::Properties::default())
// }

// /// Creates a [`YewRenderer`] with document body as root.
// pub fn body() -> Self {
// Self::with_props(COMP::Properties::default())
// }
// }

mod feat_ssr {
use super::*;

use crate::html::Scope;
use crate::html_writer::HtmlWriter;

/// A Yew Server-side Renderer.
#[derive(Debug)]
pub struct YewServerRenderer<COMP>
where
COMP: BaseComponent,
{
props: COMP::Properties,
}

impl<COMP> Default for YewServerRenderer<COMP>
where
COMP: BaseComponent,
COMP::Properties: Default,
{
fn default() -> Self {
Self::with_props(COMP::Properties::default())
}
}

impl<COMP> YewServerRenderer<COMP>
where
COMP: BaseComponent,
COMP::Properties: Default,
{
/// Creates a [`YewServerRenderer`] with default properties.
pub fn new() -> Self {
Self::default()
}
}

impl<COMP> YewServerRenderer<COMP>
where
COMP: BaseComponent,
{
/// Creates a [`YewServerRenderer`] with custom properties.
pub fn with_props(props: COMP::Properties) -> Self {
Self { props }
}

/// Renders Yew Application into a string.
pub async fn render_to_string(self) -> String {
let s = HtmlWriter::default();

let scope = Scope::<COMP>::new(None);
scope.render_to_html(&s, self.props.into()).await;

s.into_inner()
}
}
}

pub use feat_ssr::*;

/// The main entry point of a Yew application.
/// If you would like to pass props, use the `start_app_with_props_in_element` method.
pub fn start_app_in_element<COMP>(element: Element) -> AppHandle<COMP>
Loading