将单页应用(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/
目录里面的资源嵌入到二进制文件中。
静态文件处理
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错误
- 返回
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/src/App.vue -->
<script setup lang="ts">
import Now from "./components/Now.vue";
</script>
<template>
<div
class="my-6 w-96 mx-auto border rounded-md shadow-md bg-gradient-to-br from-sky-50 to-sky-100"
>
<Now class="space-y-3 p-6" />
</div>
</template>
编译
# 在spa-ui目录下进行
rm -rf ../dist && yarn build --outDir ../dist
至此,我们的SPA已经嵌入的AXUM二进制文件中了,只需要发布、部署一个 cargo build --release
生成的二进制文件即可,无需额外发布、部署SPA页面和文件。
本章代码位于spa
目录。