在实际开发中,静态资源的引用常常是一个棘手的问题。默认情况下,Rust 构建时不会将这些资源打包进二进制文件中,这意味着如果我们不进行额外处理,在分发程序时需要单独携带这些资源以确保其正常运行。对于 Web 应用而言,通常包括模板文件或前端构建产物等。理想情况下,我们希望部署时只需一个包含所有必要的二进制文件包。
Golang 在这方面就提供了一种官方的解决方案。幸运的是,Rust 也能够通过使用 include_str!() 或 include_bytes!() 宏来简单地嵌入文本或字节数据。然而,当涉及到更复杂的目录结构时,比如前端构建工具如 Vite 打包后产生的如下所示的文件树:
❯ tree dist
dist
├── assets
│ ├── index-9rKZVvFu.js
│ ├── index-BKGZKLCa.css
│ ├── index-CDBrVPCG.js
│ ├── index-DsnN8gUL.js
│ └── settings-CfrUB0_5.js
├── index.html
├── icon.svg
└── vite.svg
为了简化这一过程,我们可以利用 rust-embed crate 来实现整个目录树的嵌入。
使用 rust-embed 实现复杂目录结构的嵌入
添加依赖
首先,在 Cargo.toml 文件中添加 rust-embed 作为依赖:
[dependencies]
rust-embed = "8.4.0"
定义结构体
接下来,定义一个结构体,并使用属性宏指定要嵌入的目录。该目录应相对于项目的根目录:
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "dist/"]
struct Asset;
若需选择性地包含或排除某些文件,可以这样做:
#[derive(RustEmbed)]
#[folder = "dist/"]
#[include = "*.html"]
#[include = "images/*"]
#[exclude = "*.txt"]
struct Asset;
获取文件内容
现在,你可以直接使用 Asset::get 方法获取文件内容。例如,获取 index.html 文件的内容:
let index_html = Asset::get("index.html").unwrap();
此外,rust-embed 提供了迭代器功能,允许你遍历所有嵌入的文件:
for file in Asset::iter() {
println!("{}", file.as_ref());
}
前端与后端路由规划
为了确保前端资源与后端API路由之间不会发生冲突,我们应该明确区分两者。一种常见的做法是将所有以 /api/ 开头的路径视为后端API路由,而其他未特别指明的路径则默认为前端路由。
在后端框架中,我们需要实现404错误处理逻辑,以便正确显示前端资源。某些框架,如 actix-web 和 axum,提供了专门的接口用于处理静态文件和全局404响应。
处理前端路由
如果前端项目(如 Vue 或 React)使用了前端路由,则需要在后端模仿类似 Nginx 的代理行为。例如,当使用 Vue Router 时,Nginx 配置会尝试匹配具体的文件或目录,如果找不到,则回退到 index.html,让前端路由接管。
同样的逻辑可以在 Rust 后端框架中实现:
async fn handler_404(uri: &Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/').to_string();
// 如果路径以 "dist/" 开头,移除前缀
if path.starts_with("dist/") {
let new_path = path.replace("dist/", "");
// 如果路径为空,默认指向 index.html
let path = if new_path.is_empty() { "index.html" } else { &new_path };
StaticFile(path)
} else {
// 对于非前端路由,返回404或其他自定义响应
// 这里假设所有的前端路由都由 index.html 处理
StaticFile("index.html")
}
}
静态文件处理器
最后,定义一个静态文件处理器,它会根据请求路径查找对应的嵌入资源,并设置正确的 MIME 类型返回给客户端:
use axum::{
body::Body,
http::{header, HeaderValue, Response, StatusCode, Uri},
response::IntoResponse,
};
use mime_guess::Mime;
pub struct StaticFile<T>(pub T);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
fn into_response(self) -> Response<Body> {
let path = self.0.into();
// 直接查找是否存在 path 文件或者目录,因为入口只有 index.html 这里只考虑文件即可。
match Asset::get(&path) {
Some(content) => {
// 返回文件内容,需要注意的时返回的 Content Type 类型,
// 最好根据不同的文件类型返回不同的 Content Type 类型。
let mime = mime_guess::from_path(&path).first_or_octet_stream();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap())
.body(Body::from(content.data))
.unwrap()
}
None => {
// 如果找不到,表示这个路由为前端的路由,默认前端路由的入口都是 index.html
// 前端代码会自动处理,我们只需要指向 index.html 入口即可。
// 直接转回index.html 实现前端路由的跳转
let content = Asset::get("index.html").unwrap();
let mime = mime_guess::from_path("index.html").first_or_octet_stream();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap())
.body(Body::from(content.data))
.unwrap()
}
}
}
}
通过这种方式,我们不仅实现了静态资源的无缝嵌入,还确保了前后端路由的和谐共存,使得应用更加易于部署和维护。