Some more WIP improvements for the Axum example

This commit is contained in:
Ben Wishovich 2022-11-15 14:55:38 -08:00
parent 3885816699
commit 19f89633ff
3 changed files with 164 additions and 155 deletions

View File

@ -27,13 +27,14 @@ axum = { version = "0.5.17", optional=true }
tower = { version = "0.4.13", optional=true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
http = {version = "0.2.8", optional = true}
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:http", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio"]
denylist = ["axum", "tower", "tower-http", "tokio", "http"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@ -1,42 +1,89 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, Bytes, BoxBody, StreamBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::io;
use futures::{Stream, StreamExt};
use leptos::*;
use leptos_router::*;
use leptos_meta::*;
use crate::*;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone()).await?;
println!("{:?}", res);
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone()).await?;
println!("{:?}", res);
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html).await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html).await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri).body(Body::empty()).unwrap();
async fn get_static_file(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
// match every path — our router will handle actual dispatch, except for the static css files
async fn render_app(request: Request<Body>) -> StreamBody<impl Stream<Item = io::Result<Bytes>>> {
// Need to get the path and query string of the Request
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));
view! { cx, <App/> }
};
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, { main } from '/pkg/leptos_hackernews_axum.js'; init().then(main);</script>"#;
let tail = "</body></html>";
let stream = 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(Bytes::from(html)) as io::Result<Bytes>);
StreamBody::new(stream)
}
}
}
}
}

View File

@ -3,131 +3,92 @@ use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
// use actix_files::{Files, NamedFile};
// use actix_web::*;
use axum::{
routing::{get},
Router,
body::Bytes,
response::{IntoResponse, Response},
};
use std::net::SocketAddr;
use futures::StreamExt;
use leptos_meta::*;
use leptos_router::*;
if #[cfg(feature = "ssr")] {
// use actix_files::{Files, NamedFile};
// use actix_web::*;
use axum::{
routing::{get},
Router,
body::Bytes,
response::{IntoResponse, Response},
};
use std::net::SocketAddr;
use futures::StreamExt;
use leptos_meta::*;
use leptos_router::*;
use leptos_hackernews_axum::*;
use crate::handlers::file_handler;
use std::io;
// #[get("/static/style.css")]
// async fn css() -> impl Responder {
// NamedFile::open_async("./style.css").await
// }
#[tokio::main]
async fn main() -> std::io::Result<()> {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
log::debug!("serving at {addr}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
// necessary for proper HTTP/2 streaming
// load TLS keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
// builder
// .set_private_key_file("key.pem", SslFiletype::PEM)
// .unwrap();
// builder.set_certificate_chain_file("cert.pem").unwrap();
// HttpServer::new(|| {
// App::new()
// .service(css)
// .service(
// web::scope("/pkg")
// .service(Files::new("", "./dist"))
// .wrap(middleware::Compress::default()),
// )
// .service(render_app)
// })
// .bind(("127.0.0.1", 8080))?
// // replace .bind with .bind_openssl to use HTTPS
// //.bind_openssl(&format!("{}:{}", host, port), builder)?
// .run()
// .await
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.nest("/pkg", get(file_handler))
.fallback(fallback.into_service());
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
tracing::debug!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use leptos_hackernews_axum::*;
use crate::handlers::file_handler;
use std::io;
#[get("/static/style.css")]
async fn css() -> impl Responder {
NamedFile::open_async("./style.css").await
}
// match every path — our router will handle actual dispatch
#[get("{tail:.*}")]
async fn render_app(req: Request) -> StreamBody<impl Stream<Item = io::Result<Bytes>> {
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));
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
};
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, { main } from '/pkg/leptos_hackernews_axum.js'; init().then(main);</script>"#;
let tail = "</body></html>";
let stream = 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(Bytes::from(html)) as Result<Bytes>);
StreamBody::new(stream)
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
log::debug!("serving at {addr}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
// necessary for proper HTTP/2 streaming
// load TLS keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
// builder
// .set_private_key_file("key.pem", SslFiletype::PEM)
// .unwrap();
// builder.set_certificate_chain_file("cert.pem").unwrap();
// HttpServer::new(|| {
// App::new()
// .service(css)
// .service(
// web::scope("/pkg")
// .service(Files::new("", "./dist"))
// .wrap(middleware::Compress::default()),
// )
// .service(render_app)
// })
// .bind(("127.0.0.1", 8080))?
// // replace .bind with .bind_openssl to use HTTPS
// //.bind_openssl(&format!("{}:{}", host, port), builder)?
// .run()
// .await
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.nest("/pkg", get(file_handler));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
tracing::debug!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// client-only stuff for Trunk
else {
use leptos_hackernews::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
});
}
});
}
}
}