📢 专栏导语
欢迎来到《企业级Python自动化运维实战》专栏的第六篇!经过前面几个版本的迭代,我们的系统已经从"一个能跑的脚本"进化成了"一个规范的后端服务"——有 FastAPI 接口、有 SQLite 持久化、还有 Swagger 自动文档。
我们要完成从"后端自动化工具"到"前后端分离的 SRE 巡检管理平台"的关键一跃:给系统装上可视化界面,让任何人打开浏览器、点两下鼠标就能完成巡检并看懂趋势。
本章你能学到:
- Vue3 + Vite 项目的搭建与目录组织方式
- Axios 封装,以及与 FastAPI 的联调
- CORS 跨域的原理与解决办法
- 组件化开发:父组件传值、子组件 Props 接收
- ECharts 异步数据渲染的坑与解法
1.V2.4版本整体架构
先建立全局认知,看清楚数据是怎么一层层流动起来的:
浏览器 | Vue3 ← 负责"看":页面展示与交互 | HTTP请求(Axios) | FastAPI ← 负责"做":业务逻辑与数据处理 | ----------------------- | | SQLite 巡检任务 (历史数据存储) (真正去连服务器采集)这张图体现了前后端分离的核心思想:前端只管展示和交互,后端只管数据和逻辑,两边通过 HTTP 接口(API)对话。
这么做的好处是职责清晰、各自独立。方便后续把前端换成 React、或者想给系统加一个手机 App,后端代码是不需要改动的。
2.技术选型
| 技术 | 作用 | 为什么选它 |
|---|---|---|
| Vue3 | 页面框架 | 渐进式框架,上手平缓,中文文档完善,对 Python 转前端的同学非常友好 |
| Vite | 项目构建 | Vue 官方推荐的新一代构建工具,启动速度远快于老牌 Webpack,改代码后热更新几乎无延迟 |
| Axios | 调用API | 最主流的 HTTP 请求库,支持拦截器、超时控制,比浏览器原生 fetch 更好用 |
| Element Plus | UI组件 | 开箱即用的企业级组件库,按钮、卡片、标签、弹窗直接拿来用,不用自己写 CSS |
| ECharts | 数据图表 | 国产开源可视化库,折线图/柱状图/仪表盘应有尽有,中文文档齐全 |
3.创建Vue项目
前置条件:安装 Node.js 并配置好环境变量(建议 Node 16 及以上版本),详细安装教程本文不展开。
先确认环境没问题,终端执行:
node-vnpm-v两条命令都能输出版本号,说明安装成功。
接着在项目根目录(也就是后端api/文件夹所在的那一层)打开终端,执行:
npmcreate vite@latest server-inspection-web执行后会有几个交互式选项:
- 提示是否安装
create-vite,输入y回车; - 选择框架:方向键选择
Vue; - 选择语言:选择
JavaScript(先不碰 TypeScript,降低上手门槛)。
看到Done. Now run:提示说明脚手架创建完成。进入目录、安装依赖、启动:
cdserver-inspection-webnpminstallnpmrun dev访问http://localhost:5173就能看到 Vue 的欢迎页面。
💡 至此前端和后端是平级的两个目录,各跑各的服务:后端占 8000 端口,前端占 5173 端口,互不影响。
4.安装项目依赖
npminstallaxios element-plus echarts这三个依赖分别对应上表中的"调用API"“UI组件”“数据图表”。装完打开package.json,能在dependencies里看到它们及版本号。
💡 如果
npm install很慢,可临时切国内镜像:npm config set registry https://registry.npmmirror.com
5.调整目录结构
脚手架生成的文件里有一堆示例内容(如HelloWorld.vue、style.css),我们先把目录整理成项目真正需要的样子。前端配置统一放在server-inspection-web目录下:
server-inspection-web ├── index.html ← 页面入口 HTML ├── package.json ← 依赖清单与启动脚本 ├── vite.config.js ← Vite 配置(后续配代理会用到) └── src ├── api │ └── request.js ← axios 封装,统一负责和 FastAPI 通信 ├── components │ ├── ServerCard.vue ← 展示单台服务器的卡片 │ └── TrendChart.vue ← ECharts 历史趋势图 ├── views │ └── Dashboard.vue ← 首页/控制中心,巡检主页面 ├── App.vue ← 根组件,所有页面的容器 └── main.js ← 程序入口,启动整个 Vue每个目录的职责:
api/:所有和后端打交道的请求都放这里,页面组件不直接写 URL,方便统一维护;components/:可复用的小组件,一张卡片、一个图表,哪里需要就往哪里放;views/:页面级组件,一个 view 通常对应一个完整页面。
💡 后面页面变多时,还会加入
router/目录做路由跳转(比如独立的 History 历史页)。当前阶段先聚焦首页,保持最小可用结构。
完整请求链路——从浏览器打开页面到数据渲染出来,中间发生了什么:
浏览器 ↓ index.html ← 浏览器加载的唯一 HTML 文件 ↓ main.js ← 启动整个 Vue 应用 ↓ App.vue ← 根组件 ↓ Dashboard.vue ← 负责巡检页面的业务逻辑 ↓ ↓ ↓ ↓ request.js ServerCard.vue ← 负责显示一台服务器 (负责和 FastAPI 通信) ↓ Axios ↓ http://127.0.0.1:8000 ↓ FastAPI ↓ SQLite理解这条链路很重要:以后页面出问题,你就知道该从哪一段排查——是请求没发出去(Axios)、后端没返回(FastAPI),还是数据没渲染(Vue)。
6.配置前端启动主入口
src/main.js是整个前端程序的起点,相当于后端的if __name__ == "__main__"。
import{createApp}from"vue";importElementPlusfrom"element-plus";import"element-plus/dist/index.css";importAppfrom"./App.vue";constapp=createApp(App);app.use(ElementPlus);app.mount("#app");逐行拆解:
| 代码 | 作用 |
|---|---|
import { createApp } from "vue" | 从 vue 中导入创建应用的工厂函数 |
import ElementPlus from "element-plus" | 导入 Element Plus 组件库 |
import "element-plus/dist/index.css" | 只加载样式文件,不接收任何导出内容 |
createApp(App) | 以 App.vue 为根组件创建 Vue 应用实例 |
app.use(ElementPlus) | 全局注册组件库,之后所有子组件都能直接用<el-button>,不用在每个 .vue 里重复 import |
app.mount("#app") | 把 Vue 应用挂载到 index.html 中id="app"的 div 上 |
整体流程:
main.js ↓ 启动 Vue 实例 ↓ 加载 App.vue ↓ 挂载到 index.html 的 #app 节点 ↓ 浏览器看到完整页面知识点:JavaScript 的导入和 Python 不太一样
Python 里我们习惯import xxx或from xxx import yyy。JS 的导出分两种,导入写法也随之不同:
① 命名导出(Named Export)
导出方用export const指定名字,一个文件可以有多个。导入时必须用大括号指定要导入哪个:
exportconstname="Tom";exportfunctionsayHi(){}// 导入import{name,sayHi}from"./demo.js";上面import { createApp } from "vue"就是命名导出——createApp是 vue 包明确导出的名字,不能随便改。
② 默认导出(Default Export)
一个文件只能有一个默认导出。导入时不需要大括号,名字可以自己起:
exportdefault{}// 下面两种写法都合法,名字随便取importAppfrom"./App.vue";importMyRootfrom"./App.vue";那么问题来了:App.vue 里并没有写export default,为什么能用默认导入?
因为 Vite/Vue 编译器会把.vue单文件组件自动处理成一个可被导入的组件模块,等价于帮你做了默认导出。所以我们写import App from './App.vue'即可,App只是我们在当前文件里给它起的本地名字。
一句话记忆:有名字用大括号,没名字自己起名。
7.配置Axios请求解决跨域问题
7.1后端调用
Axios 是一个 JavaScript 的 HTTP 请求库,作用是让前端能够访问后端 API,专门负责前端和 FastAPI 之间的 HTTP 通信。
我们创建一个专门用于访问 FastAPI 后端接口的 HTTP 请求对象,之后 Vue 页面不用每次写完整地址,只需要调用这个封装好的request即可。
src/api/request.js
importaxiosfrom"axios";constrequest=axios.create({baseURL:"http://127.0.0.1:8000/api",timeout:5000});exportdefaultrequest;两个参数的意思:
baseURL:接口地址的公共前缀。以后写request.get("/history"),实际请求的就是http://127.0.0.1:8000/api/history;timeout:超时时间(毫秒),超过 5 秒没响应就报错,避免页面一直转圈。
💡为什么要把地址抽出来?如果哪天后端换了端口或部署到服务器,你只需要改这一个文件,而不用去几十个页面里一个个改 URL。这就是"封装"的价值。
7.2跨域问题
前端 Vue 跑在localhost:5173,后端 FastAPI 跑在localhost:8000,端口不同,这就触发了浏览器的同源策略。
什么是同源?协议、域名、端口三者完全相同才算同源。我们的协议都是 http、域名都是 localhost,但端口一个是 5173 一个是 8000,所以属于跨域。
浏览器出于安全考虑,默认会拦截跨域请求,控制台会看到类似这样的报错:
Access to XMLHttpRequest has been blocked by CORS policy解决办法是在后端开启 CORS(跨域资源共享),明确告诉浏览器"我允许这个来源访问我"。修改api/main.py:
fromfastapi.middleware.corsimportCORSMiddleware app.add_middleware(CORSMiddleware,allow_origins=["http://127.0.0.1:5173","http://localhost:5173"],allow_credentials=True,allow_methods=["*"],allow_headers=["*"])app.add_middleware相当于给 FastAPI 安装一个"中间处理程序",它可以在请求进入接口之前、响应返回浏览器之前做一些统一处理。跨域校验就放在这一层。
参数说明:
| 参数 | 作用 |
|---|---|
allow_origins | 允许哪些来源访问。⚠️ 必须写完整协议+域名+端口,127.0.0.1和localhost在浏览器眼里是两个不同的源,所以两个都写上 |
allow_credentials | 允许携带凭证信息,例如 Cookie |
allow_methods | 允许所有 HTTP 请求方法(GET/POST/PUT/DELETE…) |
allow_headers | 允许前端携带各种请求头 |
💡 生产环境不要把
allow_origins写成["*"],应当只放行你自己的前端域名,否则任何人都能调你的接口。
8.创建首页Dashboard
这是我们平台的"首页/控制中心",属于业务逻辑层 + 页面层,负责:
1. 获取后端的服务器历史数据 2. 保存这些数据 3. 把数据交给 ServerCard 显示 4. 用户点击"立即巡检"时通知后端开始巡检 5. 页面打开时自动加载历史数据先理解.vue文件的三段式结构,一个.vue文件(单文件组件,SFC)可以理解为:
template → 页面长什么样(HTML) script setup → 页面怎么工作(JS 逻辑) style → 页面什么样式(CSS)src/views/Dashboard.vue
<template> <div> <h2>服务器巡检平台</h2> <!-- 点击后调用 script 中定义的 check 方法 --> <el-button type="primary" @click="check"> 立即巡检 </el-button> <div class="server-list"> <!-- 遍历 servers,每台服务器渲染一张卡片 --> <ServerCard v-for="item in servers" :key="item.server" :data="item" /> </div> </div> </template> <script setup> import { ref, onMounted } from "vue"; import ServerCard from "../components/ServerCard.vue"; import request from "../api/request"; const servers = ref([]); async function loadData() { const res = await request.get("/history"); servers.value = res.data; } async function check() { await request.post("/check"); alert("巡检任务已启动"); } // 页面加载完成后自动拉取一次历史数据 onMounted(() => { loadData(); }); </script>几个关键点:
①ref()是响应式的核心
ref([])会把数据包装成响应式对象。可以理解成:
servers={value:[]}所以必须用servers.value才能拿到真正的数据,赋值时也要写servers.value = xxx。这样一旦数据变化,页面会自动跟着刷新——不需要手动操作 DOM。
②async / await处理异步
网络请求是需要时间的。await request.get(...)表示"等这个请求返回结果再往下走",否则servers.value拿到的会是 undefined。
③res.data而不是res
Axios 的返回值res里包含data、status、headers等一堆信息,后端真正返回的业务数据在res.data里。
④onMounted生命周期钩子
页面(组件)挂载完成后自动执行里面的函数,适合放"打开页面就要加载"的逻辑。
⑤ 模板里的两个 Vue 语法
v-for="item in servers":循环渲染,有几台服务器就渲染几张卡片;:key="item.server":给每个循环项一个唯一标识,帮助 Vue 高效更新列表;:data="item":冒号是 Vue 的动态绑定语法。加了冒号,Vue 会去找item这个变量的实际值;不加冒号,传过去的就是字符串"item"本身。
数据流向:
Dashboard.vue ↓ “我要历史数据” ↓ request.js ↓ Axios ↓ FastAPI💡
alert()是浏览器原生的弹窗,能用但比较简陋。想更专业可以换成 Element Plus 的消息提示:import{ElMessage}from"element-plus";ElMessage.success("巡检任务已启动");
9.创建服务器卡片组件
这个组件负责单台服务器的展示。把它单独拆出来的好处是:Dashboard 只关心"有哪些服务器",卡片自己关心"一台服务器怎么显示",职责分离,以后改样式不影响主页。
src/components/ServerCard.vue
<template> <el-card> <h2>{{ data.server }}</h2> <p>CPU: {{ data.cpu }}%</p> <p>Memory: {{ data.memory }}%</p> <p>Disk: {{ data.disk }}%</p> <!-- 根据状态显示不同颜色的标签 --> <el-tag :type="data.status === 'success' ? 'success' : 'danger'"> {{ data.status }} </el-tag> </el-card> </template> <script setup> // 声明当前组件接收父组件传过来的数据 defineProps({ data: Object }); </script>知识点:
{{ }}插值表达式:把data.server的值渲染到页面上。数据变了,显示自动变;defineProps:声明"我要接收哪些外部数据"。这里声明了一个名为data、类型为Object的属性。父组件通过:data="item"传进来,子组件直接用data.xxx访问;- 三元表达式:
条件 ? 满足时的值 : 不满足时的值。这里表示状态是success就显示绿色标签,否则显示红色标签。
💡 这就是 Vue 的父子组件通信:父组件用
:绑定传值,子组件用defineProps接收。数据只能从父流向子(单向数据流),子组件不要直接修改 props。
10.修改App.vue
App.vue是根组件,所有页面都挂在它下面。当前阶段我们直接把首页放进来。
src/App.vue
<template> <Dashboard/> </template> <script setup> import Dashboard from './views/Dashboard.vue'; </script>完成后,启动项目验证效果:
# 启动后端uvicorn api.main:app--reload# 启动前端(另开一个终端,在 server-inspection-web 目录下)npmrun dev浏览器打开 http://localhost:5173,就能看到服务器巡检平台的页面,展示每台服务器的 CPU、内存、磁盘和状态标签。
⚠️ 注意:后端必须先启动,否则页面能打开但数据加载不出来(控制台会报网络错误)。
11.增加历史趋势页面
目前只能查看每次巡检的结果,数据对比不够直观。我们增加趋势页面观察走势,提前发现问题——趋势比单点更有价值,CPU 从 30% 涨到 80% 的过程,比一次 80% 的快照更值得警惕。
思路是:先写后端接口,再做前端呈现。
11.1新增后端趋势接口
修改api/history.py,在已有导入的基础上补充(下面代码块中前几行为该文件需要的上下文,若已存在则无需重复添加):
fromfastapiimportAPIRouter,Queryfromdatetimeimportdatetime,timedelta router=APIRouter()@router.get("/history/{server_name}/trend")defserver_trend(server_name:str,days:int=Query(7,ge=1,le=90,description="查询最近N天数据")):db=get_db()start=datetime.now()-timedelta(days=days)records=(db.query(InspectionRecord).filter(InspectionRecord.server_name==server_name,InspectionRecord.create_time>=start).order_by(InspectionRecord.create_time).all())db.close()return{"server":server_name,"time":[r.create_time.strftime("%m-%d %H:%M")forrinrecords],"cpu":[r.cpuforrinrecords],"memory":[r.memoryforrinrecords],"disk":[r.diskforrinrecords]}不要忘记在主程序中注册这个路由(若前面章节已注册过则跳过)。
逐段解析:
| 代码 | 说明 |
|---|---|
from datetime import datetime, timedelta | timedelta是 datetime 模块里用于表示时间差的类,做日期加减、时间差计算的核心工具 |
days: int = Query(7, ge=1, le=90) | Query用来处理 URL 中?days=7这类查询参数。ge=1、le=90表示最小 1、最大 90,超出范围 FastAPI 会自动返回 422 错误——参数校验不用自己写 if 判断 |
datetime.now() - timedelta(days=days) | 计算起始时间,比如 7 天前 |
.filter(...) | 两个条件:服务器名匹配 + 记录时间晚于起始时间 |
.order_by(...) | 按时间由早到晚排序,这样画出来的折线图才不会左右颠倒 |
| 列表推导式 | [r.cpu for r in records]等价于 for 循环把所有记录的 cpu 值收集成一个列表,ECharts 要的就是这种格式 |
启动后端后可以访问 http://127.0.0.1:8000/docs 测试这个接口,传入服务器名和天数,确认能返回三组数组。
11.2创建ECharts图表组件
ECharts 是一款基于 JavaScript 的开源数据可视化图表库。创建src/components/TrendChart.vue:
<template> <div ref="chartRef" style="height: 300px; width: 100%;" /> </template> <script setup> import { onMounted, onBeforeUnmount, ref, watch } from "vue"; import * as echarts from "echarts"; const props = defineProps({ title: { type: String, default: "趋势图" }, times: { type: Array, default: () => [] }, values: { type: Array, default: () => [] } }); const chartRef = ref(null); let myChart = null; function renderChart() { if (!myChart) return; myChart.setOption({ title: { text: props.title }, tooltip: { trigger: "axis" }, xAxis: { type: "category", data: props.times }, yAxis: { type: "value", min: 0, max: 100 }, series: [{ data: props.values, type: "line", smooth: true }] }, true); // true 表示不合并旧数据,直接替换 } onMounted(() => { if (chartRef.value) { myChart = echarts.init(chartRef.value); renderChart(); } }); // 关键:数据是异步请求回来的,onMounted 时还是空数组 // 所以要用 watch 监听 props 变化,数据到了再重画 watch( () => [props.times, props.values], () => { renderChart(); }, { deep: true } ); // 窗口缩放时图表自适应 const onResize = () => myChart && myChart.resize(); window.addEventListener("resize", onResize); onBeforeUnmount(() => { window.removeEventListener("resize", onResize); if (myChart) { myChart.dispose(); myChart = null; } }); </script>三个最容易踩的坑:
① 为什么需要ref="chartRef"?
ECharts 需要一个真实的 DOM 元素来挂载图表。ref让我们能在 JS 里拿到这个 div,再交给echarts.init()。
② 为什么必须用watch监听?(最重要)
这是新手最常困惑的地方。数据是后端异步请求回来的,而onMounted执行的时候,请求可能还没完成,props.times还是空数组——这时候画出来就是一张空图。
所以必须用watch监听数据变化,等数据真正到了再重画一次。{ deep: true }表示深度监听,数组内部元素变化也能感知到。
③ 为什么要在onBeforeUnmount里销毁?
ECharts 实例和 window 上的 resize 监听不会自动消失。如果不清理,组件反复切换会造成内存泄漏。这是写出健壮前端代码的习惯。
11.3改造Dashboard整合趋势图
修改src/views/Dashboard.vue,把趋势图区域加进来:
<template> <div> <h2>服务器巡检平台</h2> <el-button type="primary" @click="check">立即巡检</el-button> <!-- 趋势图区域:有数据才显示 --> <div v-if="trend.time.length" style="margin-top: 20px;"> <h3>{{ selectedServer }} 历史趋势</h3> <div style="display: flex; flex-wrap: wrap;"> <div style="width: 33%;"> <TrendChart title="CPU" :times="trend.time" :values="trend.cpu" /> </div> <div style="width: 33%;"> <TrendChart title="内存" :times="trend.time" :values="trend.memory" /> </div> <div style="width: 33%;"> <TrendChart title="磁盘" :times="trend.time" :values="trend.disk" /> </div> </div> </div> <!-- 没数据时的提示 --> <div v-else style="margin-top: 20px; color: #999;"> 暂无趋势数据,请先执行巡检 </div> <div class="server-list"> <ServerCard v-for="item in servers" :key="item.server" :data="item" /> </div> </div> </template> <script setup> import { ref, onMounted } from "vue"; import ServerCard from "../components/ServerCard.vue"; import request from "../api/request"; import TrendChart from "../components/TrendChart.vue"; const servers = ref([]); const selectedServer = ref(""); const trend = ref({ time: [], cpu: [], memory: [], disk: [] }); async function loadData() { const res = await request.get("/history"); servers.value = res.data; // 默认展示第一台服务器的趋势 if (servers.value.length > 0) { selectedServer.value = servers.value[0].server; await loadTrend(selectedServer.value); } } async function loadTrend(serverName) { const res = await request.get(`/history/${serverName}/trend`); trend.value = res.data; } async function check() { await request.post("/check"); alert("巡检任务已启动"); } onMounted(() => { loadData(); }); </script>指标历史趋势显示效果:新增了什么:
selectedServer:记录当前查看哪台服务器的趋势,默认取第一台;trend:保存趋势数据,包含time、cpu、memory、disk四个数组;loadTrend():单独封装一个函数拉取趋势数据,await保证拿到数据后再往下走;v-if / v-else:有数据才渲染图表,否则显示提示文案,避免页面上出现三张空图。
💡 注意
loadTrend里用的是模板字符串(反引号`),/history/${serverName}/trend会把变量名替换成实际值,比字符串拼接"/history/" + serverName + "/trend"清爽得多。
12.常见问题排查
配置过程中遇到问题,先对照这里排查,能省下大量搜索时间:
| 现象 | 原因与解决办法 |
|---|---|
npm run dev提示端口被占用 | 5173 端口被其他程序占用,终端输入y让 Vite 自动换一个端口,或修改vite.config.js指定 port |
| 页面能打开但数据加载不出来 | 后端没启动,或后端启动报错。先看终端里 uvicorn 是否正常运行,再访问 8000 端口的 /docs 验证接口 |
| 控制台报 CORS policy 错误 | 检查allow_origins是否包含你实际访问的地址——用127.0.0.1:5173打开页面就必须在列表里有它,写localhost不算同一个源 |
报401/403 | 检查 Gitee 仓库是否开源、Token 是否正确(这是上一章图床的坑,别混淆) |
报422 Unprocessable Entity | 接口参数不合法,比如趋势接口的days传了 0 或 100(超出 1~90 范围) |
| ECharts 图表一片空白 | ① 容器 div 没设置高度(必须有明确的 height);② 数据还没请求回来就初始化了(需要 watch);③ 父容器display:none时初始化会导致尺寸为 0 |
| 提示组件找不到 / 编译报错 | 检查组件名拼写与文件名、import 路径是否一致,components别写成component,路径区分大小写 |
| 改了代码页面没变化 | Vite 是热更新的,若没生效,检查终端是否报错,或手动刷新浏览器(Ctrl+F5 强制刷新) |
npm install卡住不动 | 切换国内镜像:npm config set registry https://registry.npmmirror.com |
| 巡检后页面数据不刷新 | 当前是手动刷新。可以在check()里加setTimeout(() => loadData(), 3000),等巡检完成后自动重载 |
| 图片在博客里显示不出来 | 本文图片用的是本地绝对路径,发布到博客前需要先上传图床(可参考本系列的图床搭建章节) |
13.本章小结
回顾这一章,我们完成了平台"从后端到全栈"的关键一步:
搭建 Vue3 项目(Vite) ↓ 封装 Axios,打通前后端通信 ↓ 后端开启 CORS,解决跨域 ↓ 开发 Dashboard 首页 + ServerCard 卡片组件 ↓ 新增趋势接口 + ECharts 可视化现在我们的巡检平台已经是一个真正可用的前后端分离系统了:打开浏览器就能看到所有服务器的状态,点一下按钮就能触发巡检,还能看到 CPU、内存、磁盘的历史走势。
阶段性成果:从"命令行脚本" → “后端 API 服务” → “可视化管理平台”,这条路径也是很多企业内部工具的标准演进路线。
下一步可以继续演进的方向:
我们将进入另一个大版本,接入企业真实使用的监控方式,更加贴近企业实际情况
- 接入Prometheus基础监控:实时采集服务,服务器的监控 数据;
- Grafana可视化:将采集的数据以大盘形式呈现;
- AlertManager告警:到达阈值后触发告警;
- SRE自动化平台融合:融入现在的平台。
如果你在跟着做的过程中遇到了本文没覆盖的问题,欢迎在评论区留言交流。觉得有帮助的话,点个赞收藏一下吧~