basic incremental renderer

This commit is contained in:
Evan Almloff 2023-06-21 18:36:32 -07:00
parent bc063c58b7
commit 3a690877d1
4 changed files with 290 additions and 68 deletions

View file

@ -23,9 +23,12 @@ wasm-bindgen = { version = "0.2.86", optional = true }
web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"] } web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"] }
js-sys = { version = "0.3.63", optional = true } js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true } gloo-utils = { version = "0.1.6", optional = true }
dioxus-ssr = { path = "../ssr", optional = true }
lru = { version = "0.10.0", optional = true }
[features] [features]
default = ["web"] default = ["web", "ssr"]
ssr = ["dioxus-ssr", "lru"]
wasm_test = [] wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"] serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"] web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]

View file

@ -2,74 +2,33 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_router::prelude::*; use dioxus_router::prelude::*;
use std::io::prelude::*; use dioxus_router::ssr::{DefaultRenderer, IncrementalRendererConfig};
use std::{path::PathBuf, str::FromStr};
fn main() { fn main() {
render_static_pages(); let mut renderer = IncrementalRendererConfig::new(DefaultRenderer {
} before_body: r#"<!DOCTYPE html>
fn render_static_pages() {
for route in Route::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
{
// check if this is a static segment
let mut file_path = PathBuf::from("./");
let mut full_path = String::new();
let mut is_static = true;
for segment in &route {
match segment {
SegmentType::Static(s) => {
file_path.push(s);
full_path += "/";
full_path += s;
}
_ => {
// skip routes with any dynamic segments
is_static = false;
break;
}
}
}
if is_static {
let route = Route::from_str(&full_path).unwrap();
let mut vdom = VirtualDom::new_with_props(RenderPath, RenderPathProps { path: route });
let _ = vdom.rebuild();
file_path.push("index.html");
std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
let mut file = std::fs::File::create(file_path).unwrap();
let body = dioxus_ssr::render(&vdom);
let html = format!(
r#"
<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width,
<title>{}</title> initial-scale=1.0">
<title>Dioxus Application</title>
</head> </head>
<body> <body>"#
{} .to_string(),
</body> after_body: r#"</body>
</html> </html>"#
"#, .to_string(),
full_path, body })
); .static_dir("./static")
file.write_all(html.as_bytes()).unwrap(); .memory_cache_limit(5)
} .build();
}
}
#[inline_props] renderer.pre_cache_static::<Route>();
fn RenderPath(cx: Scope, path: Route) -> Element {
let path = path.clone(); for _ in 0..2 {
render! { for id in 0..10 {
Router { renderer.render(Route::Post { id });
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
} }
} }
} }
@ -84,7 +43,16 @@ fn Blog(cx: Scope) -> Element {
} }
#[inline_props] #[inline_props]
fn Post(cx: Scope) -> Element { fn Post(cx: Scope, id: usize) -> Element {
render! {
div {
"PostId: {id}"
}
}
}
#[inline_props]
fn PostHome(cx: Scope) -> Element {
render! { render! {
div { div {
"Post" "Post"
@ -107,8 +75,12 @@ enum Route {
#[nest("/blog")] #[nest("/blog")]
#[route("/")] #[route("/")]
Blog {}, Blog {},
#[route("/post")] #[route("/post/index")]
Post {}, PostHome {},
#[route("/post/:id")]
Post {
id: usize,
},
#[end_nest] #[end_nest]
#[route("/")] #[route("/")]
Home {}, Home {},

View file

@ -6,6 +6,9 @@
pub mod navigation; pub mod navigation;
pub mod routable; pub mod routable;
#[cfg(feature = "ssr")]
pub mod ssr;
/// Components interacting with the router. /// Components interacting with the router.
pub mod components { pub mod components {
mod default_errors; mod default_errors;

244
packages/router/src/ssr.rs Normal file
View file

@ -0,0 +1,244 @@
//! Incremental file based incremental rendering
#![allow(non_snake_case)]
use crate::prelude::*;
use dioxus::prelude::*;
use std::{
io::{Read, Write},
num::NonZeroUsize,
path::{Path, PathBuf},
str::FromStr,
};
/// Something that can render a HTML page from a body.
pub trait RenderHTML {
/// Render a HTML page from a body.
fn render_html(&self, body: &str) -> String;
}
/// The default page renderer
pub struct DefaultRenderer {
/// The HTML before the body.
pub before_body: String,
/// The HTML after the body.
pub after_body: String,
}
impl Default for DefaultRenderer {
fn default() -> Self {
let before = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#;
let after = r#"</body>
</html>"#;
Self {
before_body: before.to_string(),
after_body: after.to_string(),
}
}
}
impl RenderHTML for DefaultRenderer {
fn render_html(&self, body: &str) -> String {
format!("{}{}{}", self.before_body, body, self.after_body)
}
}
/// A configuration for the incremental renderer.
pub struct IncrementalRendererConfig<R: RenderHTML> {
static_dir: PathBuf,
memory_cache_limit: usize,
render: R,
}
impl Default for IncrementalRendererConfig<DefaultRenderer> {
fn default() -> Self {
Self::new(DefaultRenderer::default())
}
}
impl<R: RenderHTML> IncrementalRendererConfig<R> {
/// Create a new incremental renderer configuration.
pub fn new(render: R) -> Self {
Self {
static_dir: PathBuf::from("./static"),
memory_cache_limit: 100,
render,
}
}
/// Set the static directory.
pub fn static_dir<P: AsRef<Path>>(mut self, static_dir: P) -> Self {
self.static_dir = static_dir.as_ref().to_path_buf();
self
}
/// Set the memory cache limit.
pub const fn memory_cache_limit(mut self, memory_cache_limit: usize) -> Self {
self.memory_cache_limit = memory_cache_limit;
self
}
/// Build the incremental renderer.
pub fn build(self) -> IncrementalRenderer<R> {
IncrementalRenderer {
static_dir: self.static_dir,
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
.map(|limit| lru::LruCache::new(limit)),
render: self.render,
}
}
}
/// An incremental renderer.
pub struct IncrementalRenderer<R: RenderHTML> {
static_dir: PathBuf,
memory_cache: Option<lru::LruCache<String, String>>,
render: R,
}
impl<R: RenderHTML> IncrementalRenderer<R> {
/// Create a new incremental renderer builder.
pub fn builder(renderer: R) -> IncrementalRendererConfig<R> {
IncrementalRendererConfig::new(renderer)
}
fn render_uncached<Rt>(&self, route: Rt) -> String
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
{
let mut vdom = VirtualDom::new_with_props(RenderPath, RenderPathProps { path: route });
let _ = vdom.rebuild();
let body = dioxus_ssr::render(&vdom);
self.render.render_html(&body)
}
fn add_to_cache(&mut self, route: String, html: String) {
let file_path = self.route_as_path(&route);
if let Some(parent) = file_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).unwrap();
}
}
let file = std::fs::File::create(dbg!(file_path)).unwrap();
let mut file = std::io::BufWriter::new(file);
file.write_all(html.as_bytes()).unwrap();
if let Some(cache) = self.memory_cache.as_mut() {
cache.put(route, html);
}
}
fn search_cache(&mut self, route: String) -> Option<String> {
if let Some(cache_hit) = self
.memory_cache
.as_mut()
.and_then(|cache| cache.get(&route).cloned())
{
println!("memory cache hit");
Some(cache_hit)
} else {
let file_path = self.route_as_path(&route);
if let Ok(file) = dbg!(std::fs::File::open(file_path)) {
let mut file = std::io::BufReader::new(file);
let mut html = String::new();
file.read_to_string(&mut html).ok()?;
println!("file cache hit");
Some(html)
} else {
None
}
}
}
/// Render a route or get it from cache.
pub fn render<Rt>(&mut self, route: Rt) -> String
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
{
// check if this route is cached
if let Some(html) = self.search_cache(route.to_string()) {
return html;
}
// if not, create it
println!("cache miss");
let html = self.render_uncached(route.clone());
self.add_to_cache(route.to_string(), html.clone());
html
}
fn route_as_path(&self, route: &str) -> PathBuf {
let mut file_path = self.static_dir.clone();
for segment in route.split('/') {
file_path.push(segment);
}
file_path.push("index");
file_path.set_extension("html");
file_path
}
/// Pre-cache all static routes.
pub fn pre_cache_static<Rt>(&mut self)
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
{
for route in Rt::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
{
// check if this is a static segment
let mut is_static = true;
let mut full_path = String::new();
for segment in &route {
match segment {
SegmentType::Static(s) => {
full_path += "/";
full_path += s;
}
_ => {
// skip routes with any dynamic segments
is_static = false;
break;
}
}
}
if is_static {
match Rt::from_str(&full_path) {
Ok(route) => {
let _ = self.render(route);
}
Err(e) => {
log::error!("Error pre-caching static route: {}", e);
}
}
}
}
}
}
#[inline_props]
fn RenderPath<R>(cx: Scope, path: R) -> Element
where
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
let path = path.clone();
render! {
GenericRouter::<R> {
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}