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-http = { version = "0.5", features = [
|
||||
"fs",
|
||||
"compression-gzip",
|
||||
"compression-br",
|
||||
], 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"] }
|
||||
wasm-bindgen = "0.2"
|
||||
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]
|
||||
default = []
|
||||
|
@ -49,6 +52,8 @@ ssr = [
|
|||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:http",
|
||||
"dep:rust-embed",
|
||||
"dep:mime_guess",
|
||||
"leptos/ssr",
|
||||
"leptos_axum",
|
||||
"leptos_meta/ssr",
|
||||
|
@ -94,6 +99,12 @@ bin-features = ["ssr"]
|
|||
# Optional. Defaults to 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
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-compress.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
# 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
|
||||
|
||||
|
@ -8,4 +13,4 @@ See the [Examples README](../README.md) for setup and run instructions.
|
|||
|
||||
## 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::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
http::{header, Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::LeptosOptions;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[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(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
let accept_encoding = req
|
||||
.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 {
|
||||
res.into_response()
|
||||
|
@ -30,19 +44,56 @@ pub async fn file_and_error_handler(
|
|||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
accept_encoding: String,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
let (_, path) = uri.path().split_at(1); // split off the first `/`
|
||||
let mime = mime_guess::from_path(path);
|
||||
|
||||
let (path, encoding) = if DEV_MODE {
|
||||
// during DEV, don't care about the precompression -> faster workflow
|
||||
(Cow::from(path), "none")
|
||||
} else if accept_encoding.contains("br") {
|
||||
(Cow::from(format!("{}.br", path)), "br")
|
||||
} else if accept_encoding.contains("gzip") {
|
||||
(Cow::from(format!("{}.gz", path)), "gzip")
|
||||
} 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::*;
|
||||
pub use leptos::get_configuration;
|
||||
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 leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
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
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.layer(
|
||||
CompressionLayer::new()
|
||||
.quality(CompressionLevel::Fastest)
|
||||
.compress_when(predicate),
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
|
Loading…
Reference in a new issue