mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-27 14:40:44 +00:00
basic incremental renderer
This commit is contained in:
parent
bc063c58b7
commit
3a690877d1
4 changed files with 290 additions and 68 deletions
|
@ -23,9 +23,12 @@ wasm-bindgen = { version = "0.2.86", optional = true }
|
|||
web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"] }
|
||||
js-sys = { version = "0.3.63", optional = true }
|
||||
gloo-utils = { version = "0.1.6", optional = true }
|
||||
dioxus-ssr = { path = "../ssr", optional = true }
|
||||
lru = { version = "0.10.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = ["web", "ssr"]
|
||||
ssr = ["dioxus-ssr", "lru"]
|
||||
wasm_test = []
|
||||
serde = ["dep:serde", "gloo-utils/serde"]
|
||||
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
|
||||
|
|
|
@ -2,74 +2,33 @@
|
|||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::*;
|
||||
use std::io::prelude::*;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
use dioxus_router::ssr::{DefaultRenderer, IncrementalRendererConfig};
|
||||
|
||||
fn main() {
|
||||
render_static_pages();
|
||||
}
|
||||
let mut renderer = IncrementalRendererConfig::new(DefaultRenderer {
|
||||
before_body: 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>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
})
|
||||
.static_dir("./static")
|
||||
.memory_cache_limit(5)
|
||||
.build();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.pre_cache_static::<Route>();
|
||||
|
||||
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">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{}</title>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
full_path, body
|
||||
);
|
||||
file.write_all(html.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn RenderPath(cx: Scope, path: Route) -> Element {
|
||||
let path = path.clone();
|
||||
render! {
|
||||
Router {
|
||||
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
|
||||
for _ in 0..2 {
|
||||
for id in 0..10 {
|
||||
renderer.render(Route::Post { id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +43,16 @@ fn Blog(cx: Scope) -> Element {
|
|||
}
|
||||
|
||||
#[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! {
|
||||
div {
|
||||
"Post"
|
||||
|
@ -107,8 +75,12 @@ enum Route {
|
|||
#[nest("/blog")]
|
||||
#[route("/")]
|
||||
Blog {},
|
||||
#[route("/post")]
|
||||
Post {},
|
||||
#[route("/post/index")]
|
||||
PostHome {},
|
||||
#[route("/post/:id")]
|
||||
Post {
|
||||
id: usize,
|
||||
},
|
||||
#[end_nest]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
pub mod navigation;
|
||||
pub mod routable;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr;
|
||||
|
||||
/// Components interacting with the router.
|
||||
pub mod components {
|
||||
mod default_errors;
|
||||
|
|
244
packages/router/src/ssr.rs
Normal file
244
packages/router/src/ssr.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue