React Todos 前端独立开发全解:用 vite-plugin-mock + axios 封装,再也不等后端接口
适用技术栈:React 19 + Vite 8 + axios + vite-plugin-mock 3。目标读者:正在学前后端分离、但总被后端进度卡住的前端新手。读完你会得到一个"前端先行"的最小可复用模板:路由懒加载、API 目录工程化、Mock 假数据、切换真后端只改一行配置。代码运行未验证。
文章目录
- [React Todos 前端独立开发全解:用 vite-plugin-mock + axios 封装,再也不等后端接口](#React Todos 前端独立开发全解:用 vite-plugin-mock + axios 封装,再也不等后端接口)
-
- [前言:写一个 Todo 应用,为什么非要等后端接口?](#前言:写一个 Todo 应用,为什么非要等后端接口?)
- [1. 项目结构与技术栈:前端三驾马车 + 后端骨架](#1. 项目结构与技术栈:前端三驾马车 + 后端骨架)
-
- [1.1 目录设计](#1.1 目录设计)
- [1.2 技术栈概览](#1.2 技术栈概览)
- [2. 前端路由与代码分割:BrowserRouter + lazy + Suspense 三件套](#2. 前端路由与代码分割:BrowserRouter + lazy + Suspense 三件套)
-
- [2.1 为什么要做路由懒加载?](#2.1 为什么要做路由懒加载?)
- [2.2 最小可复用模板(本项目 App.jsx)](#2.2 最小可复用模板(本项目 App.jsx))
- [2.3 `<Link>` vs 原生 `<a>`:为什么 SPA 推荐 Link](#2.3
<Link>vs 原生<a>:为什么 SPA 推荐 Link)
- [3. src/api 工程化:axios 实例 + 按模块拆分](#3. src/api 工程化:axios 实例 + 按模块拆分)
-
- [3.1 axios.create:统一 baseURL + timeout 的全局层](#3.1 axios.create:统一 baseURL + timeout 的全局层)
- [3.2 按模块拆分接口:一个文件管一种资源](#3.2 按模块拆分接口:一个文件管一种资源)
- [4. 页面拿数据:useEffect + 立即执行异步函数(IIFE)](#4. 页面拿数据:useEffect + 立即执行异步函数(IIFE))
- [5. vite-plugin-mock:前端自己给自己"造后端"](#5. vite-plugin-mock:前端自己给自己"造后端")
-
- [5.1 插件注册(vite.config.js)](#5.1 插件注册(vite.config.js))
- [5.2 编写接口规则(mock/todos.js)](#5.2 编写接口规则(mock/todos.js))
- [6. 后端写完了怎么切?两种最小改动方案](#6. 后端写完了怎么切?两种最小改动方案)
-
- [方案 A(推荐):Vite 代理 + 路径重写](#方案 A(推荐):Vite 代理 + 路径重写)
- [方案 B:直接改 axios baseURL](#方案 B:直接改 axios baseURL)
- [7. 常见错误排错自检表(收藏用)](#7. 常见错误排错自检表(收藏用))
- [8. 五步自检:你的"前端先行"项目合格了吗?](#8. 五步自检:你的"前端先行"项目合格了吗?)
- [9. 延伸练习:从"能跑"到"工程化"](#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 顶部写:
jsx
import Home from './pages/Home'
import Todos from './pages/Todos'
首屏一次性把两个页面的 JS 全下载了。页面多了以后首屏极慢。
React 提供 lazy 做路由级代码分割------只有 URL 匹配到了对应页面,浏览器才去下载那个页面的 chunk。Suspense fallback=... 就是下载过程中的兜底显示,没有它会白屏。
2.2 最小可复用模板(本项目 App.jsx)
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
jsx
<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:
js
import axios from 'axios'
const instance = axios.create({
baseURL: '/api', // 所有请求自动拼 /api 前缀
timeout: 5000, // 5 秒无响应自动失败,防止转圈卡死
})
export default instance
为什么不用 fetch?材料注释里写得很直接:"fetch 的缺点是功能小"。展开四点:
- fetch 遇到 HTTP 4xx/5xx 不抛异常 ,返回的仍是 resolved Promise,要手动判断
res.ok;axios 自动 reject。 - fetch 没有原生
timeout,要AbortController + setTimeout自己凑;axios 一个字段搞定。 - fetch 无拦截器;axios 有 request/response 拦截器,将来统一加 token、统一错误 Toast,全项目加在一处就行。
- fetch 无"独立实例"概念;
axios.create可以建多个实例(例如一个请求 /api,一个请求 /third),互不干扰。
3.2 按模块拆分接口:一个文件管一种资源
src/api/todos.js:
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 的数据请求:
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)
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)
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):
js
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
js
// 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. 延伸练习:从"能跑"到"工程化"
当你把模板跑通后,可按这个路线继续加固(和面试知识点高度重合):
- 给 axios 实例加
request拦截器注入 token,response拦截器统一弹 Toast 错误。 - 真正把 zustand 用起来:把 todos 数组迁出页面 useState,跨页面共享(比如 Home 显示总数)。
- 在 backend 目录写最小 Koa
GET /todos,返回和 Mock 相同的 JSON;然后用方案 A 切换并验收。 - 给 Todos 页面加 POST /api/todos(输入框 + 按钮),同步加 Mock、加 API 模块、加 Koa 路由,体验"完整 CRUD 的契约驱动开发"。
- 把整个项目从 JSX 改成 TSX,给 axios 响应写泛型类型,体验"接口契约 → TS 类型 → 组件里类型提示"的链路。
结语:真正的前后端分离,从"前端能独立交付"开始
很多人以为前后端分离 = 前端写 React,后端写 Koa,拼在一起就完事。其实真正的分离是开发节奏能解耦:前端不依赖后端进度,能用 Mock 独立把界面、交互、校验跑通;后端把接口契约按时交付,联调只是"切换流量去向"的配置操作。
这个 Todos 项目虽然小,但演示了完整套路:lazy + Suspense(首屏性能) + src/api 工程化(职责分离) + vite-plugin-mock(节奏解耦) + Vite proxy(联调无缝切换)。把这个四件套变成你的默认项目模板,下次写任何 CRUD 项目都不会再被后端进度卡死,也不会被"接口改一处我搜全项目"折磨。
下一步,就从上面的"五步自检"和"延伸练习"开始,照着模板写一个属于你自己的版本吧。
标签(3-5):React、Vite、axios、前后端分离、vite-plugin-mock