Jan 03, 2025
3 min read
Rust,

在 Rust 中嵌入静态资源:实现单文件分发

实现单文件分发你的程序

在实际开发中,静态资源的引用常常是一个棘手的问题。默认情况下,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-webaxum,提供了专门的接口用于处理静态文件和全局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()
            }
        }
    }
}

通过这种方式,我们不仅实现了静态资源的无缝嵌入,还确保了前后端路由的和谐共存,使得应用更加易于部署和维护。