前后端分离的口号大家都听过,但落到代码上,前端到底怎么"不依赖后端也能独立开发"?答案就藏在这七个文件里。本文用一个 Todos 项目,逐文件拆解前端是如何通过 API 层 + Mock 数据,把和后端的耦合降到最低的。
一句话概括分工:
| 文件 | 职责 |
|---|---|
main.jsx |
启动应用 |
App.jsx |
路由分配页面 |
Nav.jsx |
导航链接 |
Home.jsx / Todos.jsx |
页面内容 |
api/config.js + api/todos.js |
数据请求层(前后端唯一的耦合点) |
① main.jsx:入口,只做一件事
javascript
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 18+ 的新渲染入口,替代老的ReactDOM.render。document.getElementById('root'):找到index.html里的<div id="root">,整个应用渲染进这里。<StrictMode>:开发模式的严格模式,帮你提前暴露一些潜在问题(比如副作用执行两次),生产环境自动失效。
入口文件很薄,这是好事------它只负责"启动",具体逻辑全交给 App。
② App.jsx:路由接管一切(重点:懒加载)
javascript
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. <Router> 路由接管一切
<BrowserRouter>(别名Router):用 HTML5 History API 管理地址栏 URL。<Routes>+<Route>:根据当前 URL 匹配要渲染哪个组件。path="/"渲染Home,path="/todos"渲染Todos。<Nav />放在<Routes>外面,所以导航栏在所有页面都显示,只有下面区域随路由切换。
2. lazy + Suspense 按需加载(懒加载)
ini
const Home = lazy(() => import('./pages/Home'));
lazy(() => import(...)):动态导入,切到某个页面时才去加载那个页面的代码,而不是一开始全部打包加载。- 这样首页首屏加载更快------用户可能根本不去
/todos,那就没必要一开始就下载它的代码。 - 配套的
<Suspense>用来在懒加载完成前显示占位内容(这里暂时没写fallback,实际项目应补上,比如<Suspense fallback={<div>加载中...</div>}>)。
小提醒:代码里
<Suspense>其实没用到(lazy组件没包进去),严格来说这是个待完善点。完整写法应该是:
xml<Suspense fallback={<div>加载中...</div>}> <Routes>...</Routes> </Suspense>
③ Nav.jsx:页面级导航
javascript
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
关键点:为什么用 <Link> 而不是 <a>?
<a href="/todos">会刷新整个页面,重新加载所有资源------SPA(单页应用)的大忌。<Link to="/todos">只改地址栏 + 触发路由切换,不刷新页面,体验流畅。
这是 React Router 最基础也最重要的一个习惯。
④⑤ Home.jsx / Todos.jsx:页面组件
Home.jsx 目前极简:
javascript
function Home() {
return (
<>
Home
</>
)
}
export default Home
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
</>
)
}
export default Todos
逐行看:
useState([]):声明响应式状态todos,初始为空数组。useEffect(..., []):依赖数组为空,表示只在组件挂载时执行一次(等价于"页面加载时发一次请求")。(async () => {...})():IIFE 立即执行函数 。因为useEffect的回调不能直接写成async,所以包一层立即执行的 async 函数,才能在里面用await。await getTodos():调接口拿数据。setTodos(data):拿到数据后更新状态,触发重新渲染。
这里藏着"前后端分离"的雏形 :Todos.jsx 不关心数据从哪来、是真后端还是 Mock,它只管调用 getTodos() 这个接口函数。数据源怎么变,页面代码一行都不用改。
⑥ api/config.js:axios 实例化,baseURL 一键切换
javascript
import axios from 'axios';
const instance = axios.create({
baseURL: '/api', // 前端模拟的请求地址 /api/todos
// baseURL: 'http://localhost:3000', // 切换真实后端时改这里
timeout: 5000,
})
export default instance;
这是全项目最值钱的一个文件,逐行拆:
axios.create(...):创建一个配置好的 axios 实例 ,而不是到处axios.get(...)。baseURL: '/api':所有请求自动加上/api前缀。所以get('/todos')实际请求的是/api/todos。timeout: 5000:5 秒超时,避免请求卡死。- 注释里那行
baseURL: 'http://localhost:3000'是关键:后端接口真正写好之后,只需要改这一行,把/api换成真实后端地址,全项目所有接口自动切换,不用去每个文件里一个个改。
为什么不用原生 fetch,要升级 axios?
fetch 功能太少:没有超时、没有拦截器、错误处理麻烦、要手动 .json()。axios 提供了实例化、超时、拦截器等,是"从 fetch 升级到 axios"的标准姿势。
⑦ api/todos.js:接口统一管理
javascript
import axios from './config';
export const getTodos = async () => {
const res = await axios.get('/todos');
return res.data;
}
这个文件体现了「一个模块一个 js 文件」的接口管理规范:
- 所有和
todos相关的接口,都放这一个文件里,导出给页面用。 - 页面(
Todos.jsx)只import { getTodos } from '../api/todos',不直接接触 axios、不接触 URL。 - 好处:将来接口变化、要加请求头、要统一错误处理,都只需改 api 目录,页面无感知。
这里有个小细节:
axios.get('/todos')拼上 config.js 里的baseURL: '/api',最终请求/api/todos。这个/api/todos正是 Mock 拦截的地址,前后端在这里"接上头"。
附:mock/todos.js ------ 让前端"不傻等后端"的关键
yaml
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 }
]
}
}
}
]
配合 vite.config.js:
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 // 开发环境开启 mock
})],
})
它的作用:
vite-plugin-mock 在开发服务器里拦截 /api/todos 这个请求,直接返回 mock/todos.js 里写死的假数据,根本不走后端。
于是前端在开发阶段就有了自己的"假接口",可以先独立把界面写完、把数据流跑通 。等后端真正把 /api/todos 写好,再把 config.js 的 baseURL 一切换,无缝对接。
串起来:一次请求的完整链路
- 用户访问
/todos→App.jsx路由匹配到<Todos />。 Todos.jsx挂载 →useEffect触发getTodos()。api/todos.js用config.js配置好的 axios 实例,请求baseURL + '/todos'=/api/todos。- 开发阶段
vite-plugin-mock拦截/api/todos,返回mock/todos.js的假数据。 - 数据回到
Todos.jsx,setTodos触发渲染。
整条链路里,页面(Todos.jsx)从头到尾不知道数据是"假"的------这正是前后端分离要达到的效果。
总结:这七个文件教会我们的三件事
| 核心思想 | 落地点 |
|---|---|
| 解耦的关键 = 接口请求 | 前端只认 getTodos(),不认后端 |
| api 目录统一管理接口 | config.js 管配置,todos.js 管接口 |
| baseURL 一键切换 | Mock → 真实后端只改一行 |
再加一句工程方法论:前端永远不要傻等后端接口 。先建好 API 层 + Mock 数据,把界面和交互完整做出来;后端接口好了之后,切一行 baseURL 就能连调。这就是"前后端分离"在代码层面的真正含义。