移除 SSR HTML minify 中间件及相关工具
This commit is contained in:
parent
476fad27e5
commit
2c1190d8fb
@ -21,7 +21,6 @@ mod db;
|
||||
mod highlight;
|
||||
mod hooks;
|
||||
#[cfg(feature = "server")]
|
||||
mod middleware;
|
||||
mod models;
|
||||
mod pages;
|
||||
mod router;
|
||||
@ -104,11 +103,6 @@ fn main() {
|
||||
// 合并三条路由:自定义 API、静态资源、Dioxus 主应用
|
||||
let router = api_routes.merge(static_routes).merge(dioxus_app);
|
||||
|
||||
// 对 SSR 返回的 HTML 做空白压缩(保留 <pre>/<code> 等标签内格式)
|
||||
let router = router.layer(axum::middleware::from_fn(
|
||||
crate::middleware::minify_html::layer,
|
||||
));
|
||||
|
||||
Ok(router)
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
//! SSR HTML 空白压缩中间件。
|
||||
//!
|
||||
//! 对 Dioxus fullstack 返回的 `text/html` 响应做轻量 minify。
|
||||
//!
|
||||
//! 注意:这里**不**维护 per-URL 缓存。同一个 URL 在 admin 登录态与匿名态下
|
||||
//! 渲染出的 HTML 不同(例如是否带管理按钮),按 URL 缓存会导致跨用户内容
|
||||
//! 泄露。Dioxus 的增量渲染器(ISRG)本身已经按 URL 缓存了 SSR HTML,
|
||||
//! 这里的 minify 只是对其输出做一次无损空白折叠,无需再叠加一层缓存。
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Request,
|
||||
http::{header, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
/// Axum 中间件入口。
|
||||
pub async fn layer(request: Request, next: Next) -> Response {
|
||||
let response = next.run(request).await;
|
||||
|
||||
let is_html = response
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|ct| ct.starts_with("text/html"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_html {
|
||||
return response;
|
||||
}
|
||||
|
||||
let (parts, body) = response.into_parts();
|
||||
let bytes = match body.collect().await {
|
||||
Ok(collected) => collected.to_bytes(),
|
||||
Err(_) => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::empty())
|
||||
.expect("failed to build error response");
|
||||
}
|
||||
};
|
||||
|
||||
let original = String::from_utf8_lossy(&bytes);
|
||||
let minified = crate::utils::html_minify::minify_html(&original);
|
||||
|
||||
let mut response = Response::builder()
|
||||
.status(parts.status)
|
||||
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
|
||||
.body(Body::from(minified.clone()))
|
||||
.expect("failed to build minified response");
|
||||
|
||||
// 保留原响应的其他 header,并修正 Content-Length
|
||||
*response.headers_mut() = parts.headers;
|
||||
response.headers_mut().remove(header::TRANSFER_ENCODING);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::CONTENT_LENGTH, minified.len().into());
|
||||
|
||||
response
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
#[cfg(feature = "server")]
|
||||
pub mod minify_html;
|
||||
@ -1,93 +0,0 @@
|
||||
//! HTML 空白压缩工具函数。
|
||||
//!
|
||||
//! 仅服务端使用。对 HTML 片段做轻量 minify:
|
||||
//! - 合并标签之间连续空白为一个空格;
|
||||
//! - 移除普通 HTML 注释,但**保留 Dioxus hydration marker 注释**;
|
||||
//! - 保留 `<pre>`、`<code>`、`<textarea>`、`<script>`、`<style>` 内部原样空白。
|
||||
//!
|
||||
//! Dioxus 0.7 SSR 会在输出中插入若干 load-bearing 的注释 marker,客户端
|
||||
//! hydration 依赖它们定位动态节点。删除这些注释会导致前端 hydration
|
||||
//! 失败(节点对不齐、交互失效)。需要保留的格式:
|
||||
//! `<!--#-->` 动态文本节点结束 marker
|
||||
//! `<!--node-id<N>-->` 动态文本节点起始 marker
|
||||
//! `<!--placeholder-->` / `<!--placeholder<N>-->` 占位节点 marker
|
||||
//! (参考 others/dioxus/packages/ssr/src/renderer.rs)
|
||||
|
||||
use lol_html::{doc_comments, doc_text, element, rewrite_str, RewriteStrSettings};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// 判断一段注释文本是否为 Dioxus hydration marker,必须原样保留。
|
||||
fn is_hydration_marker(comment_text: &str) -> bool {
|
||||
comment_text == "#"
|
||||
|| comment_text.starts_with("node-id")
|
||||
|| comment_text.starts_with("placeholder")
|
||||
}
|
||||
|
||||
/// 压缩 HTML 中的无用空白。
|
||||
pub fn minify_html(input: &str) -> String {
|
||||
let protected_depth: Rc<Cell<usize>> = Rc::new(Cell::new(0));
|
||||
|
||||
rewrite_str(
|
||||
input,
|
||||
RewriteStrSettings {
|
||||
element_content_handlers: vec![element!(
|
||||
"pre, code, textarea, script, style",
|
||||
{
|
||||
let depth = protected_depth.clone();
|
||||
move |el| {
|
||||
depth.set(depth.get() + 1);
|
||||
let depth_end = depth.clone();
|
||||
let _ = el.on_end_tag(lol_html::end_tag!(move |_end| {
|
||||
depth_end.set(depth_end.get().saturating_sub(1));
|
||||
Ok(())
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
)],
|
||||
document_content_handlers: vec![
|
||||
doc_text!({
|
||||
let depth = protected_depth.clone();
|
||||
move |text| {
|
||||
if depth.get() == 0 {
|
||||
let s = text.as_str();
|
||||
let collapsed = collapse_whitespace(s);
|
||||
if collapsed != s {
|
||||
text.set_str(collapsed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}),
|
||||
doc_comments!(|c| {
|
||||
// 只删除非 hydration marker 的普通注释。
|
||||
if !is_hydration_marker(c.text().as_str()) {
|
||||
c.remove();
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
],
|
||||
..RewriteStrSettings::default()
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|_| input.to_string())
|
||||
}
|
||||
|
||||
/// 将连续空白字符合并为一个空格。
|
||||
fn collapse_whitespace(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut prev_ws = false;
|
||||
for ch in s.chars() {
|
||||
if ch.is_whitespace() {
|
||||
if !prev_ws {
|
||||
out.push(' ');
|
||||
prev_ws = true;
|
||||
}
|
||||
} else {
|
||||
out.push(ch);
|
||||
prev_ws = false;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
//! 临时 bench:测量 minify_html 对真实大小 HTML 的耗时。
|
||||
//! 用完即删。
|
||||
|
||||
#[cfg(all(test, feature = "server"))]
|
||||
mod bench {
|
||||
use std::time::Instant;
|
||||
|
||||
fn synthetic_html(size_hint: usize) -> String {
|
||||
// 构造一个带空白、注释、嵌套标签的模拟 HTML,体积接近真实 SSR 输出。
|
||||
let unit = "<div class=\"x\">\n <p>hello world</p>\n <!-- comment -->\n <a href=\"/p\">link</a>\n</div>\n";
|
||||
let unit_len = unit.len();
|
||||
let repeats = size_hint / unit_len + 1;
|
||||
unit.repeat(repeats)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_minify_throughput() {
|
||||
for &kb in &[50usize, 200, 568] {
|
||||
let html = synthetic_html(kb * 1024);
|
||||
// 预热
|
||||
let _ = crate::utils::html_minify::minify_html(&html);
|
||||
|
||||
let n = 20;
|
||||
let start = Instant::now();
|
||||
for _ in 0..n {
|
||||
let _ = crate::utils::html_minify::minify_html(&html);
|
||||
}
|
||||
let elapsed = start.elapsed() / n;
|
||||
let input_kb = html.len() as f64 / 1024.0;
|
||||
let mbps = (html.len() as f64 / elapsed.as_secs_f64()) / (1024.0 * 1024.0);
|
||||
println!(
|
||||
"input={:.0}KB avg minify={:?} (~{:.0} MB/s)",
|
||||
input_kb,
|
||||
elapsed,
|
||||
mbps
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,3 @@
|
||||
pub mod text;
|
||||
/// 跨平台的异步睡眠等时间工具。
|
||||
pub mod time;
|
||||
/// HTML 空白压缩工具。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod html_minify;
|
||||
|
||||
#[cfg(all(test, feature = "server"))]
|
||||
mod html_minify_bench;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user