移除 SSR HTML minify 中间件及相关工具
Some checks failed
CI / check (push) Failing after 5m21s
CI / build (push) Has been skipped

This commit is contained in:
xfy 2026-06-17 10:39:26 +08:00
parent 476fad27e5
commit 2c1190d8fb
6 changed files with 0 additions and 209 deletions

View File

@ -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)
});
}

View File

@ -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
}

View File

@ -1,2 +0,0 @@
#[cfg(feature = "server")]
pub mod minify_html;

View File

@ -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
}

View File

@ -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
);
}
}
}

View File

@ -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;