Browse Source

basic incremental renderer

Evan Almloff 2 năm trước cách đây
mục cha
commit
3a690877d1

+ 4 - 1
packages/router/Cargo.toml

@@ -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"]

+ 35 - 63
packages/router/examples/static_generation.rs

@@ -2,89 +2,57 @@
 
 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();
-}
-
-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;
-                }
-            }
-        }
+    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();
 
-        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();
+    renderer.pre_cache_static::<Route>();
 
-            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();
+    for _ in 0..2 {
+        for id in 0..10 {
+            renderer.render(Route::Post { id });
         }
     }
 }
 
 #[inline_props]
-fn RenderPath(cx: Scope, path: Route) -> Element {
-    let path = path.clone();
+fn Blog(cx: Scope) -> Element {
     render! {
-        Router {
-            config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
+        div {
+            "Blog"
         }
     }
 }
 
 #[inline_props]
-fn Blog(cx: Scope) -> Element {
+fn Post(cx: Scope, id: usize) -> Element {
     render! {
         div {
-            "Blog"
+            "PostId: {id}"
         }
     }
 }
 
 #[inline_props]
-fn Post(cx: Scope) -> Element {
+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 {},

+ 3 - 0
packages/router/src/lib.rs

@@ -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 - 0
packages/router/src/ssr.rs

@@ -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))
+        }
+    }
+}