Друзья, сегодня я решил рассказать вам о способе, который позволит запустить полноценное Vue.js 3 приложение через FastAPI. Для начала разберемся с тем, почему это вообще возможно.
Возможным это становится благодаря тому, что после завершения разработки проекта на Vue.js 3 и его сборки (этим мы займемся чуть дальше), на выходе вы получаете стандартные файлы CSS + HTML + JS, которые останется только запустить через FastAPI.
Сам FastAPI не умеет заниматься рендерингом таких файлов, поэтому нам на помощь придет дружественный этому фреймворку рендер Jinja2. На выходе мы получаем следующую логику:
Сейчас на простом примере я продемонстрирую этот подход.
Первым делом напишем простое Vue 3 приложение (предполагается, что у вас имеются соответствующие знания). В помощь можете использовать мою готовую сборку vue-typescript-tailwind-starter - внутри проекта вы найдете инструкцию по установке и запуску этой сборки.
Я напишу простой секундомер:
<template>
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
<div class="bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">Секундомер</h1>
<div class="text-4xl font-mono mb-4">{{ formattedTime }}</div>
<div class="flex space-x-4">
<button
@click="startTimer"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition"
>
Запустить
</button>
<button
@click="stopTimer"
class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition"
>
Остановить
</button>
<button
@click="resetTimer"
class="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400 transition"
>
Сбросить
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onBeforeUnmount } from "vue";
const time = ref<number>(0);
const timer = ref<ReturnType<typeof setInterval> | null>(null);
const formattedTime = computed(() => {
const minutes = String(Math.floor((time.value % 3600) / 60)).padStart(2, "0");
const seconds = String(time.value % 60).padStart(2, "0");
return `${minutes}:${seconds}`;
});
const startTimer = () => {
if (!timer.value) {
timer.value = setInterval(() => {
time.value++;
}, 1000);
}
};
const stopTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
const resetTimer = () => {
stopTimer();
time.value = 0;
};
onBeforeUnmount(() => {
stopTimer();
});
</script>
<style scoped>
/* Add additional styles if needed */
</style>
Выполним сборку проекта командой:
npm run build
После сборки появится папка dist
с файлами HTML, CSS и JS.
Установим необходимые пакеты:
fastapi==0.110.0
uvicorn==0.27.1
jinja2==3.1.3
python-multipart==0.0.9
aiofiles==23.2.1
Установка через pip:
pip install -r requirements.txt
Подготовим структуру проекта:
project_root/
│
├── main.py
├── requirements.txt
│
├── assets/ # Скомпилированные файлы Vue.js
│ ├── js/
│ ├── css/
│ └── img/
│
└── templates/
└── index.html # Главная страница
Содержимое файла index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
<title>Стартовый проект Vue.js 3</title>
<script type="module" crossorigin src="/assets/index-BEiruxOb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bofh_QRR.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
Тут обратите внимание на формат указания путей к статическим файлам. Это крайне важно, так как на селдующем шаге мы примонтируем папку assets для обработки статических файлов.
Создадим файл main.py
:
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
# Монтируем статические файлы
app.mount("/assets", StaticFiles(directory="assets"), name="assets")
# Настраиваем шаблоны
templates = Jinja2Templates(directory="templates")
@app.get("/")
async def read_root(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8005)
Обратите внимание: здесь мы монтируем папку assets
как статические файлы.
После запуска все должно работать корректно.