Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/
server_renderer.rs

1use std::fmt;
2
3use futures::pin_mut;
4use futures::stream::{Stream, StreamExt};
5use tracing::Instrument;
6
7use crate::html::{BaseComponent, Scope};
8use crate::platform::fmt::BufStream;
9use crate::platform::{LocalHandle, Runtime};
10
11#[cfg(feature = "ssr")]
12pub(crate) mod feat_ssr {
13    /// Passed top-down as context for `render_into_stream` functions to know the current innermost
14    /// `VTag` kind to apply appropriate text escaping.
15    /// Right now this is used to make `VText` nodes aware of their environment and correctly
16    /// escape their contents when rendering them during SSR.
17    #[derive(Default, Clone, Copy)]
18    pub(crate) enum VTagKind {
19        /// <style> tag
20        Style,
21        /// <script> tag
22        Script,
23        #[default]
24        /// any other tag
25        Other,
26    }
27
28    impl<T: AsRef<str>> From<T> for VTagKind {
29        fn from(value: T) -> Self {
30            let value = value.as_ref();
31            if value.eq_ignore_ascii_case("style") {
32                Self::Style
33            } else if value.eq_ignore_ascii_case("script") {
34                Self::Script
35            } else {
36                Self::Other
37            }
38        }
39    }
40}
41
42/// A Yew Server-side Renderer that renders on the current thread.
43///
44/// # Note
45///
46/// This renderer does not spawn its own runtime and can only be used when:
47///
48/// - `wasm-bindgen-futures` is selected as the backend of Yew runtime.
49/// - running within a [`Runtime`](crate::platform::Runtime).
50/// - running within a tokio [`LocalSet`](struct@tokio::task::LocalSet).
51#[cfg(feature = "ssr")]
52#[derive(Debug)]
53pub struct LocalServerRenderer<COMP>
54where
55    COMP: BaseComponent,
56{
57    props: COMP::Properties,
58    hydratable: bool,
59}
60
61impl<COMP> Default for LocalServerRenderer<COMP>
62where
63    COMP: BaseComponent<Properties: Default>,
64{
65    fn default() -> Self {
66        Self::with_props(COMP::Properties::default())
67    }
68}
69
70impl<COMP> LocalServerRenderer<COMP>
71where
72    COMP: BaseComponent<Properties: Default>,
73{
74    /// Creates a [LocalServerRenderer] with default properties.
75    pub fn new() -> Self {
76        Self::default()
77    }
78}
79
80impl<COMP> LocalServerRenderer<COMP>
81where
82    COMP: BaseComponent,
83{
84    /// Creates a [LocalServerRenderer] with custom properties.
85    pub fn with_props(props: COMP::Properties) -> Self {
86        Self {
87            props,
88            hydratable: true,
89        }
90    }
91
92    /// Sets whether an the rendered result is hydratable.
93    ///
94    /// Defaults to `true`.
95    ///
96    /// When this is sets to `true`, the rendered artifact will include additional information
97    /// to assist with the hydration process.
98    pub fn hydratable(mut self, val: bool) -> Self {
99        self.hydratable = val;
100
101        self
102    }
103
104    /// Renders Yew Application.
105    pub async fn render(self) -> String {
106        let s = self.render_stream();
107        futures::pin_mut!(s);
108
109        s.collect().await
110    }
111
112    /// Renders Yew Application to a String.
113    pub async fn render_to_string(self, w: &mut String) {
114        let s = self.render_stream();
115        futures::pin_mut!(s);
116
117        while let Some(m) = s.next().await {
118            w.push_str(&m);
119        }
120    }
121
122    fn render_stream_inner(self) -> impl Stream<Item = String> {
123        let scope = Scope::<COMP>::new(None);
124
125        let outer_span = tracing::Span::current();
126        BufStream::new(move |mut w| async move {
127            let render_span = tracing::debug_span!("render_stream_item");
128            render_span.follows_from(outer_span);
129            scope
130                .render_into_stream(
131                    &mut w,
132                    self.props.into(),
133                    self.hydratable,
134                    Default::default(),
135                )
136                .instrument(render_span)
137                .await;
138        })
139    }
140
141    // The duplicate implementation below is to selectively suppress clippy lints.
142    // These implementations should be merged once https://github.com/tokio-rs/tracing/issues/2503 is resolved.
143
144    /// Renders Yew Application into a string Stream
145    #[tracing::instrument(
146        level = tracing::Level::DEBUG,
147        name = "render_stream",
148        skip(self),
149        fields(hydratable = self.hydratable),
150    )]
151    #[inline(always)]
152    pub fn render_stream(self) -> impl Stream<Item = String> {
153        self.render_stream_inner()
154    }
155}
156
157/// A Yew Server-side Renderer.
158///
159/// This renderer spawns the rendering task to a Yew [`Runtime`]. and receives result when
160/// the rendering process has finished.
161///
162/// See [`yew::platform`] for more information.
163#[cfg(feature = "ssr")]
164pub struct ServerRenderer<COMP>
165where
166    COMP: BaseComponent,
167{
168    create_props: Box<dyn Send + FnOnce() -> COMP::Properties>,
169    hydratable: bool,
170    rt: Option<Runtime>,
171}
172
173impl<COMP> fmt::Debug for ServerRenderer<COMP>
174where
175    COMP: BaseComponent,
176{
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.debug_struct("ServerRenderer<_>").finish_non_exhaustive()
179    }
180}
181
182impl<COMP> Default for ServerRenderer<COMP>
183where
184    COMP: BaseComponent<Properties: Default>,
185{
186    fn default() -> Self {
187        Self::with_props(Default::default)
188    }
189}
190
191impl<COMP> ServerRenderer<COMP>
192where
193    COMP: BaseComponent<Properties: Default>,
194{
195    /// Creates a [ServerRenderer] with default properties.
196    pub fn new() -> Self {
197        Self::default()
198    }
199}
200
201impl<COMP> ServerRenderer<COMP>
202where
203    COMP: BaseComponent,
204{
205    /// Creates a [ServerRenderer] with custom properties.
206    ///
207    /// # Note
208    ///
209    /// The properties does not have to implement `Send`.
210    /// However, the function to create properties needs to be `Send`.
211    pub fn with_props<F>(create_props: F) -> Self
212    where
213        F: 'static + Send + FnOnce() -> COMP::Properties,
214    {
215        Self {
216            create_props: Box::new(create_props),
217            hydratable: true,
218            rt: None,
219        }
220    }
221
222    /// Sets the runtime the ServerRenderer will run the rendering task with.
223    pub fn with_runtime(mut self, rt: Runtime) -> Self {
224        self.rt = Some(rt);
225
226        self
227    }
228
229    /// Sets whether an the rendered result is hydratable.
230    ///
231    /// Defaults to `true`.
232    ///
233    /// When this is sets to `true`, the rendered artifact will include additional information
234    /// to assist with the hydration process.
235    pub fn hydratable(mut self, val: bool) -> Self {
236        self.hydratable = val;
237
238        self
239    }
240
241    /// Renders Yew Application.
242    pub async fn render(self) -> String {
243        let Self {
244            create_props,
245            hydratable,
246            rt,
247        } = self;
248
249        let (tx, rx) = futures::channel::oneshot::channel();
250        let create_task = move || async move {
251            let props = create_props();
252            let s = LocalServerRenderer::<COMP>::with_props(props)
253                .hydratable(hydratable)
254                .render()
255                .await;
256
257            let _ = tx.send(s);
258        };
259
260        Self::spawn_rendering_task(rt, create_task);
261
262        rx.await.expect("failed to render application")
263    }
264
265    /// Renders Yew Application to a String.
266    pub async fn render_to_string(self, w: &mut String) {
267        let mut s = self.render_stream();
268
269        while let Some(m) = s.next().await {
270            w.push_str(&m);
271        }
272    }
273
274    #[inline]
275    fn spawn_rendering_task<F, Fut>(rt: Option<Runtime>, create_task: F)
276    where
277        F: 'static + Send + FnOnce() -> Fut,
278        Fut: Future<Output = ()> + 'static,
279    {
280        match rt {
281            // If a runtime is specified, spawn to the specified runtime.
282            Some(m) => m.spawn_pinned(create_task),
283            None => match LocalHandle::try_current() {
284                // If within a Yew Runtime, spawn to the current runtime.
285                Some(m) => m.spawn_local(create_task()),
286                // Outside of Yew Runtime, spawn to the default runtime.
287                None => Runtime::default().spawn_pinned(create_task),
288            },
289        }
290    }
291
292    /// Renders Yew Application into a string Stream.
293    pub fn render_stream(self) -> impl Send + Stream<Item = String> {
294        let Self {
295            create_props,
296            hydratable,
297            rt,
298        } = self;
299
300        let (tx, rx) = futures::channel::mpsc::unbounded();
301        let create_task = move || async move {
302            let props = create_props();
303            let s = LocalServerRenderer::<COMP>::with_props(props)
304                .hydratable(hydratable)
305                .render_stream();
306            pin_mut!(s);
307
308            while let Some(m) = s.next().await {
309                let _ = tx.unbounded_send(m);
310            }
311        };
312
313        Self::spawn_rendering_task(rt, create_task);
314
315        rx
316    }
317}