将单页应用(SPA)嵌入到AXUM应用中

本章我们将讨论如何把单页应用(SPA)嵌入到AXUM二进制文件中。

目标

我们的目标是把一个使用VUE3构建的SPA嵌入到AXUM二进制文件中。

[dependencies]
tokio = { version = "1", features = ["full"] }
axum = { version = "0.7" }
serde = { version = "1", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
rust-embed = "8.5"
mime_guess = "2"

Rust 代码

资源模块

// src/asset.rs

use axum::{
    http::{header, StatusCode, Uri},
    response::{Html, IntoResponse, Response},
};
use rust_embed::Embed;

pub const INDEX_HTML: &'static str = "index.html";

#[derive(Embed)]
#[folder = "dist/"]
pub struct Assets;

pub async fn static_handler(uri: Uri) -> impl IntoResponse {
    let path = uri.path().trim_start_matches("/");

    if path.is_empty() || path == INDEX_HTML {
        return index_html().await;
    }

    match Assets::get(path) {
        Some(content) => {
            let mime = mime_guess::from_path(path).first_or_octet_stream();

            ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
        }
        None => {
            if path.contains('.') {
                return not_found().await;
            }
            index_html().await
        }
    }
}

pub async fn index_html() -> Response {
    match Assets::get(INDEX_HTML) {
        Some(content) => Html(content.data).into_response(),
        None => not_found().await,
    }
}

pub async fn not_found() -> Response {
    (StatusCode::NOT_FOUND, "404").into_response()
}

资源结构体

#[derive(Embed)]
#[folder = "dist/"]
pub struct Assets;

我们的资源结构体将把 dist/ 目录里面的资源嵌入到二进制文件中。

注意:为了避免编译错误,请手动在项目根目录下创建 dist 目录。等编写好SPA之后,会自动生成该目录和资源文件

静态文件处理

pub async fn static_handler(uri: Uri) ...
  • 通过请求的URI判断是否是首页,如果是的话,渲染首页:

    if path.is_empty() || path == INDEX_HTML {
        return index_html().await;
    }
    
  • 如果不是首页,如果资源文件不存在,渲染not_found();否则,猜测其MIME,然后进行渲染

    match Assets::get(path) {
        Some(content) => {
            let mime = mime_guess::from_path(path).first_or_octet_stream();
    
            ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
        }
        None => {
            if path.contains('.') {
                return not_found().await;
            }
            index_html().await
        }
    }
    

首页渲染

pub async fn index_html() -> Response {
    match Assets::get(INDEX_HTML) {
        Some(content) => Html(content.data).into_response(),
        None => not_found().await,
    }
}
  • 如果静态资源 index.html 存在,则返回该文件的内容
  • 否则,渲染 not_found()

404错误

pub async fn not_found() -> Response {
    (StatusCode::NOT_FOUND, "404").into_response()
}
  • 返回 StatusCode::NOT_FOUND 响应
  • 内容为 404。你可以写详细点,比如:你请求的资源不存在
#[tokio::main]
async fn main() {
    let tcp_listener = TcpListener::bind("0.0.0.0:9527").await.unwrap();
    let app = Router::new()
        .route("/now", get(now_handler))
        .fallback(asset::static_handler);

    axum::serve(tcp_listener, app).await.unwrap();
}
  • 绑定了 /路由,交由 now_handler来处理
  • 其他路由,通过 asset::static_handler处理

路由处理函数

async fn now_handler() -> String {
    Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
}

知识简单的返回当前时间。

前端

创建前端项目

# 创建项目
yarn create vite spa-ui --template vue-ts 
cd spa-ui 

# 安装依赖
yarn

# 安装tailwindcss
# 参见官方文档 https://tailwindcss.com/docs/guides/vite#vue

# 安装dayjs
yarn add dayjs
<!-- spa-ui/src/components/Now.vue -->

<script setup lang="ts">
import dayjs from "dayjs";
import { onMounted, ref } from "vue";

const remoteNow = ref<string>("正在获取");
const loadData = () => {
  fetch("/now", {
    method: "GET",
  })
    .then((r) => r.text())
    .then((r) => (remoteNow.value = r));
};

onMounted(() => {
  loadData();
});
</script>

<template>
  <ul>
    <li>服务器时间:{{ remoteNow }}</li>
    <li>客户端时间:{{ dayjs().format("YYYY-MM-DD HH:mm:ss") }}</li>
  </ul>
</template>

渲染组件

# 在spa-ui目录下进行
rm -rf ../dist && yarn build --outDir ../dist

至此,我们的SPA已经嵌入的AXUM二进制文件中了,只需要发布、部署一个 cargo build --release 生成的二进制文件即可,无需额外发布、部署SPA页面和文件。

本章代码位于spa目录。

要查看完整内容,请先登录