后端接口还没交付,前端如何独立把整套业务跑通

那个让我卡住的下午

说实话,刚开始做这个 Todos 项目的时候,我脑子里的 "前后端分离" 就是一句口号。

我把 React 组件写好了,路由配好了,页面也能跳转了。然后到了 Todos 页面,要展示一个任务列表 ------ 我下意识地写了个 fetch('http://localhost:3000/todos'),一跑,控制台红了一片。

哦对,后端还没开始写呢。

当时我懵了。前端页面都搭好了,就差数据,总不能在那儿干等后端吧?但不等的话,我连列表长什么样都没法验证。那段时间我甚至想过直接在组件里写死一个数组 const todos = [{id:1, title:'...'}],等后端好了再换掉 ------ 现在想想,这办法不是不行,但太糙了,后面替换的时候肯定漏。

后来我去翻了几个开源项目的源码,才发现人家前端根本不依赖后端。人家有自己的一套 "接口层",后端没好的时候用假数据顶着,后端好了改一行配置就切过去。

这篇文章就是我把这套东西搞明白的过程。

前后端分离,到底分离了个啥

在说怎么解决之前,我得先把我之前理解错的地方掰过来。

以前我以为前后端分离就是 "前端写页面,后端写接口,用 JSON 通信"。这话没错,但太浅了。真正的分离是 ------前端是一个独立的应用,后端是另一个独立的应用,它们之间唯一的耦合点是接口约定

什么意思呢?前端这一侧,其实有自己的 "三驾马车":

  • 组件:负责页面长什么样,数据变了自动刷新
  • 路由:负责哪个 URL 显示哪个页面
  • 状态管理:负责组件之间共享数据,像个银行,存了取、取了存

这三样东西凑齐,前端本身就是一个完整的应用了。它不依赖后端也能跑 ------ 只是没有真实数据而已。

那数据从哪来?从接口来。但接口不一定非得是后端提供的,前端自己也能 "造" 一个接口出来,只要返回的 JSON 格式和后端将来返回的一样就行。

这就是 mock 的意义:前端先把接口契约定下来,自己造数据把整个应用跑通,等后端把真接口写好了,改个地址就接上了。

我后来的理解前后端分离的核心不是 "用 JSON 通信",而是 "前端可以独立开发、独立运行、独立测试"。mock 就是实现这个独立性的关键一环。

项目结构长这样

几个关键目录说一下:

  • src/pages/ ------ 页面级组件,Home 和 Todos
  • src/components/ ------ 可复用组件,比如导航栏 Nav
  • src/api/ ------ 前端接口层,所有请求都从这里走
  • mock/ ------ mock 数据,模拟后端接口

重点是 src/api/ 这个目录。很多初学者(包括以前的我)喜欢在组件里直接 fetch,但那样的话,接口地址散落在各个组件里,后面要改 baseURL 或者加统一拦截器,得一个个找,累死。

把所有请求集中到 api/ 目录,就是把 "数据获取" 这件事从组件里抽出来,单独管理。

先把路由和页面搭起来

这部分比较常规,但还是得说一下,因为后面的 mock 数据是在 Todos 页面里用的。

路由用的是 react-router-dom,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;

这里用了 lazy 懒加载,Home 和 Todos 页面会按需加载,首屏不用把所有代码都拉下来。不过懒加载要配合 Suspense 用,我上面的代码省了,实际项目里要包一层,不然切换路由会报错 ------ 别问我为什么知道,试了三次。

导航栏就两个链接:

javascript 复制代码
import { Link } from 'react-router-dom';

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

export default Nav;

Home 页面就是个占位,重点在 Todos 页面。

axios 配置:为什么不用 fetch

到了接口层,第一个选择是用什么发请求。原生 fetch 当然能用,但它有个问题 ------ 功能太少了。

你想统一加个超时?fetch 得自己包一层 AbortController。你想统一处理错误?fetch 只有网络错误才 reject,HTTP 404、500 它都认为是 "成功"。你想统一加请求头?每个请求都得手动传。

所以我直接上了 axios,先建个实例:

javascript 复制代码
// src/api/config.js
import axios from 'axios';

// 实例化 axios
// fetch 缺点是功能小
// app /api/todos -> :3000/todos
// 统一管理,fetch 升级为 axios
const instance = axios.create({
    baseURL: '/api',  // dev 前端模拟的请求地址 /api/todos
    // baseURL: 'http://localhost:3000',  // 后端好了之后切到这行
    timeout: 5000,
})

export default instance;

注意 baseURL 这一行。现在写的是 /api,意思是所有请求都会自动加上 /api 前缀。比如我调用 axios.get('/todos'),实际请求的是 /api/todos

将来后端写好了,地址是 http://localhost:3000,我只要把 baseURL 改成那个地址就行了,其他代码一行都不用动。

这就是 "接口层" 的价值 ------所有和后端相关的配置都集中在一个地方,切换环境只改一处。

然后在 api/todos.js 里定义具体的接口函数:

javascript 复制代码
// src/api/todos.js
// 一个模块一个 js 文件
import axios from './config';

// api 目录的职责:提供数据接口
// 不是直接去请求后端,后端还没有开发好,前后端分离
export const getTodos = async () => {
    const res = await axios.get('/todos');
    return res.data;
}

很简单,就一个函数。但这个函数就是前端和后端之间的 "契约"------ 不管数据是 mock 的还是真实的,组件调用的方式不变。

mock:前端自己造一个后端

现在问题来了:baseURL/api,那 /api/todos 这个请求发给谁?

如果什么都不做,浏览器会请求当前域名下的 /api/todos,也就是 http://localhost:5173/api/todos,然后返回 404,因为前端开发服务器上根本没有这个接口。

这时候就需要 vite-plugin-mock 了。它的作用是:在 Vite 开发服务器里拦截指定的请求,返回你预先写好的假数据。

bash 复制代码
pnpm i -D vite-plugin-mock

然后在 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
  })],
})

mockPath: 'mock' 的意思是去根目录下的 mock 文件夹里找 mock 定义。然后在 mock/todos.js 里写:

javascript 复制代码
// mock/todos.js
export default [
    {
        url: '/api/todos',
        method: 'get',
        timeout: 2000,  // 模拟网络延迟,2秒后返回
        response: (req, res) => {
            return {
                code: 0, // 成功,没问题
                todos: [
                    { id: 1, title: '学习 React', completed: false },
                    { id: 2, title: '学习 Vue', completed: true },
                    { id: 3, title: '学习 Node.js', completed: false },
                ]
            }
        }
    }
]

这样一来,当浏览器请求 /api/todos 的时候,Vite 开发服务器会拦截这个请求,等 2 秒(模拟网络延迟),然后返回上面那个 JSON。

我跑起来之后,在 Network 面板里看到的是这样:

看到 code: 0 和那三条 todo 的时候,说实话有点小激动 ------ 前端真的不依赖后端就能拿到数据了。

在页面里用起来

最后一步,在 Todos 页面里调用这个接口:

javascript 复制代码
// src/pages/Todos.jsx
import { getTodos } from '../api/todos';
import { useEffect, useState } from 'react';

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

    useEffect(() => {
        // IIFE 立即执行函数表达式
        // 因为 useEffect 的回调不能直接写 async,所以用 IIFE 包一层
        (async () => {
            const data = await getTodos();
            setTodos(data.todos);  // 注意是 data.todos,不是 data
        })()
    }, []);

    return (
        <>
            <h1>Todos</h1>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>{todo.title}</li>
                ))}
            </ul>
        </>
    )
}

export default Todos;

这里有个小细节:useEffect 的回调函数不能直接写成 async,因为 async 函数返回的是 Promise,而 useEffect 期望返回的是清理函数(或者 undefined)。所以用了一个 IIFE(立即执行函数表达式)把异步逻辑包起来。

还有就是 setTodos(data.todos) 这一行 ------ 接口返回的是 { code: 0, todos: [...] },所以要取 data.todos,不是直接 setTodos(data)。我一开始写错了,页面啥也不显示,调了半天才发现。

我踩过的坑

坑一:baseURL 少写了个斜杠

最开始我写的是 baseURL: 'api'(没有前导斜杠),结果请求发出去变成了 http://localhost:5173/todos/api/todos------ 因为相对路径会基于当前页面的 URL 拼接。

改成 baseURL: '/api' 就好了。带前导斜杠的是绝对路径,不带的是相对路径,这个区别坑了我半小时。

坑二:mock 文件没生效

配置完 vite-plugin-mock 之后,我改了 mock 数据,刷新页面,结果返回的还是旧数据。后来才发现,mock 文件的修改需要重启 Vite 开发服务器才能生效 ------ 它不像 React 组件那样有热更新。

不过新版本的 vite-plugin-mock 好像支持热更新了,我用的版本还不行,每次改完得手动 Ctrl+Cpnpm dev

坑三:timeout 不是 axios 的 timeout

mock 里那个 timeout: 2000 是 mock 插件模拟的网络延迟,和 axios 配置里的 timeout: 5000 不是一回事。我一开始以为是同一个东西,还纳闷为什么设了 5 秒超时,请求却总是 2 秒就返回了。

注意mock 的 timeout 是 "等多久再返回数据",axios 的 timeout 是 "最多等多久就认为请求失败"。两个是不同层面的东西。

坑四:生产环境忘了关 mock

localEnabled: true 只在开发环境生效,生产构建的时候 mock 是不会打进去的。但如果你用了 prodEnabled: true,那生产环境也会有 mock------ 这通常不是你想要的。

我目前只在开发环境用 mock,所以只开了 localEnabled

往深里想一层:这套东西到底怎么运转的

搞通了之后,我又好奇了一下:vite-plugin-mock 是怎么拦截请求的?

其实原理不复杂。Vite 的开发服务器是基于 Node.js 的 Connect 中间件框架跑的。vite-plugin-mock 本质上就是往这个服务器里注入了一个中间件 ------ 每当有请求进来,它先检查 URL 和 method 有没有匹配上 mock 定义,如果匹配上了,就直接返回假数据,不再往后传递;如果没匹配上,就交给下一个中间件处理(比如返回 index.html 或者静态资源)。

所以整个请求链路是这样的:

而 axios 的 baseURL 就是给所有请求统一加个前缀。这个前缀在开发环境下刚好和 mock 的 URL 对上,在生产环境下可以改成后端的真实地址。前端代码完全不用改,改的只是配置。

这就是 "接口层" 和 "mock" 配合起来的威力:前端用假数据把整个应用跑通,后端用真数据把接口实现,最后两边一对接,完事。

不是万能的

说了这么多好处,也得说说 mock 什么时候不该用。

第一,如果后端接口已经写好了,就没必要 mock 了,直接连真接口就行。mock 是为了解决 "前后端开发步调不一致" 的问题,后端都好了还 mock 就是多此一举。

第二,mock 数据要和后端约定好格式。如果前端 mock 的返回结构和后端实际返回的不一样,那联调的时候还是要改代码。所以最好前后端先定好接口文档(返回字段、类型、错误码),再各自开发。

第三,mock 只能模拟正常返回,模拟不了真实的网络异常、服务器错误、并发冲突这些。所以 mock 阶段跑通了不代表真的没问题,联调还是得认真测。

最后说几句

搞完这一套之后,我最大的感受是:前端真的不是 "只能写页面"。

以前我总觉得前端得等后端给接口才能干活,现在发现不是 ------ 前端可以自己把数据层、路由层、组件层都搭好,用 mock 数据把整个应用跑起来,等后端好了直接对接。这种独立性才是 "前后端分离" 真正的意思。

如果让我拎三个最关键的点:

  1. api 目录是前端的接口层,所有请求集中管理,baseURL 一键切换环境
  2. mock 让前端可以独立开发,不用等后端,只要接口格式约定好就行
  3. 组件里不要直接发请求,通过 api 层调用,后面改起来才不痛苦

这套东西我也是刚搞明白不久,可能理解还有不到位的地方。你要是有不同的想法或者踩过别的坑,评论区聊聊,我也想看看你的理解。

相关推荐
王琦03182 小时前
WEB服务
前端
霹雳桃2 小时前
Vue3 + Vite 构建版本注入实战:一份 version.json 终结「线上到底是哪一版」
前端
黄金决明子2 小时前
浏览器Window底层操作全解
前端·javascript
ydyd202604213 小时前
设备OEE怎么提升?数据采集+分析优化的完整方案
java·服务器·前端
(╹◡╹)3 小时前
11.RK3588本地大模型内存评估优化
java·linux·前端
程序员跑路3 小时前
Barrel Export、Jest、Babel 与依赖图
react.js
电气研究所3 小时前
Java + Redis 实现工业设备告警去抖,避免变频器故障重复推送
java·前端
AI视觉网奇3 小时前
cannot import name ‘model_urls‘ from ‘torchvision.models.resnet‘
linux·前端·javascript