从前后端分离到前端接口工程:React + MockJS + Vite 解析
前后端分离不是把代码放在不同文件夹就叫分离------真正关键是开发时序的解耦。本文通过一个 Todos 全栈项目,从底层讲透前端如何独立于后端完成开发闭环。
一、前后端分离的真正痛点
1.1 表面分离 vs 实际耦合
很多团队说"我们做了前后端分离",实际情况却是:
bash
前端开发:我要一个 /api/todos 接口
后端开发:还没写好,等两天
前端开发:...那我等着吧
这就是时序耦合 ------虽然代码分开了,但前端开发进度仍然被后端卡脖子。前端页面写完了,数据接口没就绪,联调不了,测试不了,只能干等。
1.2 理想的状态
scss
┌─────────────────────┐
前端独立开发 │ 组件 + 路由 + 状态 │
(第1天开始) │ + Mock 接口层 │
└─────────────────────┘
│
│ 后端就绪后
│ 一键切换 baseURL
▼
┌─────────────────────┐
前后端联调 │ 真实 API 接入 │
(后端完成后) │ 改一行配置即可 │
└─────────────────────┘
前端不应该等后端。这就是"前端接口工程"要解决的问题。
二、项目架构:三层分离
arduino
todos-fullstack/
├── frontend/ ← 前端:独立王国
│ └── todos/
│ ├── mock/ ← 模拟接口层
│ │ └── todos.js
│ ├── src/
│ │ ├── api/ ← 前端接口工程
│ │ │ ├── config.js ← axios 配置 + baseURL
│ │ │ └── todos.js ← 具体接口方法
│ │ ├── components/ ← 组件层
│ │ │ └── Nav.jsx
│ │ ├── pages/ ← 页面级路由
│ │ │ ├── Home.jsx
│ │ │ └── todos.jsx
│ │ └── App.jsx ← 路由总入口
│ └── vite.config.js ← Vite + Mock 插件
│
├── backend/ ← 后端(Node + Koa + MySQL)
│ └── ... ← 独立开发,不影响前端
│
└── readme.md ← 架构笔记
三层泾渭分明:
| 层 | 职责 | 独立性 |
|---|---|---|
mock/ |
开发期数据源 | 后端不在时顶上去 |
api/ |
统一请求管理 | 管配置、管请求、管切换 |
pages/ + components/ |
UI 渲染 + 状态 | 只跟 api 层交互,不关心数据从哪来 |
三、前端三驾马车:组件 + 路由 + 状态管理
readme 里点明了独立前端应用的三根支柱:
markdown
组件 (响应式) + 路由 + 状态管理
↓ ↓ ↓
用户看到 页面跳转 数据流转
3.1 路由:React Router 接管一切
javascript
// App.jsx
import { Routes, Route, BrowserRouter as Router } from 'react-router-dom';
function App() {
return (
<Router>
<Nav />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</Router>
)
}
<Router> 在最外层包裹整个应用,<Routes> 接管所有的页面级路由。<Nav> 组件放在 <Routes> 外面,意味着导航栏在所有页面都可见------这是路由布局的经典模式。
3.2 懒加载:优化首屏
ini
const Home = lazy(() => import('./pages/Home'));
const Todos = lazy(() => import('./pages/todos'));
React.lazy() 配合动态 import() 实现了代码分割(Code Splitting) :
makefile
没做懒加载: 首屏加载所有代码 → 慢
做了懒加载: 首屏只加载当前页代码 → 快
切换到 /todos 时再动态加载 Todos 组件
这是 Vite 底层 Rollup 打包的功劳,结合 React 的 Suspense 还能在加载时显示 fallback UI。
四、前端接口工程:api/ 目录设计
这是本文的核心------把接口管理提升为前端工程的一等公民。
4.1 为什么需要 api/ 目录?
readme 给出了四个理由:
① 后端接口往往不能及时提供
② 需要统一管理所有接口
③ 先用 mock 数据跑通流程
④ baseURL 一键切换开发/生产环境
4.2 第一层:axios 配置(config.js)
javascript
// api/config.js
import axios from 'axios';
const instance = axios.create({
baseURL: '/api',
timeout: 5000,
});
export default instance;
这里没有直接使用 axios 全局实例,而是创建了一个独立实例:
| 设计决策 | 原因 |
|---|---|
axios.create() |
不污染全局 axios 默认配置,可以有多个独立实例(比如不同的后端服务) |
baseURL: '/api' |
所有请求自动拼接前缀。get('/todos') → 实际请求 /api/todos |
timeout: 5000 |
5 秒超时保护,防止请求挂死 |
axios vs fetch 为什么选 axios?
bash
fetch: axios:
❌ 需要手动 .json() ✅ 自动 JSON 解析
❌ 请求超时需要 abort ✅ 内置 timeout
❌ 拦截器需要包装 ✅ 请求/响应拦截器
❌ 取消请求复杂 ✅ CancelToken
❌ 进度事件简陋 ✅ 上传/下载进度
fetch 是浏览器原生 API,功能简单、偏底层。axios 在 fetch 之上提供了一套企业级请求管理能力,更适合正经项目。
4.3 第二层:具体接口方法(todos.js)
javascript
// api/todos.js
import axios from './config';
export const getTodos = async () => {
const res = await axios.get('/todos');
return res.data;
}
每个接口方法只做一件事:发起请求、返回数据 。async/await 让异步代码读起来像同步,调用方不需要关心 Promise 链。
bash
调用链:
pages/todos.jsx
→ api/todos.js (getTodos)
→ api/config.js (axios 实例)
→ HTTP 请求
五、MockJS:让前端独立启动
5.1 核心思路
前端的两类路由,需要两种路由系统来分别处理:
bash
前端应用:
├── 页面路由 → react-router-dom 处理
│ ├── / → Home.jsx
│ └── /todos → todos.jsx
│
└── 接口路由 (/api/*) → vite-plugin-mock 处理
└── /api/todos → mock/todos.js
react-router-dom 只管页面跳转,管不了 HTTP 接口请求 。/api/todos 这个地址在浏览器看来就是一个 HTTP 请求 URL,Vite 开发服务器需要把它拦截下来,返回前端准备好的假数据。
5.2 vite.config.js:装上 Mock 插件
javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
react(),
viteMockServe({
mockPath: 'mock', // mock 文件目录
localEnabled: true // 开发环境启用
})
],
})
viteMockServe 的工作流程:
markdown
1. Vite 开发服务器启动
2. viteMockServe 读取 mock/ 目录下的所有文件
3. 拦截匹配到的 HTTP 请求路径
4. 直接返回 mock 文件中定义的 response
5. 请求根本不到后端 ------ 在 Vite 服务器层就被拦截了
5.3 定义 Mock 接口
yaml
// mock/todos.js
export default [
{
url: '/api/todos',
method: 'get',
timeout: 2000,
response: (req, res) => {
return {
code: 0,
todos: [
{
id: 1,
title: '学习前端接口工程',
completed: true,
},
{
id: 2,
title: '看龙餐馆',
completed: false,
}
]
}
}
}
]
逐字段解析:
| 字段 | 类型 | 作用 |
|---|---|---|
url |
string | 匹配的请求路径,拦截 /api/todos |
method |
string | 匹配的 HTTP 方法 get / post / put / delete |
timeout |
number | 故意延迟 2 秒返回------模拟真实网络延迟,防止开发时太快、上线后才发现加载态 bug |
response |
function | 返回假数据。可以是一个对象,也可以是一个函数(动态生成) |
timeout: 2000 是一个容易被忽略但很关键的细节。很多前端开发者在 mock 阶段看到数据秒出,就忽略了 loading 状态的处理。上线后网络延迟 500ms、1000ms,页面白屏、闪烁、无提示------这些都是 mock 阶段"太快"埋下的坑。故意加延迟让前端在本地就养成处理异步状态的肌肉记忆。
5.4 完整请求链路
bash
页面访问 http://localhost:5173/todos
│
├── react-router 匹配 → 渲染 <Todos /> 组件
│
└── <Todos /> useEffect 中调用 getTodos() 即 GET /api/todos
│
│ viteMockServe 拦截 ↓
│
│ url="/api/todos" method="get" 匹配成功!
│ → 等待 2 秒(timeout)
│ → 返回 { code: 0, todos: [...] }
│
├── axios 收到响应
├── getTodos 返回 response.data
├── setTodos(data) ← 状态更新
└── UI 重新渲染 ← 用户看到列表
六、页面层:连接接口与 UI
6.1 todos.jsx:状态驱动的页面
javascript
import { getTodos } from '../api/todos';
import { useEffect, useState } from 'react';
function Todos() {
const [todos, setTodos] = useState([]);
useEffect(() => {
(async () => {
const data = await getTodos();
setTodos(data);
})();
}, []);
return (
<>
Todos
</>
)
}
逐行解析:
useState([]) :todos 状态初始为空数组,等接口返回后通过 setTodos 更新。这里的初始值 [] 很重要------如果用 null,组件渲染时 todos.map() 会直接报错。
useEffect(() => {}, []) :空依赖数组,只在组件挂载时执行一次。这是数据获取的经典用法------进页面 → 调接口 → 拿数据 → 渲染。
IIFE(立即执行函数) :
scss
(async () => {
const data = await getTodos();
setTodos(data);
})();
为什么不能直接写 await?
scss
// ❌ 不合法:useEffect 的回调不能是 async 函数
useEffect(async () => {
const data = await getTodos(); // async 返回 Promise
}, []);
// 原因:useEffect 的返回值必须是一个清理函数或 undefined。
// async 函数永远返回 Promise,React 会把这个 Promise 当成清理函数
// → 类型不匹配 → 潜在 bug
// ✅ 正确:普通函数 + 内部 IIFE
useEffect(() => {
(async () => {
const data = await getTodos();
setTodos(data);
})();
}, []);
七、baseURL 一键切换:从 Mock 到真实后端
这是前端接口工程最精彩的设计:
php
// api/config.js
const instance = axios.create({
baseURL: '/api', // ← 开发环境:走 mock
// baseURL: 'http://localhost:3000', // ← 生产联调:切到真实后端
timeout: 5000,
})
切换对比
bash
开发阶段 (Mock):
getTodos() → GET /api/todos
→ Vite 代理拦截 → mock/todos.js → 返回假数据
联调阶段 (真实后端):
注释掉 baseURL: '/api'
启用 baseURL: 'http://localhost:3000'
getTodos() → GET http://localhost:3000/todos
→ 真实 Node + Koa 后端 → 返回数据库数据
改一行代码,整个应用的所有接口全部切换------这就是 baseURL 集中管理的威力。
为什么用 /api 前缀?
bash
前端开发服务器:localhost:5173
真实后端服务器:localhost:3000
/api/todos → Vite Mock 拦截(开发期)
/api/todos → Vite proxy 转发到 localhost:3000/todos(联调期)
/api 前缀是一个约定 ------所有接口 URL 都以 /api 开头,浏览器看到 localhost:5173/api/todos,Vite 开发服务器可以决定是自己处理(mock)还是转发(proxy)。如果没有这个前缀,/todos 到底是页面路由还是接口请求就分不清了:
bash
/todos → 可能是页面路由(react-router)也可能是接口(http请求)
/api/todos → 明确是接口请求
八、完整架构图
bash
┌─────────────────────────────────────────────────────────────────┐
│ 前端应用 (localhost:5173) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ UI 层 │ │
│ │ App.jsx ──Router── Nav.jsx │ │
│ │ ├── / → Home.jsx │ │
│ │ └── /todos → todos.jsx [useState + useEffect]│ │
│ └──────────────────────┬───────────────────────────────────┘ │
│ │ import { getTodos } │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 接口工程层 api/ │ │
│ │ config.js ← axios.create({baseURL:'/api', timeout:5000}) │ │
│ │ todos.js ← getTodos() → axios.get('/todos') │ │
│ └──────────────────────┬───────────────────────────────────┘ │
│ │ HTTP GET /api/todos │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Vite 开发服务器 │ │
│ │ viteMockServe({ mockPath: 'mock', localEnabled: true }) │ │
│ │ │ │ │
│ │ ├── 开发期: 拦截 /api/todos → mock/todos.js │ │
│ │ └── 联调期: proxy 到 localhost:3000 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ mock/todos.js │ ← 开发期数据源 │
│ │ { url:'/api/todos', │ │
│ │ method:'get', │ │
│ │ response: {...} } │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓ 后端完成后,改一行 baseURL
┌─────────────────────────────────────────────────────────────────┐
│ 后端服务 (localhost:3000) │
│ Node + Koa + MySQL → /todos → JSON │
└─────────────────────────────────────────────────────────────────┘
九、总结
本文通过一个全栈 Todos 项目,梳理了前端独立开发的完整工程链路:
- 三驾马车:React(组件)+ React Router(路由)+ useState/useEffect(状态管理)构成前端骨架
- 前端接口工程 :
api/目录统一管理所有接口------config.js 管配置(axios 实例 + baseURL + timeout),todos.js 管具体方法(一个模块一个 JS 文件) - MockJS 解耦 :
vite-plugin-mock在 Vite 服务器层拦截/api/todos请求,返回mock/目录定义的假数据,前端无需等后端 - timeout 细节:mock 接口故意加 2 秒延迟,模拟真实网络环境,倒逼前端写好 loading 状态处理
- baseURL 一键切换 :注释
/api,启用http://localhost:3000,整个应用所有接口从 mock 切到真实后端------只需改一行 - IIFE + useEffect :
useEffect不支持直接async,用 IIFE 包裹await是最佳实践
核心思想一句话:前端不止是画页面,前端也是一门独立的工程学科------有自己的接口层、数据层和独立的开发闭环。 这才是前后端分离的本质意义。