mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: move to a channel-based implementation for meta
This commit is contained in:
parent
e2d41f91fa
commit
f70702c6c4
6 changed files with 77 additions and 55 deletions
|
@ -741,7 +741,7 @@ where
|
|||
|
||||
async move {
|
||||
let res_options = ResponseOptions::default();
|
||||
let meta_context = ServerMetaContext::new();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
|
||||
let additional_context = {
|
||||
let meta_context = meta_context.clone();
|
||||
|
@ -755,7 +755,7 @@ where
|
|||
|
||||
let res = ActixResponse::from_app(
|
||||
app_fn,
|
||||
meta_context,
|
||||
meta_output,
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
|
@ -945,12 +945,13 @@ where
|
|||
let _ = any_spawner::Executor::init_tokio();
|
||||
|
||||
let owner = Owner::new_root(None);
|
||||
let (mock_meta, _) = ServerMetaContext::new();
|
||||
let routes = owner
|
||||
.with(|| {
|
||||
// stub out a path for now
|
||||
provide_context(RequestUrl::new(""));
|
||||
provide_context(ResponseOptions::default());
|
||||
provide_context(ServerMetaContext::new());
|
||||
provide_context(mock_meta);
|
||||
additional_context();
|
||||
RouteList::generate(&app_fn)
|
||||
})
|
||||
|
|
|
@ -763,7 +763,7 @@ where
|
|||
Box::pin(async move {
|
||||
let add_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
let meta_context = ServerMetaContext::new();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
|
||||
let additional_context = {
|
||||
let meta_context = meta_context.clone();
|
||||
|
@ -787,7 +787,7 @@ where
|
|||
|
||||
let res = AxumResponse::from_app(
|
||||
app_fn,
|
||||
meta_context,
|
||||
meta_output,
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
|
@ -1155,12 +1155,8 @@ where
|
|||
provide_context(RequestUrl::new(""));
|
||||
let (mock_parts, _) =
|
||||
http::Request::new(Body::from("")).into_parts();
|
||||
provide_contexts(
|
||||
"",
|
||||
&Default::default(),
|
||||
mock_parts,
|
||||
Default::default(),
|
||||
);
|
||||
let (mock_meta, _) = ServerMetaContext::new();
|
||||
provide_contexts("", &mock_meta, mock_parts, Default::default());
|
||||
additional_context();
|
||||
RouteList::generate(&app_fn)
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@ use leptos::{
|
|||
reactive_graph::owner::{Owner, Sandboxed},
|
||||
IntoView,
|
||||
};
|
||||
use leptos_meta::ServerMetaContext;
|
||||
use leptos_meta::ServerMetaContextOutput;
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
|
||||
|
@ -24,7 +24,7 @@ pub trait ExtendResponse: Sized {
|
|||
|
||||
fn from_app<IV>(
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
meta_context: ServerMetaContext,
|
||||
meta_context: ServerMetaContextOutput,
|
||||
additional_context: impl FnOnce() + Send + 'static,
|
||||
res_options: Self::ResponseOptions,
|
||||
stream_builder: fn(
|
||||
|
|
|
@ -67,11 +67,13 @@ pub fn Body(
|
|||
attributes.push(value.into_any_attr());
|
||||
}
|
||||
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
|
||||
// instead, they'll be handled separately by the server integration
|
||||
// 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 }
|
||||
|
|
|
@ -78,11 +78,13 @@ pub fn Html(
|
|||
})),
|
||||
);
|
||||
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
|
||||
// instead, they'll be handled separately by the server integration
|
||||
// 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 }
|
||||
|
|
|
@ -73,7 +73,10 @@ use or_poisoned::OrPoisoned;
|
|||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::{Arc, RwLock},
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, RwLock,
|
||||
},
|
||||
};
|
||||
use wasm_bindgen::JsCast;
|
||||
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.
|
||||
#[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 {
|
||||
inner: Arc<RwLock<ServerMetaContextInner>>,
|
||||
/// Metadata associated with the `<title>` element.
|
||||
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 {
|
||||
/// 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
|
||||
/// appropriate place.
|
||||
///
|
||||
|
@ -174,17 +221,19 @@ impl ServerMetaContext {
|
|||
self,
|
||||
mut stream: impl Stream<Item = String> + Send + Unpin,
|
||||
) -> 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 meta_buf =
|
||||
std::mem::take(&mut self.inner.write().or_poisoned().head_html);
|
||||
|
||||
// create <title> tag
|
||||
let title = self.title.as_string();
|
||||
let title_len = title
|
||||
.as_ref()
|
||||
.map(|n| "<title>".len() + n.len() + "</title>".len())
|
||||
.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() {
|
||||
first_chunk
|
||||
} 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
|
||||
/// at the highest possible level, without overwriting a [`MetaContext`] that has already been provided
|
||||
/// (for example, by a server-rendering integration.)
|
||||
|
@ -293,12 +313,13 @@ where
|
|||
|
||||
#[cfg(feature = "ssr")]
|
||||
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(
|
||||
&mut inner.head_html,
|
||||
&mut buf,
|
||||
&mut Position::NextChild,
|
||||
false,
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"tried to use a leptos_meta component without `ServerMetaContext` \
|
||||
|
|
Loading…
Reference in a new issue