feat: Add Compression to Hacker News w/ Islands Example (#2613)

* Add task for cargo leptos w/ precompression

* Update makefile

* Update deps

* Serve precompressed assets

Code was taken from https://github.com/leptos-rs/cargo-leptos/pull/165#issuecomment-1647843037

Co-authored-by: Sebastian Dobe <sebastiandobe@mailbox.org>

* Dynamically compress html

* Update README

* Refactor: Format for ci

* Refactor: Replace use of format!

* Chore: Remove old build file

* Feat: Hash files

This will prevent users from using an old cached file after updates are made

* Fix: Prevent chicken & egg problem with target/site

* Refactor: Use normal cargo-leptos

---------

Co-authored-by: Sebastian Dobe <sebastiandobe@mailbox.org>
This commit is contained in:
David Karrick 2024-06-28 15:01:05 -04:00 committed by GitHub
parent ff0c8252b0
commit c53fc67d38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 149 additions and 29 deletions

View file

@ -0,0 +1,44 @@
extend = [
{ path = "./lint.toml" }
]
[tasks.make-target-site-dir]
command = "mkdir"
args = ["-p", "target/site"]
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.build]
clear = true
command = "cargo"
dependencies = ["make-target-site-dir"]
args = ["leptos", "build", "--release", "-P"]
[tasks.check]
clear = true
dependencies = ["check-debug", "check-release"]
[tasks.check-debug]
toolchain = "stable"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-release]
toolchain = "stable"
command = "cargo"
args = ["check-all-features", "--release"]
install_crate = "cargo-all-features"
[tasks.lint]
dependencies = ["make-target-site-dir", "check-style"]
[tasks.start-client]
dependencies = ["install-cargo-leptos"]
command = "cargo"
args = ["leptos", "watch", "--release", "-P"]

View file

@ -31,6 +31,7 @@ axum = { version = "0.7", optional = true, features = ["http2"] }
tower = { version = "0.4", optional = true } tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = [ tower-http = { version = "0.5", features = [
"fs", "fs",
"compression-gzip",
"compression-br", "compression-br",
], optional = true } ], optional = true }
tokio = { version = "1", features = ["full"], optional = true } tokio = { version = "1", features = ["full"], optional = true }
@ -38,6 +39,8 @@ http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] } web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
lazy_static = "1.4.0" lazy_static = "1.4.0"
rust-embed = { version = "8", features = ["axum", "mime_guess", "tokio"], optional = true }
mime_guess = { version = "2.0.4", optional = true }
[features] [features]
default = [] default = []
@ -49,6 +52,8 @@ ssr = [
"dep:tower-http", "dep:tower-http",
"dep:tokio", "dep:tokio",
"dep:http", "dep:http",
"dep:rust-embed",
"dep:mime_guess",
"leptos/ssr", "leptos/ssr",
"leptos_axum", "leptos_axum",
"leptos_meta/ssr", "leptos_meta/ssr",
@ -94,6 +99,12 @@ bin-features = ["ssr"]
# Optional. Defaults to false. # Optional. Defaults to false.
bin-default-features = false bin-default-features = false
# This feature will add a hash to the filename of assets.
# This is useful here because our files are precompressed and use a `Cache-Control` policy to reduce HTTP requests
#
# Optional. Defaults to false.
hash_file = true
# The features to use when compiling the lib target # The features to use when compiling the lib target
# #
# Optional. Can be over-ridden with the command line parameter --lib-features # Optional. Can be over-ridden with the command line parameter --lib-features

View file

@ -1,6 +1,6 @@
extend = [ extend = [
{ path = "../cargo-make/main.toml" }, { path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" }, { path = "../cargo-make/cargo-leptos-compress.toml" },
] ]
[env] [env]

View file

@ -1,6 +1,11 @@
# Leptos Hacker News Example with Axum # Leptos Hacker News Example with Axum
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server. This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to:
- Create a client-side rendered app
- Create a server side rendered app with hydration
- Precompress static assets and bundle those in with the server binary
This repo differs from the main Hacker News example by using Axum as it's server, precompressing and embedding static assets into the binary, and dynamically compressing the generated HTML.
## Getting Started ## Getting Started
@ -8,4 +13,4 @@ See the [Examples README](../README.md) for setup and run instructions.
## Quick Start ## Quick Start
Run `cargo leptos watch` to run this example. Run `cargo leptos watch --release -P` to run this example.

View file

@ -1,8 +0,0 @@
wasm-pack build --target=web --features=hydrate --release
cd pkg
rm *.br
cp hackernews.js hackernews.unmin.js
cat hackernews.unmin.js | esbuild > hackernews.js
brotli hackernews.js
brotli hackernews_bg.wasm
brotli style.css

View file

@ -2,20 +2,34 @@ use crate::error_template::error_template;
use axum::{ use axum::{
body::Body, body::Body,
extract::State, extract::State,
http::{Request, Response, StatusCode, Uri}, http::{header, Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse}, response::{IntoResponse, Response as AxumResponse},
}; };
use leptos::LeptosOptions; use leptos::LeptosOptions;
use tower::ServiceExt; use std::borrow::Cow;
use tower_http::services::ServeDir;
#[cfg(not(debug_assertions))]
const DEV_MODE: bool = false;
#[cfg(debug_assertions)]
const DEV_MODE: bool = true;
#[derive(rust_embed::RustEmbed)]
#[folder = "target/site/"]
struct Assets;
pub async fn file_and_error_handler( pub async fn file_and_error_handler(
uri: Uri, uri: Uri,
State(options): State<LeptosOptions>, State(options): State<LeptosOptions>,
req: Request<Body>, req: Request<Body>,
) -> AxumResponse { ) -> AxumResponse {
let root = options.site_root.clone(); let accept_encoding = req
let res = get_static_file(uri.clone(), &root).await.unwrap(); .headers()
.get("accept-encoding")
.map(|h| h.to_str().unwrap_or("none"))
.unwrap_or("none")
.to_string();
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
if res.status() == StatusCode::OK { if res.status() == StatusCode::OK {
res.into_response() res.into_response()
@ -30,19 +44,56 @@ pub async fn file_and_error_handler(
async fn get_static_file( async fn get_static_file(
uri: Uri, uri: Uri,
root: &str, accept_encoding: String,
) -> Result<Response<Body>, (StatusCode, String)> { ) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder() let (_, path) = uri.path().split_at(1); // split off the first `/`
.uri(uri.clone()) let mime = mime_guess::from_path(path);
.body(Body::empty())
.unwrap(); let (path, encoding) = if DEV_MODE {
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // during DEV, don't care about the precompression -> faster workflow
// This path is relative to the cargo root (Cow::from(path), "none")
match ServeDir::new(root).oneshot(req).await { } else if accept_encoding.contains("br") {
Ok(res) => Ok(res.into_response()), (Cow::from(format!("{}.br", path)), "br")
Err(err) => Err(( } else if accept_encoding.contains("gzip") {
StatusCode::INTERNAL_SERVER_ERROR, (Cow::from(format!("{}.gz", path)), "gzip")
format!("Something went wrong: {}", err), } else {
)), (Cow::from(path), "none")
};
match Assets::get(path.as_ref()) {
Some(content) => {
let body = Body::from(content.data);
let res = match DEV_MODE {
true => Response::builder()
.header(
header::CONTENT_TYPE,
mime.first_or_octet_stream().as_ref(),
)
.header(header::CONTENT_ENCODING, encoding)
.body(body)
.unwrap(),
false => Response::builder()
.header(header::CACHE_CONTROL, "max-age=86400")
.header(
header::CONTENT_TYPE,
mime.first_or_octet_stream().as_ref(),
)
.header(header::CONTENT_ENCODING, encoding)
.body(body)
.unwrap(),
};
Ok(res.into_response())
}
None => {
eprintln!(">> Asset {} not found", path);
for a in Assets::iter() {
eprintln!("Available asset: {}", a);
}
Err((StatusCode::NOT_FOUND, "Not found".to_string()))
}
} }
} }

View file

@ -6,16 +6,33 @@ async fn main() {
use hackernews_islands::*; use hackernews_islands::*;
pub use leptos::get_configuration; pub use leptos::get_configuration;
pub use leptos_axum::{generate_route_list, LeptosRoutes}; pub use leptos_axum::{generate_route_list, LeptosRoutes};
use tower_http::compression::{
predicate::{NotForContentType, SizeAbove},
CompressionLayer, CompressionLevel, Predicate,
};
let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options; let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr; let addr = leptos_options.site_addr;
let routes = generate_route_list(App); let routes = generate_route_list(App);
let predicate = SizeAbove::new(1500) // files smaller than 1501 bytes are not compressed, since the MTU (Maximum Transmission Unit) of a TCP packet is 1500 bytes
.and(NotForContentType::GRPC)
.and(NotForContentType::IMAGES)
// prevent compressing assets that are already statically compressed
.and(NotForContentType::const_new("application/javascript"))
.and(NotForContentType::const_new("application/wasm"))
.and(NotForContentType::const_new("text/css"));
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler)) .route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, App) .leptos_routes(&leptos_options, routes, App)
.layer(
CompressionLayer::new()
.quality(CompressionLevel::Fastest)
.compress_when(predicate),
)
.fallback(file_and_error_handler) .fallback(file_and_error_handler)
.with_state(leptos_options); .with_state(leptos_options);