Skip to content

Commit

Permalink
feat: ✨ added initial load control
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
arctic-hen7 committed Sep 18, 2021
1 parent 5cb465a commit 7335418
Show file tree
Hide file tree
Showing 20 changed files with 629 additions and 215 deletions.
8 changes: 7 additions & 1 deletion examples/cli/.perseus/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use actix_web::{App, HttpServer};
use app::{get_config_manager, get_locales, get_templates_map, get_translations_manager};
use app::{
get_config_manager, get_error_pages, get_locales, get_templates_map, get_translations_manager,
APP_ROOT,
};
use futures::executor::block_on;
use perseus_actix_web::{configurer, Options};
use std::env;
Expand All @@ -25,6 +28,9 @@ async fn main() -> std::io::Result<()> {
wasm_bundle: "dist/pkg/perseus_cli_builder_bg.wasm".to_string(),
templates_map: get_templates_map(),
locales: get_locales(),
root_id: APP_ROOT.to_string(),
snippets: "dist/pkg/snippets".to_string(),
error_pages: get_error_pages(),
},
get_config_manager(),
block_on(get_translations_manager()),
Expand Down
45 changes: 36 additions & 9 deletions examples/cli/.perseus/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use app::{get_error_pages, get_locales, get_templates_map, APP_ROOT};
use perseus::error_pages::ErrorPageData;
use perseus::router::{RouteInfo, RouteVerdict};
use perseus::shell::get_render_cfg;
use perseus::shell::{get_initial_state, get_render_cfg, InitialState};
use perseus::{app_shell, create_app_route, detect_locale, ClientTranslationsManager, DomNode};
use std::cell::RefCell;
use std::rc::Rc;
Expand All @@ -13,15 +14,24 @@ use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
pub fn run() -> Result<(), JsValue> {
// Panics should always go to the console
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
// Get the root (for the router) we'll be injecting page content into
// Get the root we'll be injecting the router into
let root = web_sys::window()
.unwrap()
.document()
.unwrap()
.query_selector(APP_ROOT)
.query_selector(&format!("#{}", APP_ROOT))
.unwrap()
.unwrap();

// Get the root we'll be injecting actual content into (created by the server)
// This is an `Option<Element>` until we know we aren't doing loclae detection (in which case it wouldn't exist)
let container = web_sys::window()
.unwrap()
.document()
.unwrap()
.query_selector("#__perseus_content")
.unwrap();

// Create a mutable translations manager to control caching
let translations_manager =
Rc::new(RefCell::new(ClientTranslationsManager::new(&get_locales())));
Expand All @@ -32,17 +42,20 @@ pub fn run() -> Result<(), JsValue> {
create_app_route! {
name => AppRoute,
// The render configuration is injected verbatim into the HTML shell, so it certainly should be present
render_cfg => get_render_cfg().expect("render configuration invalid or not injected"),
templates => get_templates_map(),
locales => get_locales()
render_cfg => &get_render_cfg().expect("render configuration invalid or not injected"),
templates => &get_templates_map(),
locales => &get_locales()
}
// TODO integrate templates fully
// BUG router sees empty template and moves on, fixed by above

sycamore::render_to(
|| {
template! {
Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle<AppRoute<DomNode>>| {
match &route.get().as_ref().0 {
// Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell!
// If a non-404 error occurred, it will be handled in the app shell
RouteVerdict::Found(RouteInfo {
path,
template,
Expand All @@ -53,15 +66,29 @@ pub fn run() -> Result<(), JsValue> {
locale.clone(),
// We give the app shell a translations manager and let it get the `Rc<Translator>` itself (because it can do async safely)
Rc::clone(&translations_manager),
Rc::clone(&error_pages)
Rc::clone(&error_pages),
container.unwrap().clone()
),
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
// Those all go to the same system that redirects to the appropriate locale
// Note that `container` doesn't exist for this scenario
// TODO redirect doesn't work until reload
RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()),
// We handle the 404 for the user for convenience
// To get a translator here, we'd have to go async and dangerously check the URL
RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None),
}
// If this is an initial load, there'll already be an error message, so we should only proceed if the declaration is not `error`
RouteVerdict::NotFound => {
if let InitialState::Error(ErrorPageData { url, status, err }) = get_initial_state() {
// Hydrate the error pages
// Right now, we don't provide translators to any error pages that have come from the server
error_pages.hydrate_page(&url, &status, &err, None, &container.unwrap());
} else {
get_error_pages::<DomNode>().get_template_for_page("", &404, "not found", None);
}
},
};
// Everything is based on hydration, and so we always return an empty template
sycamore::template::Template::empty()
}))
}
},
Expand Down
11 changes: 6 additions & 5 deletions examples/cli/src/error_pages.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
use perseus::ErrorPages;
use perseus::{ErrorPages, GenericNode};
use std::rc::Rc;
use sycamore::template;

pub fn get_error_pages() -> ErrorPages {
let mut error_pages = ErrorPages::new(Box::new(|_, _, _, _| {
pub fn get_error_pages<G: GenericNode>() -> ErrorPages<G> {
let mut error_pages = ErrorPages::new(Rc::new(|_, _, _, _| {
template! {
p { "Another error occurred." }
}
}));
error_pages.add_page(
404,
Box::new(|_, _, _, _| {
Rc::new(|_, _, _, _| {
template! {
p { "Page not found." }
}
}),
);
error_pages.add_page(
400,
Box::new(|_, _, _, _| {
Rc::new(|_, _, _, _| {
template! {
p { "Client error occurred..." }
}
Expand Down
2 changes: 1 addition & 1 deletion examples/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod pages;
use perseus::define_app;

define_app! {
root: "#root",
root: "root",
error_pages: crate::error_pages::get_error_pages(),
templates: [
crate::pages::index::get_page::<G>(),
Expand Down
13 changes: 7 additions & 6 deletions examples/i18n/src/error_pages.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
use perseus::ErrorPages;
use perseus::{ErrorPages, GenericNode};
use std::rc::Rc;
use sycamore::template;

pub fn get_error_pages() -> ErrorPages {
let mut error_pages = ErrorPages::new(Box::new(|_, _, _, _| {
pub fn get_error_pages<G: GenericNode>() -> ErrorPages<G> {
let mut error_pages = ErrorPages::new(Rc::new(|_, _, err, _| {
template! {
p { "Another error occurred." }
p { (format!("Another error occurred: '{}'.", err)) }
}
}));
error_pages.add_page(
404,
Box::new(|_, _, _, _| {
Rc::new(|_, _, _, _| {
template! {
p { "Page not found." }
}
}),
);
error_pages.add_page(
400,
Box::new(|_, _, _, _| {
Rc::new(|_, _, _, _| {
template! {
p { "Client error occurred..." }
}
Expand Down
2 changes: 1 addition & 1 deletion examples/i18n/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod templates;
use perseus::define_app;

define_app! {
root: "#root",
root: "root",
error_pages: crate::error_pages::get_error_pages(),
templates: [
crate::templates::about::get_template::<G>(),
Expand Down
11 changes: 6 additions & 5 deletions examples/showcase/src/error_pages.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
use perseus::ErrorPages;
use perseus::{ErrorPages, GenericNode};
use std::rc::Rc;
use sycamore::template;

pub fn get_error_pages() -> ErrorPages {
let mut error_pages = ErrorPages::new(Box::new(|_, _, _, _| {
pub fn get_error_pages<G: GenericNode>() -> ErrorPages<G> {
let mut error_pages = ErrorPages::new(Rc::new(|_, _, _, _| {
template! {
p { "Another error occurred." }
}
}));
error_pages.add_page(
404,
Box::new(|_, _, _, _| {
Rc::new(|_, _, _, _| {
template! {
p { "Page not found." }
}
}),
);
error_pages.add_page(
400,
Box::new(|_, _, _, _| {
Rc::new(|_, _, _, _| {
template! {
p { "Client error occurred..." }
}
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod templates;
use perseus::define_app;

define_app! {
root: "#root",
root: "root",
error_pages: crate::error_pages::get_error_pages(),
templates: [
crate::templates::index::get_template::<G>(),
Expand Down
28 changes: 18 additions & 10 deletions packages/perseus-actix-web/src/configurer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::initial_load::initial_load;
use crate::page_data::page_data;
use crate::translations::translations;
use actix_files::NamedFile;
use actix_web::{web, HttpResponse};
use perseus::{get_render_cfg, ConfigManager, Locales, SsrNode, TemplateMap, TranslationsManager};
use actix_files::{Files, NamedFile};
use actix_web::web;
use perseus::{
get_render_cfg, ConfigManager, ErrorPages, Locales, SsrNode, TemplateMap, TranslationsManager,
};
use std::collections::HashMap;
use std::fs;

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

async fn render_conf(
render_conf: web::Data<HashMap<String, String>>,
) -> web::Json<HashMap<String, String>> {
web::Json(render_conf.get_ref().clone())
}
/// This returns the HTML index file with the render configuration injected as a JS global variable.
async fn index(index_with_render_cfg: web::Data<String>) -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html")
.body(index_with_render_cfg.get_ref())
}
async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.js_bundle)
}
Expand Down Expand Up @@ -94,7 +99,10 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
"/.perseus/translations/{locale}",
web::get().to(translations::<T>),
)
// This allows gettting JS interop snippets (including ones that are supposedly 'inlined')
// These won't change, so they can be set as a filesystem dependency safely
.service(Files::new("/.perseus/snippets", &opts.snippets))
// For everything else, we'll serve the app shell directly
.route("*", web::get().to(index));
.route("*", web::get().to(initial_load::<C, T>));
}
}
Loading

0 comments on commit 7335418

Please sign in to comment.