React Todos 前端独立开发全解:用 vite-plugin-mock + axios 封装,再也不等后端接口
适用技术栈:React 19 + Vite 8 + axios + vite-plugin-mock 3。目标读者:正在学前后端分离、但总被后端进度卡住的前端新手。读完你会得到一个"前端先行"的最小可复用模板:路由懒加载、API 目录工程化、Mock 假数据、切换真后端只改一行配置。代码运行未验证。
文章目录
- React Todos 前端独立开发全解:用 vite-plugin-mock + axios 封装,再也不等后端接口
-
- 前言:写一个 Todo 应用,为什么非要等后端接口?
- 1. 项目结构与技术栈:前端三驾马车 + 后端骨架
-
- 1.1 目录设计
- 1.2 技术栈概览
- 2. 前端路由与代码分割:BrowserRouter + lazy + Suspense 三件套
-
- 2.1 为什么要做路由懒加载?
- 2.2 最小可复用模板(本项目 App.jsx)
- 2.3 `<Link>` vs 原生 `<a>`:为什么 SPA 推荐 Link
- 3. src/api 工程化:axios 实例 + 按模块拆分
-
- 3.1 axios.create:统一 baseURL + timeout 的全局层
- 3.2 按模块拆分接口:一个文件管一种资源
- 4. 页面拿数据:useEffect + 立即执行异步函数(IIFE)
- 5. vite-plugin-mock:前端自己给自己"造后端"
-
- 5.1 插件注册(vite.config.js)
- 5.2 编写接口规则(mock/todos.js)
- 6. 后端写完了怎么切?两种最小改动方案
-
- 方案 A(推荐):Vite 代理 + 路径重写
- 方案 B:直接改 axios baseURL
- 7. 常见错误排错自检表(收藏用)
- 8. 五步自检:你的"前端先行"项目合格了吗?
- 9. 延伸练习:从"能跑"到"工程化"
- 结语:真正的前后端分离,从"前端能独立交付"开始
前言:写一个 Todo 应用,为什么非要等后端接口?
你可能刚学会 React + axios,想写一个 TodoList。按照"正确流程":先让后端写 GET /todos、POST /todos,你再拿数据渲染。结果后端同学忙着别的需求,接口一周都没影——你卡在"页面能写,但没数据就是空的"这个状态上,寸步难行。
这个项目用一个 Todos 小例子完整演示了一套"前端先行"的开发模式:后端只要把接口契约(URL / method / 返回结构)定下来,前端自己用 vite-plugin-mock 提供一模一样的假 JSON,页面、状态、交互全部写完;后端真正写完接口后,只改一行配置(Vite proxy 或 axios baseURL),页面代码和 API 封装代码零改动即可联调。
本文包含:React Router 懒加载配置、src/api 目录的 axios 工程化封装、useEffect 正确的异步写法(为什么不能 async)、Mock 规则的写法、联调切换方案,以及一份"常见报错→原因→排查"的自检清单。全部内容可回溯到源码证据。
1. 项目结构与技术栈:前端三驾马车 + 后端骨架
1.1 目录设计
todos-fullstack/
├── readme.md # 顶层设计说明(解耦思路、API 目录职责)
├── fronted/todos/ # 前端项目根
│ ├── vite.config.js # Vite + vite-plugin-mock 注册
│ ├── mock/
│ │ └── todos.js # GET /api/todos Mock 规则 + 2 秒假延迟
│ └── src/
│ ├── main.jsx # React 19 createRoot 挂载
│ ├── App.jsx # BrowserRouter + lazy + Suspense
│ ├── components/Nav.jsx # Link 导航(不是原生 <a>)
│ ├── pages/
│ │ ├── Home.jsx # / 首页
│ │ └── Todos.jsx # /todos 页面(useEffect + IIFE 调接口)
│ └── api/
│ ├── config.js # axios.create({ baseURL:'/api', timeout:5000 })
│ └── todos.js # 命名导出 getTodos(按模块拆分)
└── backend/
└── package.json # 目前只装 koa 3,业务代码待写(前端先行状态)
1.2 技术栈概览
前端(已落地):React 19 / react-router-dom 7 / Vite 8 / axios 1 / vite-plugin-mock 3 / zustand 5(已安装,当前页面未实际使用)。
后端(骨架状态):koa 3(package.json 有依赖,但无 index.js、无 mysql 驱动,readme 规划但未落地)。
⚠️ 事实披露:zustand 仅在依赖中声明,src 中没有任何 useStore 代码;后端没有写业务逻辑,本项目当前是纯前端 + Mock 自给自足。不能声称"项目已使用 zustand"或"有完整 Koa 后端"。
2. 前端路由与代码分割:BrowserRouter + lazy + Suspense 三件套
2.1 为什么要做路由懒加载?
如果在 App.jsx 顶部写:
import Home from './pages/Home'
import Todos from './pages/Todos'
首屏一次性把两个页面的 JS 全下载了。页面多了以后首屏极慢。
React 提供 lazy 做路由级代码分割——只有 URL 匹配到了对应页面,浏览器才去下载那个页面的 chunk。Suspense fallback=… 就是下载过程中的兜底显示,没有它会白屏。
2.2 最小可复用模板(本项目 App.jsx)
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Nav from './components/Nav'
const Home = lazy(() => import('./pages/Home'))
const Todos = lazy(() => import('./pages/Todos'))
export default function App() {
return (
<Router>
<Nav />
<Suspense fallback={<div>Loading…</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</Suspense>
</Router>
)
}
2.3 <Link> vs 原生 <a>:为什么 SPA 推荐 Link
<Link to="/todos">Todos</Link>
Link 底层渲染成 <a>,但拦截了 e.preventDefault(),改用 HTML5 History API 改 URL,然后 React Router 内部匹配新路径,局部替换组件,不整页刷新——没有白屏、没有 CSS/JS 重新下载,用户体验顺滑。而原生 <a href="/todos"> 会重新请求 index.html + 全部资源,有明显闪烁。
3. src/api 工程化:axios 实例 + 按模块拆分
这是将来联调零改动页面代码的关键。
3.1 axios.create:统一 baseURL + timeout 的全局层
src/api/config.js:
import axios from 'axios'
const instance = axios.create({
baseURL: '/api', // 所有请求自动拼 /api 前缀
timeout: 5000, // 5 秒无响应自动失败,防止转圈卡死
})
export default instance
为什么不用 fetch?材料注释里写得很直接:“fetch 的缺点是功能小”。展开四点:
3.2 按模块拆分接口:一个文件管一种资源
src/api/todos.js:
import instance from './config'
// 按模块命名导出:一个资源(todos)一个文件
export const getTodos = async () => {
const res = await instance.get('/todos') // 拼 baseURL 后实际 GET /api/todos
return res.data // 返回值是后端响应体,不是整个 res
}
这样做的好处:接口 URL 改了、请求库换了(axios → ky)、要加 TS 类型……都只改 API 目录,页面代码完全不动。多人协作时,A 写 todos 接口,B 写 users 接口,互不冲突。
4. 页面拿数据:useEffect + 立即执行异步函数(IIFE)
src/pages/Todos.jsx 的数据请求:
import { getTodos } from '../api/todos'
import { useEffect, useState } from 'react'
export default function Todos() {
const [todos, setTodos] = useState([])
useEffect(() => {
// 为什么不能直接 useEffect(async () => …)?
// 答:async 函数返回 Promise,React 期望 useEffect 返回清理函数或 undefined
(async () => {
const data = await getTodos()
setTodos(data.todos) // 响应体结构 {code:0, todos:[…]},取 data.todos 数组
})()
}, []) // 空依赖:首次渲染只执行一次
return <div><h1>Todos</h1></div>
}
这个"IIFE 模式"可以直接背下来复用:当 useEffect 里要 await,但外层函数不能 async 时就包一层。
⚠️ 易错点:Mock 返回 {code:0, todos:[…]},必须取 data.todos。如果直接 setTodos(data),把对象当数组 map,UI 不会显示但也不报错,排查极其痛苦。
5. vite-plugin-mock:前端自己给自己"造后端"
5.1 插件注册(vite.config.js)
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 规则文件放在项目根的 mock/ 目录
localEnabled: true, // 只在开发环境启用,生产构建会自动剔除
})
],
})
5.2 编写接口规则(mock/todos.js)
export default [
{
url: '/api/todos', // 必须和 axios 实际请求的 URL 完全一致(baseURL 拼好后)
method: 'get', // GET/POST 对应 axios.get/post
timeout: 2000, // 模拟 2 秒网络延迟,体验更真实,顺便测 Loading
response: (req, res) => ({
code: 0,
todos: [
{ id: 1, title: '学习react', completed: false },
{ id: 2, title: '学习vue', completed: false },
]
})
}
]
效果:前端发起 GET http://localhost:5173/api/todos,Vite 开发服务器在 HTTP 层就拦截住,不经过真实网络,timeout 后返回假 JSON。你的 axios、页面、状态管理代码完全按"真后端"写,但不需要真后端。
⚠️ 易错点:这里的 url 必须和 axios 实际请求一致。axios baseURL 是 /api,getTodos 调 /todos,加起来就是 /api/todos——所以 mock 规则必须写 /api/todos,只写 /todos 会命中不了,报 404。
6. 后端写完了怎么切?两种最小改动方案
这就是前面"API 工程化"的回报:后端 Koa 写完跑在 http://localhost:3000/todos 时,页面和 src/api/todos.js 一行代码都不用改。
方案 A(推荐):Vite 代理 + 路径重写
在 vite.config.js 加 server.proxy,把 /api/* 转发到 3000 端口,并去掉 /api 前缀(后端路径是 /todos 不是 /api/todos):
export default defineConfig({
plugins: [react(), viteMockServe({…})], // 联调时可把 localEnabled 改为 false 关掉 Mock
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\\/api/, '') // /api/todos → /todos
}
}
}
})
优点:前端仍然请求同域 /api/todos,没有跨域问题,不需要后端开 CORS。线上部署时同理(Nginx 反代)。
方案 B:直接改 axios baseURL
// api/config.js
const instance = axios.create({
baseURL: 'http://localhost:3000', // 直接请求后端
timeout: 5000,
})
缺点:浏览器跨域(5173 → 3000),需要后端开启 CORS(koa 要装 @koa/cors);同时后端接口路径要和前端一致(/todos 而不是 /api/todos,不然再加 rewrite 逻辑)。一般只在独立环境开发时用。
7. 常见错误排错自检表(收藏用)
| 路由懒加载后白屏,控制台报 “suspended” | lazy 组件缺少 <Suspense> 兜底 | 在 <Routes> 外层包 <Suspense fallback='Loading…'> |
| /todos 页面一直空,Network 200 但数据不对 | Mock 返回 {code,todos} 但页面 setTodos(data) 没取 .todos | 改 setTodos(data.todos);Network 面板检查响应体字段 |
| 请求 /api/todos 404 | Mock 规则写的 url 是 /todos(少了 /api 前缀) | mock/todos.js 的 url 改成 /api/todos |
| 请求 5 秒自动失败 / 转圈卡住 | axios timeout:5000 触发;或 Mock timeout 大于超时 | Mock timeout 调短(如 2s),或真后端排查接口响应 |
| useEffect 控制台警告,或异步结果不可控 | useEffect(async () => { await … }) 直接写了 async | 改为 IIFE:(async () => await getTodos())() 包裹 |
| 点导航后整页刷新、闪烁 | 组件里用了原生 <a href> 没加 Link | 改为 <Link to="…"> |
| 切到真后端后接口 3000 端口报 CORS 错误 | 用了方案 B 直接跨域请求 | 改方案 A(Vite proxy)或后端装 @koa/cors |
8. 五步自检:你的"前端先行"项目合格了吗?
照着这个清单走一遍,能立刻找出漏网之坑:
- 路由:lazy 组件外层有没有 <Suspense fallback>?点导航是不是不刷新整页?
- API 层:axios 是不是用 axios.create 实例?baseURL 有没有统一?接口函数是不是按模块拆分 + 命名导出?
- 数据流:useEffect 里是不是用 IIFE 包了 await?接口 404 有没有 catch 容错提示?
- Mock:mock 规则 url 是不是和 baseURL 拼好的实际请求一致?response 结构是不是和 setTodos(data.todos) 字段对齐?
- 切换预案:后端写完后是走 Vite proxy 还是改 baseURL?至少想好一种路线并能解释为什么(推荐方案 A 无 CORS)。
9. 延伸练习:从"能跑"到"工程化"
当你把模板跑通后,可按这个路线继续加固(和面试知识点高度重合):
结语:真正的前后端分离,从"前端能独立交付"开始
很多人以为前后端分离 = 前端写 React,后端写 Koa,拼在一起就完事。其实真正的分离是开发节奏能解耦:前端不依赖后端进度,能用 Mock 独立把界面、交互、校验跑通;后端把接口契约按时交付,联调只是"切换流量去向"的配置操作。
这个 Todos 项目虽然小,但演示了完整套路:lazy + Suspense(首屏性能) + src/api 工程化(职责分离) + vite-plugin-mock(节奏解耦) + Vite proxy(联调无缝切换)。把这个四件套变成你的默认项目模板,下次写任何 CRUD 项目都不会再被后端进度卡死,也不会被"接口改一处我搜全项目"折磨。
下一步,就从上面的"五步自检"和"延伸练习"开始,照着模板写一个属于你自己的版本吧。
标签(3-5):React、Vite、axios、前后端分离、vite-plugin-mock


