【Wit】pure-admin后台管理系统前端与FastAPI后端联调通信实例
在开始之前,先贴一下pure-admin的目录架构说明:
├── .github # GitHub 配置文件
│ ├── ISSUE_TEMPLATE # 问题提交参考模板
│ ├── workflows git # 工作流
├── .husky # 代码提交前校验配置文件
├── .vscode # IDE 工具推荐配置文件
│ │ ├── extensions.json # 一键安装平台推荐的 vscode 插件
│ │ ├── settings.json # 设置扩展程序或 vscode 编辑器的一些属性
│ │ ├── vue3.0.code-snippets # vue3.0 代码片段
│ │ └── vue3.2.code-snippets # vue3.2 代码片段
│ │ └── vue3.3.code-snippets # vue3.3 代码片段
├── build # 构建工具
│ │ ├── cdn.ts # 打包时采用 cdn 模式
│ │ ├── compress.ts # 打包时启用 gzip 压缩或 brotli 压缩
│ │ ├── info.ts # 输出打包信息(大小、用时)
│ │ ├── optimize.ts # vite 依赖预构建配置项
│ │ ├── plugins.ts # vite 相关插件存放处
│ │ ├── utils.ts # 构建工具常用方法抽离
├── locales # 国际化文件存放处
│ │ ├── en.yaml # 英文配置
│ │ ├── zh-CN.yaml # 中文配置
├── mock # mock 模拟后台数据
│ │ ├── asyncRoutes.ts # 模拟后台返回动态路由
│ │ ├── ...
├── node_modules # 模块依赖
├── public # 静态资源
│ │ ├── audio # 音频文件
│ │ ├── html # 静态 iframe 页面
│ │ ├── wasm # wasm 文件以及胶水代码
│ │ ├── favicon.ico # favicon
│ │ ├── logo.svg # logo
│ │ ├── platform-config.json # 全局配置文件(打包后修改也可生效)
├── src
│ ├── api # 接口请求统一管理
│ ├── assets # 字体、图片等静态资源
│ ├── components # 自定义通用组件
│ │ ├── ReAnimateSelector # [animate.css](https://animate.style/) 选择器组件
│ │ ├── ReAuth # 按钮级别权限组件(根据路由meta中的auths字段进行判断)
│ │ ├── ReBarcode # 条形码组件
│ │ ├── ReCol # 封装 element-plus 的 el-col 组件
│ │ ├── ReCountTo # 数字动画组件
│ │ ├── ReCropper # 图片裁剪组件
│ │ ├── ReCropperPreview # 图片裁剪预览组件
│ │ ├── ReDialog # 基于 element-plus 中 el-dialog 组件开发的函数式弹框
│ │ ├── ReFlicker # 圆点、方形闪烁动画组件
│ │ ├── ReFlop # 时间翻牌组件
│ │ ├── ReFlowChart # LogicFlow 流程图组件
│ │ ├── ReIcon # 图标组件
│ │ ├── ReImageVerify # 图形验证码组件
│ │ ├── ReMap # 高德地图组件
│ │ ├── RePerms # 按钮级别权限组件(根据登录接口返回的permissions字段进行判断)
│ │ ├── RePureTableBar # 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
│ │ ├── ReQrcode # 二维码组件
│ │ ├── ReSeamlessScroll # 无缝滚动组件
│ │ ├── ReSegmented # 分段控制器组件
│ │ ├── ReSelector # 选择器组件
│ │ ├── ReSplitPane # 切割面板组件
│ │ ├── ReText # 支持 Tooltip 提示的文本省略组件
│ │ ├── ReTreeLine # 树形连接线组件(基于element-plus)
│ │ ├── ReTypeit # 打字机效果组件
│ │ ├── ReVxeTableBar # 配合 vxe-table 实现快速便捷的表格操作
│ ├── config # 获取平台动态全局配置
│ ├── directives # 自定义指令
│ │ ├── auth # 按钮级别权限指令(根据路由meta中的auths字段进行判断)
│ │ ├── copy # 文本复制指令(默认双击复制)
│ │ ├── longpress # 长按指令
│ │ ├── optimize # 防抖、节流指令
│ │ ├── perms # 按钮级别权限指令(根据登录接口返回的permissions字段进行判断)
│ │ ├── ripple # 水波纹效果指令
│ ├── layout # 主要页面布局
│ ├── plugins # 处理一些库或插件,导出更方便的 api
│ ├── router # 路由配置
│ ├── store # pinia 状态管理
│ ├── style # 全局样式
│ │ ├── dark.scss # 暗黑模式样式适配文件
│ │ ├── element-plus.scss # 全局覆盖 element-plus 样式文件
│ │ ├── reset.scss # 全局重置样式文件
│ │ ├── sidebar.scss # layout 布局样式文件
│ │ ├── tailwind.css # tailwindcss 自定义样式配置文件
│ │ ├── ...
│ ├── utils # 全局工具方法
│ │ ├── http # 封装 axios 文件
│ │ ├── localforage # 二次封装 localforage (https://localforage.docschina.org/) 支持设置过期时间,提供完整的类型提示
│ │ ├── progress # 封装 nprogress
│ │ └── auth.ts # 处理用户信息和 token 相关
│ │ └── chinaArea.ts # 汉字转区域码
│ │ └── globalPolyfills.ts # 解决项目可能因为安装某个依赖出现 `global is not defined` 报错
│ │ └── message.ts # 消息提示函数
│ │ ├── mitt.ts # 触发公共事件,类似 EventBus
│ │ ├── preventDefault.ts # 阻止键盘F12、浏览器默认右键菜单、页面元素选中、图片默认可拖动的方法
│ │ ├── print.ts # 打印函数
│ │ ├── propTypes.ts # 二次封装 vue 的 propTypes
│ │ ├── responsive.ts # 全局响应式 storage 配置
│ │ ├── sso.ts # 前端单点登录逻辑处理
│ │ ├── tree.ts # 树结构相关处理函数
│ ├── views # 存放编写业务代码页面
│ ├── App.vue # 入口页面
│ ├── main.ts # 入口文件
├── types # 全局 TS 类型配置
│ │ ├── directives.d.ts # 全局自定义指令类型声明
│ │ ├── global-components.d.ts # 自定义全局组件获得 Volar 提示(自定义的全局组件需要在这里声明下才能获得 Volar 类型提示哦)
│ │ ├── global.d.ts # 全局类型声明,无需引入直接在 `.vue` 、`.ts` 、`.tsx` 文件使用即可获得类型提示
│ │ ├── index.d.ts # 此文件跟同级目录的 global.d.ts 文件一样也是全局类型声明,只不过这里存放一些零散的全局类型,无需引入直接在 .vue 、.ts 、.tsx 文件使用即可获得类型提示
│ │ ├── router.d.ts # 全局路由类型声明
│ │ ├── shims-tsx.d.ts # 该文件是为了给 .tsx 文件提供类型支持,在编写时能正确识别语法
│ │ └── shims-vue.d.ts # .vue、.scss 文件不是常规的文件类型,typescript 无法识别,所以我们需要通过下图的代码告诉 typescript 这些文件的类型,防止类型报错
├── .browserslistrc # 配置目标浏览器的环境
├── .dockerignore # 排除不需要上传到 docker 服务端的文件或目录
├── .editorconfig # 编辑器读取文件格式及样式定义配置 https://editorconfig.org/
├── .env # 全局环境变量配置(当 .env 文件与 .env.development、.env.production、.env.staging 这三个文件之一存在相同的配置 key 时,.env 优先级更低)
├── .env.development # 开发环境变量配置
├── .env.production # 生产环境变量配置
├── .env.staging # 预发布环境变量配置
├── .gitattributes # 自定义指定文件属性
├── .gitignore # git 提交忽略文件
├── .gitpod.yml # gitpod 部署配置
├── .lintstagedrc # lint-staged 配置
├── .markdownlint.json # markdown 格式检查配置
├── .npmrc # npm 配置文件
├── .nvmrc # 用于指定在使用 Node Version Manager(NVM)时要使用的特定 Node.js 版本
├── .prettierignore # prettier 语法检查忽略文件
├── .prettierrc.js # prettier 插件配置
├── .stylelintignore # stylelint 语法检查忽略文件
├── CHANGELOG.en_US.md # 版本更新日志(英文版)
├── CHANGELOG.md # 版本更新日志(英文版)
├── CHANGELOG.zh_CN.md # 版本更新日志(中文版)
├── Dockerfile # 用来构建 docker 镜像
├── LICENSE # 证书
├── README.en-US.md # README(英文版)
├── README.md # README
├── commitlint.config.js # git 提交前检查配置
├── eslint.config.js # eslint 语法检查配置
├── index.html # html 主入口
├── package.json # 依赖包管理以及命令配置
├── pnpm-lock.yaml # 依赖包版本锁定文件
├── postcss.config.js # postcss 插件配置
├── stylelint.config.js # stylelint 配置
├── tailwind.config.ts # tailwindcss 配置
├── tsconfig.json # typescript 配置
└── vite.config.ts # vite 配置
首先需要在vite.config.ts添加如下代码:
代码1
proxy: {"/api": {target: "http://127.0.0.1:8000",changeOrigin: true,rewrite: path => path.replace(/^\/api/, "")}},
代码1解析:
那么为什么要在vite.config.ts中添加api前缀,这是一个标准的前端开发代理配置方案。在Vite配置中添加 /api 前缀主要有以下几个作用:
开发环境代理 :在开发环境中,前端请求会添加 /api 前缀,然后通过Vite的代理配置转发到后端服务器( http://127.0.0.1:8000 )
解决跨域问题 :通过代理机制,前端可以在同一域名和端口下与后端通信,有效避免了浏览器的跨域限制
路径重写处理 :代理配置中的 rewrite 选项会将请求路径中的 /api 前缀移除,确保后端接收到的是正确的API路径
环境适配 :从 api/utils.ts 文件可以看到,项目使用了环境区分的URL策略:
在实际API调用中,项目使用这个前缀策略,例如在 routes.ts 中的API调用:
export const getAsyncRoutes = () =>
{return http.request<Result>("get", baseUrlApi("get-async-routes"));
};
其工作流程如下:
开发环境下,前端请求URL会被设置为 /api/get-async-routes,Vite的代理配置捕获到 /api 开头的请求,代理将请求转发到 http://127.0.0.1:8000 ,并自动移除 /api 前缀,实际请求变为 http://127.0.0.1:8000/get-async-routes,这样既解决了跨域问题,又实现了前后端分离开发时的API请求转发。这种配置方式是前端工程化中的最佳实践,能够有效分离开发环境和生产环境的API请求处理策略。
接下来,在src/api目录下新建utils.ts文件,在其中写入一下代码:
代码2:
export const baseUrlApi = (url: string) =>process.env.NODE_ENV === "development"? `/api/${url}`: `http://127.0.0.1:8000/${url}`;
ps:"utils" 是 "utilities" 的缩写,中文通常翻译为工具函数、工具类或工具模块。在编程中,utils 的作用是封装通用的、可复用的功能代码,主要用途包括①:代码复用:将多个地方都会用到的功能(如日期格式化、数据验证、字符串处理等)抽离到 utils 中,避免重复编码。②简化主逻辑:把辅助性的复杂逻辑(如加密解密、数据转换)放到 utils 中,让业务代码更简洁清晰。③统一维护:工具函数集中管理,修改时只需改一处,所有引用的地方都会生效,便于维护。④分类组织:通常会按功能细分 utils(如 dateUtils、stringUtils),使项目结构更有序。
代码2是一个用于处理 API 基础路径的工具函数,主要功能是根据当前环境(开发环境 / 生产环境)返回不同的 API 基础路径,以便前端请求能正确指向向后端服务。
代码解析:
首先定义与导出
export const baseUrlApi = (url: string) => { ... }
这是一个 TypeScript 箭头函数,命名为 baseUrlApi,接收一个字符串参数 url(表示 API 的具体路径部分),并通过 export 导出供其他模块使用。
接下来时环境判断
process.env.NODE_ENV === "development"
process.env.NODE_ENV 是 Node.js 环境中的一个变量,用于标识当前的运行环境:
development 表示开发环境(本地开发时)
production 表示生产环境(项目部署上线后)
路径拼接逻辑
若为开发环境:返回 /api/${url}
例如传入 url 为 /user/login,则前端显示接口结果为 /api/user/login
(通常配合本地开发服务器的代理配置,转发到后端开发服务)
若为生产环境:返回 https://www.wicms.cn/${url}
例如传入 url 为 /user/login,则结果为 https://www.wicms.cn/user/login
(直接指向生产环境的后端 API 域名)
作用与使用场景:
统一管理 API 路径:避免在每个接口请求中硬编码完整路径,便于维护。
环境隔离:开发时用本地 / 测试接口,上线后自动切换到生产接口,无需手动修改代码。
第三步,在具体的接口设置中引入代码2,以登录login为例,在src\api\user.ts文件通过以下代码引入代码2:
import { baseUrlApi } from "./utils";
在定义接口的地方调用baseUrlApi方法,如在定义的登录接口:
/** 登录 */
export const getLogin = (data?: object) => {return http.request<UserResult>("post", baseUrlApi("login"), { data });
};
至此,完成了前端的接口设置。接下来打开FastAPI后端,定义post路由返回对应结果,需要特别注意的是,前端显示的请求地址和真实的后端接口是不一样的,通过以上流程设置后,前端登录接口地址是:127.0.0.1:8848/api/login,然而实际请求的后端地址时:127.0.0.1:8000/login:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from routers import get_async_routesapp = FastAPI()# 配置CORS,允许前端跨域请求
app.add_middleware(CORSMiddleware,allow_origins=["http://localhost:8848"], # 前端地址allow_credentials=True,allow_methods=["*"],allow_headers=["*"],
)# 定义请求数据模型(Pydantic)
class UserRequest(BaseModel):username: stremail: strage: int = None # 可选字段@app.get("/")
async def read_root():return {"Hello": "World"}# 定义POST路由
@app.post("/api/user")
async def create_user(user: UserRequest):# 这里可以处理业务逻辑,比如存入数据库# 示例:返回处理后的用户数据return {"message": "用户创建成功","data": {"username": user.username,"email": user.email,"age": user.age if user.age else "未提供"}}# 定义POST路由
@app.post("/login")
async def login():# 使用JSONResponse返回格式化的JSON数据response_data = {"success": True,"data": {"avatar": "https://avatars.githubusercontent.com/u/44761321","username": "admin","nickname": "Fast","roles": ["admin"],"permissions": ["*:*:*"],"accessToken": "eyJhbGciOiJIUzUxMiJ9.admin","refreshToken": "eyJhbGciOiJIUzUxMiJ9.adminRefresh","expires": "2030/10/30 00:00:00"}}return JSONResponse(content=response_data, status_code=200)# 提供异步路由配置
@app.get("/get-async-routes")
async def async_routes():# 调用routers.py中的函数获取路由配置return JSONResponse(content=get_async_routes(), status_code=200)if __name__ == "__main__":import uvicornuvicorn.run(app, host="127.0.0.1", port=8000)
举一反三,只要明白了login的接口请求方法,其他的api请求是同样的道理,只是需要特别注意在需要使用的地方要引入src/api/ultis.ts配置。其他接口返回的数据类型参照mock里面定义的样式。
结合官方文档说明如下:
首先,要对接FastAPI后端,需要先将模拟数据接口Mock引用给注释掉,官方文档http请求 | Pure Admin 官方文档说明如下:
一、快速删除 mock
来到 build/plugins.ts (opens new window)文件,将 import { vitePluginFakeServer } from "vite-plugin-fake-server";
注释,最后把 这里选中 (opens new window)55-60行的都注释即可
二、参考原来的Mock定义接口和数据及其使用方式
① 来到 mock (opens new window)文件存放目录,比如添加 login
接口,采用 post
请求,参考下面代码。
import { MockMethod } from "vite-plugin-mock";export default [{url: "/login",method: "post",response: ({ body }) => {return {success: true,data: {},};},},
] as MockMethod[];
② 上面我们添加了 login
接口,接着来到 api (opens new window)目录,比如添加 user.ts
文件,参考下面代码
import { http } from "@/utils/http";// 这里定义返回值类型,使接口拥有良好的类型推导
export type UserResult = {/** 是否请求成功 */success: boolean;data: {/** 用户名 */username: string;/** 当前登陆用户的角色 */roles: Array<string>;/** `token` */accessToken: string;/** 用于调用刷新`accessToken`的接口时所需的`token` */refreshToken: string;/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */expires: Date;};
};/** 登录接口 */
export const getLogin = (data?: object) => {return http.request<UserResult>("post", "/login", { data });
};
③ 上面导出了 getLogin
接口,接下来我们来到需要使用这个接口的地方导入,参考下面代码
点击查看
<script setup lang="ts">
import { getLogin } from "@/api/user";async function onLogin() {let result = await getLogin({ username: "admin", password: "admin123" });if (result.success) {console.log(result.data);}
}
</script><template><el-button @click="onLogin">点击登录</el-button>
</template>
您可以注意到我们在上面 ②
步骤中定义了接口返回值类型 UserResult
,在使用时就会获得良好的类型推导,如下图
三、与FastAPI后台联调通信
3.1 解决跨域问题
跨域本质是浏览器基于同源策略的一种安全手段。同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能。当协议(protocol)、主机(host)、端口(port)其中一项不相同的时候就会产生跨域。跨域只产生在浏览器,这也是为什么您用 postman
请求接口时不会有跨域问题的原因
3.1.1解决办法
① 本地开发的话,我们来到 vite.config.ts (opens new window),参考下面代码配置本地跨域代理即可
proxy: {"/api": {// 这里填写后端地址target: "http://127.0.0.1:3000",changeOrigin: true,rewrite: path => path.replace(/^\/api/, "")}
}
② 配置好上面的跨域代理后,来到 src/api (opens new window)目录,这里我们先新建一个 utils.ts
文件用来配置环境,参考下面代码
注意:这里又分两种写法
第一种(也是最常用的一种,满足大多数场景,首选这种写法哦):比如后端使用 spring cloud
微服务架构,不同服务可能会部署在不同机器上,这时候前端必定产生跨域,推荐部署前端项目首选 nginx (opens new window),可以利用 nginx
的代理转发功能来解决跨域问题
utils.ts
export const baseUrlApi = (url: string) => `/api/${url}`;
第二种:前端打包直接丢给后端,放到后端项目里一起部署,协议、主机、端口都相同了,就没有跨域问题
utils.ts
export const baseUrlApi = (url: string) =>process.env.NODE_ENV === "development"? `/api/${url}`: `http://127.0.0.1:3000/${url}`;
③ 写完上面的 utils.ts
后,继续在 src/api (opens new window)目录,我们再建一个 user.ts
文件,导出这个接口,供页面调用,参考下面代码
user.ts
import { http } from "@/utils/http";
import { baseUrlApi } from "./utils";/** 登录 */
export const getLogin = (data?: object) => {return http.request<any>("post", baseUrlApi("login"), { data });
};
④ 上面都完成后,我们在需要调用该接口的地方调用即可,参考 src/store/modules/user.ts(opens new window)
多个后端地址如何联调
大致步骤和上面差不多,这里我们就简单写一下,具体对比上面的就行
① 在 vite.config.ts
写入下面代码
proxy: {// 第一个代理后端地址"/api": {target: "http://127.0.0.1:3000",changeOrigin: true,rewrite: path => path.replace(/^\/api/, "")},// 第二个代理后端地址"/otherApi": {target: "http://127.0.0.1:3290",changeOrigin: true,rewrite: path => path.replace(/^\/otherApi/, "")},// websocket地址(知识点:wss只能在https安全协议下使用)// "/wsApi": {// target: "ws://localhost:3000",// ws: true,// },// ...依此类推,有几个后端地址就写几个后端地址
}
② 在 utils.ts
里我们采用第一种写法,参考下面代码
utils.ts
// 第一个代理后端地址
export const baseUrlApi = (url: string) => `/api/${url}`;
// 第二个代理后端地址
export const baseUrlOtherApi = (url: string) => `/otherApi/${url}`;
③ 第一个代理后端地址上面建的是 user.ts
文件,那么第二个代理后端地址我们就建个 other.ts
文件吧,参考下面代码
other.ts
import { http } from "@/utils/http";
import { baseUrlOtherApi } from "./utils";/** 该接口采用 http://127.0.0.1:3290 后端地址 */
export const getOther = (data?: object) => {return http.request<any>("get", baseUrlOtherApi("other"), { data });
};
最后,需要在哪里调用,引入使用即可
如何声明接口返回值类型
可以看到上面的写法 http.request
后面跟的都是 any
,这很不友好,参考下面友好类型写法即可
import { http } from "@/utils/http";
import { baseUrlApi } from "./utils";// 定义 login 接口返回值类型为 UserResult
export type UserResult = {/** 是否请求成功 */success: boolean;data: {/** 用户名 */username: string;/** 当前登陆用户的角色 */roles: Array<string>;/** `token` */accessToken: string;/** 用于调用刷新`accessToken`的接口时所需的`token` */refreshToken: string;/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */expires: Date;};
};/** 登录 */
export const getLogin = (data?: object) => {return http.request<UserResult>("post", baseUrlApi("login"), { data });
};
平台封装的请求可以设置额外的 axios 配置吗
当然可以。点击查看这里,可以看到自定义 axios
配置中 axiosConfig
作为最后一个参数
import { http } from "@/utils/http";
import { baseUrlApi } from "./utils";export const testRequest = (data?: object) => {return http.request<any>("post",baseUrlApi("login"),{ data },// 自定义的axios配置在下面对象填写即可{headers: {"Content-Type": "application/x-www-form-urlencoded",},});
};
#JWT Token
(内置无感刷新 token
解决方案)
实现原理:后端返回两个 token
(一个用来请求,一个用来刷新)和过期时间,前端将其进行本地存储,每当接口请求时,把本地存储的过期时间与本地当前时间对比,如果 token
过期就把当前请求暂存,然后去请求刷新 token
接口,获取到新 token
后,再 触发 (opens new window)暂存的请求
#信息存储
具体看 src/utils/auth (opens new window)文件,里面有很详细的备注
#细节处理
① 不需要携带 token
的接口我们设置了 请求白名单(opens new window)
② 当 token
过期后,平台会暂存请求,直到拿到新 token
才会请求,避免了当页面有多个请求会重复刷新 token
的问题。核心代码有三处:一 (opens new window)、二 (opens new window)、三(opens new window)
生产环境 Mock 可以和真实接口共存吗?
当然可以共存,不过可能会遇到 Mock
干扰真实接口的问题。如果您在开发环境接口没问题,但是到生产环境出问题了,在排除后端问题前提下可以尝试 删除 Mock
除后端问题前提下可以尝试 删除 Mock
参考:Pure Admin 官方文档