feat: move to a channel-based implementation for meta
This commit is contained in:
parent
4a0f173bb5
commit
208ab97867
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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` \
|
||||||
|
|
Loading…
Reference in New Issue