Web 框架的 SPA 功能集成
支持 Actix-web、Axum、Salvo 三大框架,通过统一的 spa! 宏将前端静态资源嵌入 Rust 二进制文件并提供 SPA 路由服务。
功能特性
- SPA 路由:未匹配路径自动 fallback 到
index.html - 路径遍历防护:自动拒绝包含
..的恶意路径 - ETag 缓存:基于
SHA256生成 ETag,支持If-None-Match返回304 Not Modified - Gzip 预压缩(可选):启动时预压缩文本资源,自动响应
Accept-Encoding: gzip - Brotli 预压缩(可选):比 Gzip 压缩率高 15-20%,自动响应
Accept-Encoding: br - 内容协商:br > gzip > identity 自动选择最优编码
- 安全头注入(可选):一键注入 X-Content-Type-Options、X-Frame-Options 等安全头
- 自定义错误页面(可选):配置自定义 404 等错误页面
- Vary 头:有压缩变体时自动添加
Vary: Accept-Encoding - charset:text/* 类资源自动追加
; charset=utf-8 - Range 下载(断点续传):支持
Range/If-Range请求,返回206 Partial Content - 文件系统覆盖:启动时自动扫描 assets 目录,同名文件覆盖嵌入资源,支持新增文件、压缩、ETag
使用方法
添加依赖
# actix-web 框架
= { = "*", = ["actix"] }
# axum 框架
= { = "*", = ["axum"] }
# salvo 框架
= { = "*", = ["salvo"] }
# 启用压缩(可选,与框架 feature 组合使用)
= { = "*", = ["actix", "gzip"] }
= { = "*", = ["axum", "gzip", "brotli"] }
# 注意:框架 features 互斥,只能启用一个;gzip 和 brotli 可同时启用
# 由于 `rust-embed` 是过程宏,需要手动添加以下依赖
= "8.9.0"
= "1.0.15"
spa! 宏使用
宏需要用在 main.rs 或者 lib.rs 文件中
/// spa!(名称, 资源路径, 路由前缀, [index 文件名称数组])
/// 路由前缀和 index 文件名数组可选
spa!;
/// 等价于
spa!;
/// 带路由前缀的 Dashboard
spa!;
/// 带扩展配置:用 { } 配置块,在任意简写形式后追加
spa!;
spa!;
/// 完整形式也支持 { } 配置块
spa!;
示例代码
Actix-web 示例
use ;
use spa;
use info;
spa!;
spa!;
async
pub async
Axum 示例(带 Brotli + 安全头)
use spa;
use ;
spa!;
async
async
Salvo 示例
use spa;
use *;
spa!;
spa!;
async
async
API 对比
| 框架 | 宏生成的方法 | 返回类型 |
|---|---|---|
| Actix-web | Struct::spa_service() |
actix_web::Resource |
| Axum | Struct::spa_router() |
axum::Router |
| Salvo | Struct::spa_router() |
salvo::Router |
HTTP 缓存策略
| 资源类型 | Cache-Control | ETag | Gzip | Brotli |
|---|---|---|---|---|
| 静态资源(JS/CSS/图片等) | public, max-age=31536000 |
SHA256 | 可选 | 可选 |
| HTML 页面 | no-cache |
SHA256 | 可选 | 可选 |
| 未匹配路径(SPA fallback) | no-cache |
SHA256 | 可选 | 可选 |
ETag
所有响应自动携带 ETag header。嵌入资源使用 rust_embed 内置的 SHA256 哈希(零额外计算),覆盖文件使用 sha2 crate 计算相同格式的 ETag。客户端发送 If-None-Match 时返回 304 Not Modified,避免重复传输。
压缩
启用 gzip 和/或 brotli feature 后,SpaHandler 在初始化时预压缩所有文本类文件(text/*、application/javascript、application/json、application/xml、application/wasm、image/svg+xml)。仅当压缩后体积更小时才缓存压缩版本。
当两者同时启用时,自动协商最优编码:br > gzip > identity。
# 不压缩:824 bytes
# Brotli 压缩:296 bytes(-64%)
# Gzip 压缩:429 bytes(-48%)
# 条件请求:304 Not Modified
安全头
通过 SpaConfig 配置,支持自定义和预设两种方式:
// 使用预设安全头
spa!;
// 注入以下头:
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN
// X-XSS-Protection: 1; mode=block
// Referrer-Policy: strict-origin-when-cross-origin
// 自定义安全头
spa!;
安全头会应用到所有响应,包括 304 Not Modified 和错误响应。
自定义错误页面
spa!;
当 SPA fallback 也找不到 index 文件时,返回配置的自定义错误页面(支持压缩协商和安全头)。
Range 下载(断点续传)
所有响应自动携带 Accept-Ranges: bytes 头,客户端可通过 Range 请求部分内容:
# 请求前 50 字节
# → 206 Partial Content
# → Content-Range: bytes 0-49/824
# 从第 100 字节到末尾
# 请求最后 10 字节
# If-Range:ETag 匹配时返回 206,不匹配时返回完整内容 200
支持三种 Range 格式:bytes=start-end、bytes=start-(开放结尾)、bytes=-suffix(末尾 N 字节)。If-Range 支持 ETag 匹配。
文件系统覆盖
spa! 宏默认以配置的资源目录(如 "assets")作为覆盖目录。启动时自动扫描该目录:
- 同名文件覆盖:文件系统中的文件优先于嵌入资源
- 新增文件:嵌入资源中不存在的文件也可通过文件系统提供
- 压缩支持:覆盖文件同样支持 gzip/brotli 预压缩
- 无侵入:目录不存在或为空时,行为与纯嵌入模式完全一致
# 部署二进制后,在运行目录创建 assets 目录即可覆盖
# → 日志: SPA override directory: assets
# → 日志: SPA override: loaded index.html
# → 日志: SPA override: 1 files loaded
自定义覆盖目录:
// 默认:覆盖目录 = "assets"(即 spa! 宏配置的资源路径)
spa!;
// 自定义覆盖目录
spa!;
完整示例
查看 examples/ 目录获取可运行的完整项目:
- actix-spa-example — Actix-web 基础用法
- actix-gzip-example — Actix-web + Gzip 压缩
- axum-spa-example — Axum 基础用法
- axum-compression-example — Axum + Brotli + Gzip + 安全头 + 自定义 404 + Range 下载
- axum-override-example — Axum + 文件系统覆盖
- salvo-spa-example — Salvo 基础用法