feat: move to a channel-based implementation for meta

This commit is contained in:
Greg Johnston 2024-07-06 08:16:48 -04:00
parent 4a0f173bb5
commit 208ab97867
6 changed files with 77 additions and 55 deletions

View File

@ -741,7 +741,7 @@ where
async move { async move {
let res_options = ResponseOptions::default(); let res_options = ResponseOptions::default();
let meta_context = ServerMetaContext::new(); let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = { let additional_context = {
let meta_context = meta_context.clone(); let meta_context = meta_context.clone();
@ -755,7 +755,7 @@ where
let res = ActixResponse::from_app( let res = ActixResponse::from_app(
app_fn, app_fn,
meta_context, meta_output,
additional_context, additional_context,
res_options, res_options,
stream_builder, stream_builder,
@ -945,12 +945,13 @@ where
let _ = any_spawner::Executor::init_tokio(); let _ = any_spawner::Executor::init_tokio();
let owner = Owner::new_root(None); let owner = Owner::new_root(None);
let (mock_meta, _) = ServerMetaContext::new();
let routes = owner let routes = owner
.with(|| { .with(|| {
// stub out a path for now // stub out a path for now
provide_context(RequestUrl::new("")); provide_context(RequestUrl::new(""));
provide_context(ResponseOptions::default()); provide_context(ResponseOptions::default());
provide_context(ServerMetaContext::new()); provide_context(mock_meta);
additional_context(); additional_context();
RouteList::generate(&app_fn) RouteList::generate(&app_fn)
}) })

View File

@ -763,7 +763,7 @@ where
Box::pin(async move { Box::pin(async move {
let add_context = additional_context.clone(); let add_context = additional_context.clone();
let res_options = ResponseOptions::default(); let res_options = ResponseOptions::default();
let meta_context = ServerMetaContext::new(); let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = { let additional_context = {
let meta_context = meta_context.clone(); let meta_context = meta_context.clone();
@ -787,7 +787,7 @@ where
let res = AxumResponse::from_app( let res = AxumResponse::from_app(
app_fn, app_fn,
meta_context, meta_output,
additional_context, additional_context,
res_options, res_options,
stream_builder, stream_builder,
@ -1155,12 +1155,8 @@ where
provide_context(RequestUrl::new("")); provide_context(RequestUrl::new(""));
let (mock_parts, _) = let (mock_parts, _) =
http::Request::new(Body::from("")).into_parts(); http::Request::new(Body::from("")).into_parts();
provide_contexts( let (mock_meta, _) = ServerMetaContext::new();
"", provide_contexts("", &mock_meta, mock_parts, Default::default());
&Default::default(),
mock_parts,
Default::default(),
);
additional_context(); additional_context();
RouteList::generate(&app_fn) RouteList::generate(&app_fn)
}) })

View File

@ -5,7 +5,7 @@ use leptos::{
reactive_graph::owner::{Owner, Sandboxed}, reactive_graph::owner::{Owner, Sandboxed},
IntoView, IntoView,
}; };
use leptos_meta::ServerMetaContext; use leptos_meta::ServerMetaContextOutput;
use std::{future::Future, pin::Pin, sync::Arc}; use std::{future::Future, pin::Pin, sync::Arc};
pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>; pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
@ -24,7 +24,7 @@ pub trait ExtendResponse: Sized {
fn from_app<IV>( fn from_app<IV>(
app_fn: impl FnOnce() -> IV + Send + 'static, app_fn: impl FnOnce() -> IV + Send + 'static,
meta_context: ServerMetaContext, meta_context: ServerMetaContextOutput,
additional_context: impl FnOnce() + Send + 'static, additional_context: impl FnOnce() + Send + 'static,
res_options: Self::ResponseOptions, res_options: Self::ResponseOptions,
stream_builder: fn( stream_builder: fn(

View File

@ -67,11 +67,13 @@ pub fn Body(
attributes.push(value.into_any_attr()); attributes.push(value.into_any_attr());
} }
if let Some(meta) = use_context::<ServerMetaContext>() { if let Some(meta) = use_context::<ServerMetaContext>() {
let mut meta = meta.inner.write().or_poisoned();
// if we are server rendering, we will not actually use these values via RenderHtml // if we are server rendering, we will not actually use these values via RenderHtml
// instead, they'll be handled separately by the server integration // instead, they'll be handled separately by the server integration
// so it's safe to take them out of the props here // so it's safe to take them out of the props here
meta.body = mem::take(&mut attributes); for attr in attributes.drain(0..) {
// fails only if receiver is already dropped
_ = meta.body.send(attr);
}
} }
BodyView { attributes } BodyView { attributes }

View File

@ -78,11 +78,13 @@ pub fn Html(
})), })),
); );
if let Some(meta) = use_context::<ServerMetaContext>() { if let Some(meta) = use_context::<ServerMetaContext>() {
let mut meta = meta.inner.write().or_poisoned();
// if we are server rendering, we will not actually use these values via RenderHtml // if we are server rendering, we will not actually use these values via RenderHtml
// instead, they'll be handled separately by the server integration // instead, they'll be handled separately by the server integration
// so it's safe to take them out of the props here // so it's safe to take them out of the props here
meta.html = mem::take(&mut attributes); for attr in attributes.drain(0..) {
// fails only if receiver is already dropped
_ = meta.body.send(attr);
}
} }
HtmlView { attributes } HtmlView { attributes }

View File

@ -73,7 +73,10 @@ use or_poisoned::OrPoisoned;
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
use std::{ use std::{
fmt::Debug, fmt::Debug,
sync::{Arc, RwLock}, sync::{
mpsc::{channel, Receiver, Sender},
Arc, RwLock,
},
}; };
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::HtmlHeadElement; use web_sys::HtmlHeadElement;
@ -154,17 +157,61 @@ impl Default for MetaContext {
} }
} }
/// Contains the state of meta tags for server rendering. /// Allows you to add `<head>` content from components located in the `<body>` of the application,
/// which can be accessed during server rendering via [`ServerMetaContextOutput`].
/// ///
/// This should be provided as context during server rendering. /// This should be provided as context during server rendering.
#[derive(Clone, Default)] ///
/// No content added after the first chunk of the stream has been sent will be included in the
/// initial `<head>`. Data that needs to be included in the `<head>` during SSR should be
/// synchronous or loaded as a blocking resource.
#[derive(Clone, Debug)]
pub struct ServerMetaContext { pub struct ServerMetaContext {
inner: Arc<RwLock<ServerMetaContextInner>>,
/// Metadata associated with the `<title>` element. /// Metadata associated with the `<title>` element.
pub(crate) title: TitleContext, pub(crate) title: TitleContext,
/// Attributes for the `<html>` element.
pub(crate) html: Sender<AnyAttribute<Dom>>,
/// Attributes for the `<body>` element.
pub(crate) body: Sender<AnyAttribute<Dom>>,
/// Arbitrary elements to be added to the `<head>` as HTML.
pub(crate) elements: Sender<String>,
}
/// Allows you to access `<head>` content that was inserted via [`ServerMetaContext`].
#[must_use = "If you do not use the output, adding meta tags will have no \
effect."]
#[derive(Debug)]
pub struct ServerMetaContextOutput {
pub(crate) title: TitleContext,
html: Receiver<AnyAttribute<Dom>>,
body: Receiver<AnyAttribute<Dom>>,
elements: Receiver<String>,
} }
impl ServerMetaContext { impl ServerMetaContext {
/// Creates an empty [`ServerMetaContext`].
pub fn new() -> (ServerMetaContext, ServerMetaContextOutput) {
let title = TitleContext::default();
let (html_tx, html_rx) = channel();
let (body_tx, body_rx) = channel();
let (elements_tx, elements_rx) = channel();
let tx = ServerMetaContext {
title: title.clone(),
html: html_tx,
body: body_tx,
elements: elements_tx,
};
let rx = ServerMetaContextOutput {
title,
html: html_rx,
body: body_rx,
elements: elements_rx,
};
(tx, rx)
}
}
impl ServerMetaContextOutput {
/// Consumes the metadata, injecting it into the the first chunk of an HTML stream in the /// Consumes the metadata, injecting it into the the first chunk of an HTML stream in the
/// appropriate place. /// appropriate place.
/// ///
@ -174,17 +221,19 @@ impl ServerMetaContext {
self, self,
mut stream: impl Stream<Item = String> + Send + Unpin, mut stream: impl Stream<Item = String> + Send + Unpin,
) -> impl Stream<Item = String> + Send { ) -> impl Stream<Item = String> + Send {
// wait for the first chunk of the stream, to ensure our components hve run
let mut first_chunk = stream.next().await.unwrap_or_default(); let mut first_chunk = stream.next().await.unwrap_or_default();
let meta_buf = // create <title> tag
std::mem::take(&mut self.inner.write().or_poisoned().head_html);
let title = self.title.as_string(); let title = self.title.as_string();
let title_len = title let title_len = title
.as_ref() .as_ref()
.map(|n| "<title>".len() + n.len() + "</title>".len()) .map(|n| "<title>".len() + n.len() + "</title>".len())
.unwrap_or(0); .unwrap_or(0);
// collect all registered meta tags
let meta_buf = self.elements.into_iter().collect::<String>();
let modified_chunk = if title_len == 0 && meta_buf.is_empty() { let modified_chunk = if title_len == 0 && meta_buf.is_empty() {
first_chunk first_chunk
} else { } else {
@ -218,35 +267,6 @@ impl ServerMetaContext {
} }
} }
#[derive(Default, Debug)]
struct ServerMetaContextInner {
/*/// Metadata associated with the `<html>` element
pub html: HtmlContext,
/// Metadata associated with the `<title>` element.
pub title: TitleContext,*/
/// Metadata associated with the `<html>` element
pub(crate) html: Vec<AnyAttribute<Dom>>,
/// Metadata associated with the `<body>` element
pub(crate) body: Vec<AnyAttribute<Dom>>,
/// HTML for arbitrary tags that will be included in the `<head>` element
pub(crate) head_html: String,
}
impl Debug for ServerMetaContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerMetaContext")
.field("inner", &self.inner)
.finish_non_exhaustive()
}
}
impl ServerMetaContext {
/// Creates an empty [`ServerMetaContext`].
pub fn new() -> Self {
Default::default()
}
}
/// Provides a [`MetaContext`], if there is not already one provided. This ensures that you can provide it /// Provides a [`MetaContext`], if there is not already one provided. This ensures that you can provide it
/// at the highest possible level, without overwriting a [`MetaContext`] that has already been provided /// at the highest possible level, without overwriting a [`MetaContext`] that has already been provided
/// (for example, by a server-rendering integration.) /// (for example, by a server-rendering integration.)
@ -293,12 +313,13 @@ where
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
if let Some(cx) = use_context::<ServerMetaContext>() { if let Some(cx) = use_context::<ServerMetaContext>() {
let mut inner = cx.inner.write().or_poisoned(); let mut buf = String::new();
el.take().unwrap().to_html_with_buf( el.take().unwrap().to_html_with_buf(
&mut inner.head_html, &mut buf,
&mut Position::NextChild, &mut Position::NextChild,
false, false,
); );
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else { } else {
tracing::warn!( tracing::warn!(
"tried to use a leptos_meta component without `ServerMetaContext` \ "tried to use a leptos_meta component without `ServerMetaContext` \