mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-30 16:09:12 +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"] }
|
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"]
|
||||||
|
|
|
@ -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>
|
||||||
|
<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() {
|
renderer.pre_cache_static::<Route>();
|
||||||
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 {
|
for _ in 0..2 {
|
||||||
let route = Route::from_str(&full_path).unwrap();
|
for id in 0..10 {
|
||||||
let mut vdom = VirtualDom::new_with_props(RenderPath, RenderPathProps { path: route });
|
renderer.render(Route::Post { id });
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {},
|
||||||
|
|
|
@ -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
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