parse and inject pre rendered content to work with trunk

This commit is contained in:
Evan Almloff 2023-04-02 16:18:15 -05:00
parent f96425e425
commit 9877dd7ed8
5 changed files with 132 additions and 85 deletions

View file

@ -70,20 +70,48 @@ where
} }
fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
self, mut self,
server_fn_route: &'static str, server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>, cfg: impl Into<ServeConfig<P>>,
) -> Self { ) -> Self {
use tower_http::services::ServeDir; use tower_http::services::{ServeDir, ServeFile};
let cfg = cfg.into(); let cfg = cfg.into();
// Serve the dist folder and the index.html file // Serve all files in dist folder except index.html
let serve_dir = ServeDir::new(cfg.assets_path); let dir = std::fs::read_dir(cfg.assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&cfg.assets_path, e
)
});
self.register_server_fns(server_fn_route) for entry in dir.flatten() {
.nest_service("/assets", serve_dir) let path = entry.path();
.route_service( if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&cfg.assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path));
} else {
self = self.nest_service(&route, ServeFile::new(path));
}
}
// Add server functions and render index.html
self.register_server_fns(server_fn_route).route(
"/", "/",
get(render_handler).with_state((cfg, SSRState::default())), get(render_handler).with_state((cfg, SSRState::default())),
) )

View file

@ -50,24 +50,50 @@ impl DioxusRouterExt for Router {
} }
fn register_server_fns(self, server_fn_route: &'static str) -> Self { fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(|| ServerFnHandler { self.register_server_fns_with_handler(server_fn_route, |func| ServerFnHandler {
server_context: DioxusServerContext::default(), server_context: DioxusServerContext::default(),
function: func, function: func,
}) })
} }
fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
self, mut self,
server_fn_route: &'static str, server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>, cfg: impl Into<ServeConfig<P>>,
) -> Self { ) -> Self {
let cfg = cfg.into(); let cfg = cfg.into();
// Serve the dist folder and the index.html file
let serve_dir = StaticDir::new([cfg.assets_path]); // Serve all files in dist folder except index.html
let dir = std::fs::read_dir(cfg.assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&cfg.assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let serve_dir = StaticDir::new([path.clone()]);
let route = path
.strip_prefix(&cfg.assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}/<**path>", route);
self = self.push(Router::with_path(route).get(serve_dir))
}
self.register_server_fns(server_fn_route) self.register_server_fns(server_fn_route)
.push(Router::with_path("/").get(SSRHandler { cfg })) .push(Router::with_path("/").get(SSRHandler { cfg }))
.push(Router::with_path("assets/<**path>").get(serve_dir))
} }
} }

View file

@ -69,7 +69,7 @@ pub fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
.and(warp::get()) .and(warp::get())
.and(with_ssr_state()) .and(with_ssr_state())
.map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg)))) .map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg))))
.or(warp::path("assets").and(serve_dir)) .or(serve_dir)
.boxed() .boxed()
} }

View file

@ -1,4 +1,3 @@
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use dioxus_core::VirtualDom; use dioxus_core::VirtualDom;
@ -47,12 +46,7 @@ impl Default for SSRState {
impl SSRState { impl SSRState {
pub fn render<P: 'static + Clone>(&self, cfg: &ServeConfig<P>) -> String { pub fn render<P: 'static + Clone>(&self, cfg: &ServeConfig<P>) -> String {
let ServeConfig { let ServeConfig {
app, app, props, index, ..
application_name,
base_path,
head,
props,
..
} = cfg; } = cfg;
let mut vdom = VirtualDom::new_with_props(*app, props.clone()); let mut vdom = VirtualDom::new_with_props(*app, props.clone());
@ -63,38 +57,11 @@ impl SSRState {
let mut html = String::new(); let mut html = String::new();
let result = write!( html += &index.pre_main;
&mut html,
r#"
<!DOCTYPE html>
<html>
<head>{head}
</head><body>
<div id="main">"#
);
if let Err(err) = result {
eprintln!("Failed to write to html: {}", err);
}
let _ = renderer.render_to(&mut html, &vdom); let _ = renderer.render_to(&mut html, &vdom);
if let Err(err) = write!( html += &index.post_main;
&mut html,
r#"</div>
<script type="module">
import init from "/{base_path}/assets/dioxus/{application_name}.js";
init("/{base_path}/assets/dioxus/{application_name}_bg.wasm").then(wasm => {{
if (wasm.__wbindgen_start == undefined) {{
wasm.main();
}}
}});
</script>
</body>
</html>"#
) {
eprintln!("Failed to write to html: {}", err);
}
html html
} }

View file

@ -1,12 +1,15 @@
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use dioxus_core::Component; use dioxus_core::Component;
#[derive(Clone)] #[derive(Clone)]
pub struct ServeConfigBuilder<P: Clone> { pub struct ServeConfigBuilder<P: Clone> {
pub(crate) app: Component<P>, pub(crate) app: Component<P>,
pub(crate) props: P, pub(crate) props: P,
pub(crate) application_name: Option<&'static str>, pub(crate) root_id: Option<&'static str>,
pub(crate) base_path: Option<&'static str>, pub(crate) index_path: Option<&'static str>,
pub(crate) head: Option<&'static str>,
pub(crate) assets_path: Option<&'static str>, pub(crate) assets_path: Option<&'static str>,
} }
@ -16,32 +19,25 @@ impl<P: Clone> ServeConfigBuilder<P> {
Self { Self {
app, app,
props, props,
application_name: None, root_id: None,
base_path: None, index_path: None,
head: None,
assets_path: None, assets_path: None,
} }
} }
/// Set the application name matching the name in the Dioxus.toml file used to build the application /// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
pub fn application_name(mut self, application_name: &'static str) -> Self { pub fn index_path(mut self, index_path: &'static str) -> Self {
self.application_name = Some(application_name); self.index_path = Some(index_path);
self self
} }
/// Set the path the WASM application will be served under /// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main)
pub fn base_path(mut self, base_path: &'static str) -> Self { pub fn root_id(mut self, root_id: &'static str) -> Self {
self.base_path = Some(base_path); self.root_id = Some(root_id);
self self
} }
/// Set the head content to be included in the HTML document served /// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist)
pub fn head(mut self, head: &'static str) -> Self {
self.head = Some(head);
self
}
/// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist/assets)
pub fn assets_path(mut self, assets_path: &'static str) -> Self { pub fn assets_path(mut self, assets_path: &'static str) -> Self {
self.assets_path = Some(assets_path); self.assets_path = Some(assets_path);
self self
@ -49,31 +45,61 @@ impl<P: Clone> ServeConfigBuilder<P> {
/// Build the ServeConfig /// Build the ServeConfig
pub fn build(self) -> ServeConfig<P> { pub fn build(self) -> ServeConfig<P> {
let base_path = self.base_path.unwrap_or("."); let assets_path = self.assets_path.unwrap_or("dist");
let application_name = self.application_name.unwrap_or("dioxus");
let index_path = self
.index_path
.map(PathBuf::from)
.unwrap_or_else(|| format!("{assets_path}/index.html").into());
let root_id = self.root_id.unwrap_or("main");
let index = load_index_html(index_path, root_id);
ServeConfig { ServeConfig {
app: self.app, app: self.app,
props: self.props, props: self.props,
application_name, index,
base_path, assets_path,
head: self.head.map(String::from).unwrap_or(format!(r#"<title>Dioxus Application</title>
<link rel="preload" href="/{base_path}/assets/dioxus/{application_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="" />
<link rel="modulepreload" href="/{base_path}/assets/dioxus/{application_name}.js" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />"#)),
assets_path: self.assets_path.unwrap_or("dist/assets"),
} }
} }
} }
fn load_index_html(path: PathBuf, root_id: &'static str) -> IndexHtml {
let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built.");
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Failed to read index.html");
let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."));
let post_main = post_main.split_once('>').unwrap_or_else(|| {
panic!("Failed to find closing > after id=\"{root_id}\" in index.html.")
});
let (pre_main, post_main) = (
pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
post_main.1.to_string(),
);
IndexHtml {
pre_main,
post_main,
}
}
#[derive(Clone)]
pub(crate) struct IndexHtml {
pub(crate) pre_main: String,
pub(crate) post_main: String,
}
#[derive(Clone)] #[derive(Clone)]
pub struct ServeConfig<P: Clone> { pub struct ServeConfig<P: Clone> {
pub(crate) app: Component<P>, pub(crate) app: Component<P>,
pub(crate) props: P, pub(crate) props: P,
pub(crate) application_name: &'static str, pub(crate) index: IndexHtml,
pub(crate) base_path: &'static str,
pub(crate) head: String,
pub(crate) assets_path: &'static str, pub(crate) assets_path: &'static str,
} }