mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
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:
parent
ff0c8252b0
commit
c53fc67d38
7 changed files with 149 additions and 29 deletions
44
examples/cargo-make/cargo-leptos-compress.toml
Normal file
44
examples/cargo-make/cargo-leptos-compress.toml
Normal 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"]
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
|
@ -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()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue