`render_app_to_stream` helper in `leptos_actix`

This commit is contained in:
Greg Johnston 2022-11-20 16:03:08 -05:00
parent eff42a196f
commit 4e8c1758c3
7 changed files with 112 additions and 78 deletions

View File

@ -1,6 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_router::*;
mod counters;
// boilerplate to run in different modes
@ -11,36 +10,6 @@ cfg_if! {
use actix_web::*;
use crate::counters::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
let path = req.path();
let path = "http://leptos".to_string() + path;
println!("path = {path}");
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Isomorphic Counter</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ hydrate }} from './pkg/leptos_counter_isomorphic.js'; init().then(hydrate);</script>
</html>"#,
run_scope({
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <Counters/>}
}
})
))
}
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use futures::StreamExt;
@ -67,7 +36,7 @@ cfg_if! {
.service(Files::new("/pkg", "./pkg"))
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.service(render)
.route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_counter_isomorphic", |cx| view! { cx, <Counters/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?

View File

@ -1,8 +1,5 @@
use cfg_if::cfg_if;
use futures::StreamExt;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod todo;
// boilerplate to run in different modes
@ -13,45 +10,9 @@ cfg_if! {
use actix_web::*;
use crate::todo::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, req.clone());
view! { cx, <TodoApp/> }
};
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { hydrate } from '/pkg/todo_app_sqlite.js'; init().then(hydrate);</script>"#;
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[actix_web::main]
@ -67,8 +28,9 @@ cfg_if! {
HttpServer::new(|| {
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.service(render)
.route("/{tail:.*}", leptos_actix::render_app_to_stream("todo_app_sqlite", |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?

View File

@ -1,5 +1,6 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
@ -91,6 +92,7 @@ pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<div>
<Stylesheet href="/style.css".into()/>
<Router>
<header>
<h1>"My Tasks"</h1>

View File

@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@ -5,6 +5,13 @@ edition = "2021"
[dependencies]
actix-web = "4"
futures = "0.3"
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
"ssr",
] }
leptos_meta = { path = "../../meta", default-features = false, version = "0.0", features = [
"ssr",
] }
leptos_router = { path = "../../router", default-features = false, version = "0.0", features = [
"ssr",
] }

View File

@ -1,5 +1,8 @@
use actix_web::*;
use futures::StreamExt;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
/// Leptos server function arguments in the body, runs the server function if found,
@ -38,6 +41,92 @@ pub fn handle_server_fns() -> Route {
web::post().to(handle_server_fn)
}
/// An Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and also everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{HttpServer, App};
/// use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// HttpServer::new(|| {
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }))
/// })
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// # }
/// ```
pub fn render_app_to_stream(
client_pkg_name: &'static str,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
) -> Route {
web::get().to(move |req: HttpRequest| {
let app_fn = app_fn.clone();
async move {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = {
let app_fn = app_fn.clone();
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, req.clone());
(app_fn)(cx)
}
};
let head = format!(r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, {{ hydrate }} from '/pkg/{client_pkg_name}.js'; init().then(hydrate);</script>"#);
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async move { head.clone() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
})
}
async fn handle_server_fn(
req: HttpRequest,
params: web::Path<String>,

View File

@ -1,6 +1,6 @@
use std::fmt::Debug;
use leptos::*;
use leptos::{leptos_dom::debug_warn, *};
mod stylesheet;
mod title;
@ -16,8 +16,10 @@ pub struct MetaContext {
pub fn use_head(cx: Scope) -> MetaContext {
match use_context::<MetaContext>(cx) {
None => {
log::warn!("use_head() can only be called if a MetaContext has been provided");
panic!()
debug_warn!("use_head() is being called with a MetaContext being provided. We'll automatically create and provide one, but if this is being called in a child route it will cause bugs. To be safe, you should provide_context(cx, MetaContext::new()) somewhere in the root of the app.");
let meta = MetaContext::new();
provide_context(cx, meta.clone());
meta
}
Some(ctx) => ctx,
}