React + Vite Todo 实践:用 Axios 与 Mock 解开前后端等待

React + Vite Todo 前端先行实践:用路由、Axios 接口层与 Mock 解开前后端等待

在前后端分离项目里,前端负责页面、交互和状态,后端负责业务接口与数据持久化。职责可以拆开,开发节奏却很难彻底互不影响:前端页面最终仍要依赖后端返回的数据。

以 Todo 应用为例,Todo 页面需要一个数组才能渲染任务。如果后端的 /todos 接口还没有完成,前端是停下来等待,还是先把路由、请求和状态链路搭好?

更实用的做法,是在前端内部建立一层统一的 API 工程:

  1. 页面只调用前端封装好的请求函数;
  2. Axios 实例统一管理请求前缀与超时时间;
  3. 开发阶段由 Vite Mock 返回符合约定的数据;
  4. 后端接口就绪后,只切换请求基地址,不改页面的调用方式。

本文围绕这个过程,搭建一个 React + Vite 的 Todo 应用骨架,并详细拆解路由懒加载、Axios 实例、Mock 接口、useStateuseEffect、工程配置和当前实现边界。

一、先明确应用的职责边界

这个应用的技术方向可以分成两部分:

  • 前端:React、React Router、Axios、Vite Mock,以及后续可使用的 Zustand;
  • 后端:Node.js + Koa,目标是提供 /todos 接口,MySQL 属于后续的数据持久化规划。

当前真正打通的是前端开发链路:

text 复制代码
浏览器页面
  → React Router 匹配 /todos
  → Todos 页面调用 getTodos()
  → Axios 请求 /api/todos
  → Vite Mock 拦截请求
  → 返回 Todo JSON
  → setTodos 更新组件状态

这里需要特别区分"规划"和"已经实现":Koa 已经被声明为后端依赖,但后端服务入口和真实 /todos 路由还没有编写;Zustand 已经被加入前端依赖,但当前 Todo 数据仍由组件内的 useState 管理;Todo 数据已经进入状态,页面却还没有把数组渲染出来。

因此,这不是一个已经完成增删改查的 Todo 产品,而是一套已经把页面路由、接口封装和 Mock 取数串起来的前端工程骨架。

二、项目结构:页面、接口和 Mock 各司其职

只保留与本文主线直接相关的目录后,结构可以整理为:

text 复制代码
todos-fullstack/
├── backend/
│   └── package.json
└── frontend/todos/
    ├── mock/
    │   └── todos.js
    ├── src/
    │   ├── api/
    │   │   ├── config.js
    │   │   └── todos.js
    │   ├── components/
    │   │   └── Nav.jsx
    │   ├── pages/
    │   │   ├── Home.jsx
    │   │   └── Todos.jsx
    │   ├── App.jsx
    │   └── main.jsx
    ├── package.json
    └── vite.config.js

这个结构里有两类"路由",但它们不是同一种东西:

  • pages 下的页面由 React Router 处理,例如 //todos
  • api 下的代码负责发起数据请求,例如 /api/todos,它不属于 React Router 的管理范围。

换句话说,/todos 是浏览器页面地址,/api/todos 是数据接口地址。名称相似,职责完全不同。

三、React 应用入口

React 入口负责找到 HTML 中的 root 挂载点并渲染应用:

jsx 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

createRoot 创建 React 根节点,App 是整个组件树的起点。

StrictMode 只包裹组件,不会生成额外 DOM。它会在开发环境中帮助发现不纯的渲染和 Effect 清理问题。对当前 Todo 页面来说,一个直观现象是开发时 Effect 可能被额外执行一次,因此可能观察到不止一次 GET 请求;生产构建不会保留这种开发检查行为。

四、用 React Router 管理页面级路由

应用有两个页面:

  • / 对应首页;
  • /todos 对应 Todo 页面。

路由入口如下:

jsx 复制代码
import React, { lazy, Suspense } from "react";
import { Routes, Route, BrowserRouter as Router } from "react-router-dom";
import Nav from "./components/Nav";

const Home = lazy(() => import("./pages/Home"));
const Todos = lazy(() => import("./pages/Todos"));

function App() {
  return (
    <Router>
      <Nav />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/todos" element={<Todos />} />
      </Routes>
    </Router>
  );
}

export default App;

1. BrowserRouter 提供路由上下文

BrowserRouter 被重命名为 Router,它包裹导航和路由表。LinkRoutesRoute 等路由组件只有放在这个上下文中才能协同工作。

Nav 位于 Routes 外部,所以访问首页或 Todo 页时它都会保留。Routes 负责从一组 Route 中找出与当前地址匹配的页面。

2. lazy 让页面按需加载

jsx 复制代码
const Home = lazy(() => import("./pages/Home"));
const Todos = lazy(() => import("./pages/Todos"));

静态 import 会在应用入口加载时直接引入模块,而 lazy 配合动态 import(),会把页面变成按需加载的模块。实际生产构建中,HomeTodos 会形成独立的 JavaScript 产物,这说明页面级代码分割已经生效。

当前代码已经导入 Suspense,却没有真正使用。也就是说,懒加载页面等待下载时没有显式的加载占位内容。保持原结构不变,可以把路由表补成:

jsx 复制代码
<Router>
  <Nav />
  <Suspense fallback={<div>Loading...</div>}>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/todos" element={<Todos />} />
    </Routes>
  </Suspense>
</Router>

这样既使用了已经导入的 Suspense,也给页面模块加载过程提供了明确反馈。

导航组件非常精简:

jsx 复制代码
import { Link } from "react-router-dom";

function Nav() {
  return (
    <nav style={{ padding: 10, borderBottom: "1px solid #ccc" }}>
      <Link to="/">Home</Link>
      <Link to="/todos">Todos</Link>
    </nav>
  );
}

export default Nav;

to 对应前面声明的页面路径。样式通过 JSX 的 style 对象传入,因此属性名使用 JavaScript 写法,数值 10 会被 React 解释为像素值。

首页组件目前只返回 Home 文本。它的作用主要是验证 / 路由和页面懒加载链路。

五、为什么需要独立的 API 层

页面当然可以直接写:

js 复制代码
axios.get("/api/todos")

但如果每个组件都自行决定域名、前缀、超时和响应解析,后端地址发生变化时,就要在许多组件里逐个修改。独立 API 层把这种变化收敛到固定位置。

当前接口工程分成两层:

text 复制代码
config.js:管理 Axios 的公共配置
todos.js:管理 Todo 领域的接口函数

1. 创建统一的 Axios 实例

js 复制代码
import axios from "axios";

const instance = axios.create({
  baseURL: "/api",
  timeout: 5000
});

export default instance;

这里没有直接使用全局 axios,而是通过 axios.create 得到一个实例:

  • baseURL: "/api" 统一添加接口前缀;
  • timeout: 5000 表示请求最多等待 5000 毫秒。

开发阶段使用相对地址 /api,请求会发给当前 Vite 开发服务器,正好可以被 Mock 插件处理。

2. 一个业务模块管理一类接口

Todo 接口被封装成一个普通异步函数:

js 复制代码
import instance from "./config";

export const getTodos = async () => {
  const res = await instance.get("/todos");
  return res.data;
};

Axios 会把实例的 baseURL 和请求路径组合起来:

text 复制代码
baseURL       /api
请求路径      /todos
最终地址      /api/todos

instance.get 返回的是 Axios 响应对象,真正的接口响应体位于 res.data,所以 getTodos 只把业务需要的数据交给调用方。页面不需要知道 Axios 响应对象的其他字段,也不需要关心当前数据来自 Mock 还是真实后端。

这层封装体现了一个很重要的边界:组件负责"什么时候获取数据、拿到数据后怎么更新界面",API 模块负责"向哪里发请求、如何取得响应体"。

六、用 Vite Mock 在开发阶段提供接口

前端要在后端接口完成之前独立运行,需要让 Vite 开发服务器临时提供 /api/todos

Vite 配置同时启用了 React 插件和 Mock 插件:

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",
      localEnabled: true
    })
  ],
})

mockPath: "mock" 表示 Mock 定义放在项目根目录的 mock 目录中。配置里的 localEnabled: true 表达的是"本地启用 Mock"的意图,但当前安装的 vite-plugin-mock 3.0.2 已经没有这个选项,实际可用的开关名是 enable

这里之所以仍能在开发模式启用 Mock,是因为 3.0.2 默认在 Vite 的 serve 命令下开启,而不是 localEnabled 产生了作用。与当前版本对齐后,可以写成:

js 复制代码
viteMockServe({
  mockPath: "mock",
  enable: true
})

如果只需要默认的"开发时开启",也可以仅保留 mockPath,让插件使用默认值。这个版本差异很重要:配置对象不会因为属性名字看起来合理就自动生效,最终仍要以已安装版本实际支持的选项为准。

Todo Mock 接口定义为:

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 }
        ]
      };
    }
  }
];

这份定义同时约定了接口地址、请求方法、延迟和响应结构:

  • 只匹配 GET /api/todos
  • 延迟 2000 毫秒返回,便于观察异步请求过程;
  • code: 0 表示成功;
  • todos 是页面真正需要的数组;
  • 每个 Todo 包含 idtitlecompleted

Mock 延迟是 2000 毫秒,而 Axios 超时是 5000 毫秒,所以正常情况下响应会在超时前返回。

responsereqres 参数目前没有参与任何计算,这会触发 ESLint 的未使用变量规则。当前插件版本的普通 response 回调接收的是一个请求信息对象;只有需要读取 URL、body、query 或 headers 时才有必要声明它。这里返回的是固定数据,可以保持行为不变,直接写成:

js 复制代码
response: () => {
  return {
    code: 0,
    todos: [
      { id: 1, title: "学习前端接口工程", completed: true },
      { id: 2, title: "学习后端接口工程", completed: false }
    ]
  };
}

七、在组件中完成异步取数和状态更新

Todo 页面把接口数据保存到本地状态:

jsx 复制代码
import { getTodos } from "../api/todos";
import { useEffect, useState } from "react";

function Todos() {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    // IIFE:立即执行函数
    (async () => {
      const data = await getTodos();
      setTodos(data.todos);
    })();
  }, []);

  return (
    <>
      Todos
    </>
  );
}

export default Todos;

1. useState([]) 为什么从空数组开始

jsx 复制代码
const [todos, setTodos] = useState([]);

接口最终返回 Todo 数组,因此初始状态也使用数组。首次渲染时数据尚未到达,todos 是空数组;请求完成后,setTodos(data.todos) 写入两条 Mock 数据,并触发组件重新渲染。

2. useEffect 为什么使用空依赖数组

jsx 复制代码
useEffect(() => {
  // 获取数据
}, []);

空依赖数组表达的是:这段 Effect 不依赖组件中的某个变化值,用于组件进入页面后的初始化取数。它和入口处的 StrictMode 一起使用时,需要记住开发环境可能执行额外检查,不能简单地把开发阶段观察到的重复 GET 当成生产行为。

3. 为什么在 Effect 里使用异步 IIFE

getTodos 是异步函数,需要使用 await。当前写法没有直接把 Effect 回调声明为 async,而是在同步回调内部创建并立即执行一个异步函数:

jsx 复制代码
(async () => {
  const data = await getTodos();
  setTodos(data.todos);
})();

这个函数不需要名称,定义完成后立刻通过末尾的 () 执行,因此称为 IIFE(立即执行函数)。

4. 数据已经进入状态,但还没有进入视图

这是当前实现最容易误判的一点。setTodos(data.todos) 已经完成状态更新,但返回的 JSX 只有静态文本 Todos,没有读取 todos

jsx 复制代码
return (
  <>
    Todos
  </>
);

所以打开页面只能看到标题文本,看不到两条任务。请求链路"已经打通"和列表"已经渲染"是两件不同的事。

这也解释了 ESLint 为什么会报告 todos is assigned a value but never used:状态被声明和写入,却没有在渲染逻辑中消费。下一步应围绕现有 todos 状态补上列表展示,而不是重新设计请求层。

八、局部状态与 Zustand 的当前边界

前端依赖中已经包含 Zustand,整体规划也把 React、路由和状态管理视为独立前端应用的三块能力:

  • React 负责组件和响应式渲染;
  • React Router 负责页面切换;
  • 状态管理负责集中保存和分发共享状态。

不过,当前 Todo 数据只在 Todos 页面内部使用,实际代码选择的是 useState,并没有创建 Zustand store。不能因为依赖已经安装,就把尚未出现的 store、action 或跨组件共享描述成已经实现。

从现状看,路由与接口层已经落地,状态管理仍停留在局部组件状态阶段。

九、真实后端目前处于什么状态

后端目前只声明了 Koa 依赖:

json 复制代码
{
  "dependencies": {
    "koa": "^3.2.1"
  }
}

当前还没有后端入口、启动命令和接口路由。MySQL 只存在于架构规划中,依赖和连接代码尚未出现。

因此,现在能够为前端提供数据的是 Vite Mock,而不是 Koa。

当后端将来在 http://localhost:3000/todos 提供相同结构的数据时,API 模块的调用方式可以保持不变:

js 复制代码
export const getTodos = async () => {
  const res = await instance.get("/todos");
  return res.data;
};

只需把 Axios 实例的基地址从开发期 Mock 前缀切到后端服务地址:

js 复制代码
const instance = axios.create({
  baseURL: "http://localhost:3000",
  timeout: 5000
});

这个切换之所以简单,是因为页面从一开始依赖的就是 getTodos(),而不是某个写死在组件里的服务器地址。真正连接前后端的,是已经约定好的请求路径和 JSON 结构。

十、工程自检暴露出的未完成项

当前生产构建能够成功,并且路由页面会被拆分为独立产物;但 lint 仍有 5 个错误:

  1. Mock 响应函数的 req 未使用;
  2. Mock 响应函数的 res 未使用;
  3. App.jsx 的默认 React 导入未使用;
  4. Suspense 已导入但未渲染;
  5. todos 状态已声明但未参与 JSX。

这些错误不是随机出现的,它们准确反映了代码的完成度:Mock 暂时不读取请求参数,现代 JSX 写法不需要默认 React 变量,懒加载边界还未补齐,Todo 列表还未展示。

十一、一次请求究竟经历了什么

把前面的模块串起来,访问 /todos 后会发生以下过程:

  1. BrowserRouter 根据地址匹配 /todos
  2. lazy 加载 Todos 页面模块;
  3. Todos 首次渲染,todos 的值是空数组;
  4. useEffect 执行异步 IIFE;
  5. IIFE 调用 getTodos()
  6. getTodos() 使用 Axios 实例请求 /todos
  7. Axios 把 baseURL 与路径组合成 /api/todos
  8. Vite Mock 匹配 GET 请求并等待 2000 毫秒;
  9. Mock 返回包含两条任务的对象;
  10. API 函数取出 res.data
  11. 页面执行 setTodos(data.todos)
  12. React 重新渲染组件,但因为 JSX 尚未读取 todos,界面仍只显示 Todos

这一条链路中,页面、请求配置、业务接口和 Mock 数据是分开的。任何一层都只承担自己的职责,后端到位后也只需要替换数据来源。

十二、当前实现的完整度清单

为了避免"代码能构建"与"功能已完成"混为一谈,可以用下面这张表检查进度:

模块 当前状态 说明
React 入口 已完成 createRoot 渲染 App,并启用 StrictMode
页面路由 已完成 //todos 均已声明
导航 已完成 Link 可切换两个页面
页面懒加载 基本完成 已使用 lazy,但缺少实际的 Suspense 边界
Axios 公共配置 已完成 /api 前缀与 5 秒超时已统一管理
Todo API 模块 已完成 getTodos 返回 res.data
Mock 接口 已完成 GET /api/todos 返回两条 Todo 数据
本地状态 已完成 数据会写入 todos
Todo 列表视图 未完成 JSX 尚未消费 todos
Zustand 未使用 只有依赖和技术规划,没有 store 实现
Koa 接口 未完成 只有 Koa 依赖,没有服务入口和路由
MySQL 未实现 只存在于后端技术规划中
生产构建 可通过 页面模块可以正常生成独立构建产物
ESLint 未通过 存在 5 个与未完成代码直接相关的错误

总结

这个 Todo 骨架最值得掌握的,不是页面上暂时只有两个单词,而是前端如何在后端尚未完成时保持独立开发能力:

  • React Router 把页面组织为 //todos
  • lazy 为页面级按需加载建立基础,Suspense 还需要真正接入;
  • Axios 实例集中管理 /api 前缀和超时时间;
  • getTodos 把组件与具体请求细节隔开;
  • Vite Mock 用同一份接口契约提供开发数据;
  • useEffect 在页面初始化时取数,useState 保存响应数组;
  • 构建已经通过,但 lint 清楚指出了尚未完成的展示与清理工作;
  • Koa、MySQL 和 Zustand 是后续方向,不能当作已经实现的功能。

前后端分离并不意味着两端毫无联系。它们仍然通过 URL、请求方法和 JSON 结构协作。真正降低等待和改动成本的关键,是让页面依赖稳定的前端 API 函数,再让 Mock 与真实后端先后履行同一份数据约定。

相关推荐
yuqifang1 小时前
var deferred = jQuery.Deferred(); 使用示例
前端·javascript·面试
阳火锅1 小时前
领导夸我日报越写越详细了,其实我只敲了 npm run commit
前端·javascript·人工智能
lichenyang4531 小时前
实时团队邀请的架构与数据流
前端·后端
一鸣AI编程1 小时前
AI UI 测试平台从 0 到 1:8 次跑通一条 19 步用例的实战复盘
前端
PedroQue991 小时前
Vite 1.2.0发布:自动生成pages.json
前端·vite
ly76891 小时前
React 19 从入门到工程实践:核心原理、新特性与项目实战
前端·react.js·前端框架
tedcloud1231 小时前
diagram-design 怎么安装?用 AI 自动生成更专业的架构图、流程图
linux·运维·前端·人工智能·开源·流程图
何智超1 小时前
React 18 并发更新:高优先级先渲染,为什么最终状态不会算错?
前端
前端小万1 小时前
两个半月的时间如何赚到 3000 块钱。
前端