Skip to content

Commit 7335418

Browse files
committed
feat: ✨ added initial load control
This eliminates double trips entirely and improves all aspects of initial performance. BREAKING CHANGE: error pages use `Rc`s now, new options for actix web integration, app root must be of `<div>` form Closes #2.
1 parent 5cb465a commit 7335418

File tree

20 files changed

+629
-215
lines changed

20 files changed

+629
-215
lines changed

examples/cli/.perseus/server/src/main.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use actix_web::{App, HttpServer};
2-
use app::{get_config_manager, get_locales, get_templates_map, get_translations_manager};
2+
use app::{
3+
get_config_manager, get_error_pages, get_locales, get_templates_map, get_translations_manager,
4+
APP_ROOT,
5+
};
36
use futures::executor::block_on;
47
use perseus_actix_web::{configurer, Options};
58
use std::env;
@@ -25,6 +28,9 @@ async fn main() -> std::io::Result<()> {
2528
wasm_bundle: "dist/pkg/perseus_cli_builder_bg.wasm".to_string(),
2629
templates_map: get_templates_map(),
2730
locales: get_locales(),
31+
root_id: APP_ROOT.to_string(),
32+
snippets: "dist/pkg/snippets".to_string(),
33+
error_pages: get_error_pages(),
2834
},
2935
get_config_manager(),
3036
block_on(get_translations_manager()),

examples/cli/.perseus/src/lib.rs

+36-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use app::{get_error_pages, get_locales, get_templates_map, APP_ROOT};
2+
use perseus::error_pages::ErrorPageData;
23
use perseus::router::{RouteInfo, RouteVerdict};
3-
use perseus::shell::get_render_cfg;
4+
use perseus::shell::{get_initial_state, get_render_cfg, InitialState};
45
use perseus::{app_shell, create_app_route, detect_locale, ClientTranslationsManager, DomNode};
56
use std::cell::RefCell;
67
use std::rc::Rc;
@@ -13,15 +14,24 @@ use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
1314
pub fn run() -> Result<(), JsValue> {
1415
// Panics should always go to the console
1516
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
16-
// Get the root (for the router) we'll be injecting page content into
17+
// Get the root we'll be injecting the router into
1718
let root = web_sys::window()
1819
.unwrap()
1920
.document()
2021
.unwrap()
21-
.query_selector(APP_ROOT)
22+
.query_selector(&format!("#{}", APP_ROOT))
2223
.unwrap()
2324
.unwrap();
2425

26+
// Get the root we'll be injecting actual content into (created by the server)
27+
// This is an `Option<Element>` until we know we aren't doing loclae detection (in which case it wouldn't exist)
28+
let container = web_sys::window()
29+
.unwrap()
30+
.document()
31+
.unwrap()
32+
.query_selector("#__perseus_content")
33+
.unwrap();
34+
2535
// Create a mutable translations manager to control caching
2636
let translations_manager =
2737
Rc::new(RefCell::new(ClientTranslationsManager::new(&get_locales())));
@@ -32,17 +42,20 @@ pub fn run() -> Result<(), JsValue> {
3242
create_app_route! {
3343
name => AppRoute,
3444
// The render configuration is injected verbatim into the HTML shell, so it certainly should be present
35-
render_cfg => get_render_cfg().expect("render configuration invalid or not injected"),
36-
templates => get_templates_map(),
37-
locales => get_locales()
45+
render_cfg => &get_render_cfg().expect("render configuration invalid or not injected"),
46+
templates => &get_templates_map(),
47+
locales => &get_locales()
3848
}
49+
// TODO integrate templates fully
50+
// BUG router sees empty template and moves on, fixed by above
3951

4052
sycamore::render_to(
4153
|| {
4254
template! {
4355
Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle<AppRoute<DomNode>>| {
4456
match &route.get().as_ref().0 {
4557
// Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell!
58+
// If a non-404 error occurred, it will be handled in the app shell
4659
RouteVerdict::Found(RouteInfo {
4760
path,
4861
template,
@@ -53,15 +66,29 @@ pub fn run() -> Result<(), JsValue> {
5366
locale.clone(),
5467
// We give the app shell a translations manager and let it get the `Rc<Translator>` itself (because it can do async safely)
5568
Rc::clone(&translations_manager),
56-
Rc::clone(&error_pages)
69+
Rc::clone(&error_pages),
70+
container.unwrap().clone()
5771
),
5872
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
5973
// Those all go to the same system that redirects to the appropriate locale
74+
// Note that `container` doesn't exist for this scenario
75+
// TODO redirect doesn't work until reload
6076
RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()),
6177
// We handle the 404 for the user for convenience
6278
// To get a translator here, we'd have to go async and dangerously check the URL
63-
RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None),
64-
}
79+
// If this is an initial load, there'll already be an error message, so we should only proceed if the declaration is not `error`
80+
RouteVerdict::NotFound => {
81+
if let InitialState::Error(ErrorPageData { url, status, err }) = get_initial_state() {
82+
// Hydrate the error pages
83+
// Right now, we don't provide translators to any error pages that have come from the server
84+
error_pages.hydrate_page(&url, &status, &err, None, &container.unwrap());
85+
} else {
86+
get_error_pages::<DomNode>().get_template_for_page("", &404, "not found", None);
87+
}
88+
},
89+
};
90+
// Everything is based on hydration, and so we always return an empty template
91+
sycamore::template::Template::empty()
6592
}))
6693
}
6794
},

examples/cli/src/error_pages.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
use perseus::ErrorPages;
1+
use perseus::{ErrorPages, GenericNode};
2+
use std::rc::Rc;
23
use sycamore::template;
34

4-
pub fn get_error_pages() -> ErrorPages {
5-
let mut error_pages = ErrorPages::new(Box::new(|_, _, _, _| {
5+
pub fn get_error_pages<G: GenericNode>() -> ErrorPages<G> {
6+
let mut error_pages = ErrorPages::new(Rc::new(|_, _, _, _| {
67
template! {
78
p { "Another error occurred." }
89
}
910
}));
1011
error_pages.add_page(
1112
404,
12-
Box::new(|_, _, _, _| {
13+
Rc::new(|_, _, _, _| {
1314
template! {
1415
p { "Page not found." }
1516
}
1617
}),
1718
);
1819
error_pages.add_page(
1920
400,
20-
Box::new(|_, _, _, _| {
21+
Rc::new(|_, _, _, _| {
2122
template! {
2223
p { "Client error occurred..." }
2324
}

examples/cli/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod pages;
44
use perseus::define_app;
55

66
define_app! {
7-
root: "#root",
7+
root: "root",
88
error_pages: crate::error_pages::get_error_pages(),
99
templates: [
1010
crate::pages::index::get_page::<G>(),

examples/i18n/src/error_pages.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
use perseus::ErrorPages;
1+
use perseus::{ErrorPages, GenericNode};
2+
use std::rc::Rc;
23
use sycamore::template;
34

4-
pub fn get_error_pages() -> ErrorPages {
5-
let mut error_pages = ErrorPages::new(Box::new(|_, _, _, _| {
5+
pub fn get_error_pages<G: GenericNode>() -> ErrorPages<G> {
6+
let mut error_pages = ErrorPages::new(Rc::new(|_, _, err, _| {
67
template! {
7-
p { "Another error occurred." }
8+
p { (format!("Another error occurred: '{}'.", err)) }
89
}
910
}));
1011
error_pages.add_page(
1112
404,
12-
Box::new(|_, _, _, _| {
13+
Rc::new(|_, _, _, _| {
1314
template! {
1415
p { "Page not found." }
1516
}
1617
}),
1718
);
1819
error_pages.add_page(
1920
400,
20-
Box::new(|_, _, _, _| {
21+
Rc::new(|_, _, _, _| {
2122
template! {
2223
p { "Client error occurred..." }
2324
}

examples/i18n/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod templates;
44
use perseus::define_app;
55

66
define_app! {
7-
root: "#root",
7+
root: "root",
88
error_pages: crate::error_pages::get_error_pages(),
99
templates: [
1010
crate::templates::about::get_template::<G>(),

examples/showcase/src/error_pages.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
use perseus::ErrorPages;
1+
use perseus::{ErrorPages, GenericNode};
2+
use std::rc::Rc;
23
use sycamore::template;
34

4-
pub fn get_error_pages() -> ErrorPages {
5-
let mut error_pages = ErrorPages::new(Box::new(|_, _, _, _| {
5+
pub fn get_error_pages<G: GenericNode>() -> ErrorPages<G> {
6+
let mut error_pages = ErrorPages::new(Rc::new(|_, _, _, _| {
67
template! {
78
p { "Another error occurred." }
89
}
910
}));
1011
error_pages.add_page(
1112
404,
12-
Box::new(|_, _, _, _| {
13+
Rc::new(|_, _, _, _| {
1314
template! {
1415
p { "Page not found." }
1516
}
1617
}),
1718
);
1819
error_pages.add_page(
1920
400,
20-
Box::new(|_, _, _, _| {
21+
Rc::new(|_, _, _, _| {
2122
template! {
2223
p { "Client error occurred..." }
2324
}

examples/showcase/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod templates;
44
use perseus::define_app;
55

66
define_app! {
7-
root: "#root",
7+
root: "root",
88
error_pages: crate::error_pages::get_error_pages(),
99
templates: [
1010
crate::templates::index::get_template::<G>(),

packages/perseus-actix-web/src/configurer.rs

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use crate::initial_load::initial_load;
12
use crate::page_data::page_data;
23
use crate::translations::translations;
3-
use actix_files::NamedFile;
4-
use actix_web::{web, HttpResponse};
5-
use perseus::{get_render_cfg, ConfigManager, Locales, SsrNode, TemplateMap, TranslationsManager};
4+
use actix_files::{Files, NamedFile};
5+
use actix_web::web;
6+
use perseus::{
7+
get_render_cfg, ConfigManager, ErrorPages, Locales, SsrNode, TemplateMap, TranslationsManager,
8+
};
69
use std::collections::HashMap;
710
use std::fs;
811

@@ -22,19 +25,21 @@ pub struct Options {
2225
pub templates_map: TemplateMap<SsrNode>,
2326
/// The locales information for the app.
2427
pub locales: Locales,
28+
/// The HTML `id` of the element at which to render Perseus. On the server-side, interpolation will be done here in a highly
29+
/// efficient manner by not parsing the HTML, so this MUST be of the form `<div id="root_id">` in your markup (double or single
30+
/// quotes, `root_id` replaced by what this property is set to).
31+
pub root_id: String,
32+
/// The location of the JS interop snippets to be served as static files.
33+
pub snippets: String,
34+
/// The error pages for the app. These will be server-rendered if an initial load fails.
35+
pub error_pages: ErrorPages<SsrNode>,
2536
}
2637

2738
async fn render_conf(
2839
render_conf: web::Data<HashMap<String, String>>,
2940
) -> web::Json<HashMap<String, String>> {
3041
web::Json(render_conf.get_ref().clone())
3142
}
32-
/// This returns the HTML index file with the render configuration injected as a JS global variable.
33-
async fn index(index_with_render_cfg: web::Data<String>) -> HttpResponse {
34-
HttpResponse::Ok()
35-
.content_type("text/html")
36-
.body(index_with_render_cfg.get_ref())
37-
}
3843
async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
3944
NamedFile::open(&opts.js_bundle)
4045
}
@@ -94,7 +99,10 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
9499
"/.perseus/translations/{locale}",
95100
web::get().to(translations::<T>),
96101
)
102+
// This allows gettting JS interop snippets (including ones that are supposedly 'inlined')
103+
// These won't change, so they can be set as a filesystem dependency safely
104+
.service(Files::new("/.perseus/snippets", &opts.snippets))
97105
// For everything else, we'll serve the app shell directly
98-
.route("*", web::get().to(index));
106+
.route("*", web::get().to(initial_load::<C, T>));
99107
}
100108
}

0 commit comments

Comments
 (0)