跳至主要內容

后端眼中的前端:React 核心概念快速上手

郑天祺大约 11 分钟前端React前端

后端眼中的前端:React 核心概念快速上手

前言

作为一个 Java 后端开发者,你可能和我一样:能写出漂亮的后端架构,但一到前端就感觉手足无措。其实,React 的核心概念和后端开发有很多相通之处——组件化思想类似服务拆分、状态管理类似缓存设计、数据流类似消息传递。

本文从一个后端开发者的视角,帮你快速理解 React 的核心概念,掌握关键的思维转换,让你能在需要时写出能用的前端代码。


一、前端发展简史:理解 React 为什么出现

1.1 jQuery 时代(2006-2013)

后端 render HTML → 前端用 jQuery 操作 DOM

$.ajax({
    url: '/api/users',
    success: function(data) {
        $('#user-list').empty();
        data.forEach(function(user) {
            $('#user-list').append('<li>' + user.name + '</li>');
        });
    }
});

问题:当数据变复杂时,DOM 操作和业务逻辑纠缠在一起,代码难以维护。

1.2 三大框架时代(2013-至今)

框架发布时间核心理念适合
Angular2010(AngularJS)大而全的 MVC 框架企业级大型应用
React2013UI 组件库,视图层灵活、生态丰富
Vue2014渐进式框架上手快、中小项目

1.3 现代前端:全栈框架

Next.js (React)  /  Nuxt (Vue)  /  SvelteKit
  ↓
  服务端渲染 + 静态生成 + API Routes
  ↓
  前端和后端的边界逐渐模糊

二、React 核心理念

2.1 与后端概念的类比

后端概念React 对应说明
服务/微服务组件(Component)封装独立功能,可复用
DTO / API 返回体Props父组件传给子组件的数据
缓存(Redis)State组件内部的可变数据
消息队列(数据流)数据从上到下单向流动子组件不能直接改父组件数据
DI 容器Context跨组件共享数据

2.2 三大原则

1. 组件化:UI 拆分为独立、可复用的小块

// 类似后端的 Service 拆分
// UserService.java → UserList.jsx
// OrderService.java → OrderTable.jsx

function UserCard({ name, email, avatar }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

2. 声明式:描述 UI 应该是什么样,而不是怎么操作 DOM

// 命令式(jQuery 风格)—— 告诉浏览器怎么做
$('#status').text('Loading...');
$('#status').addClass('loading');

// 声明式(React 风格)—— 描述 UI 应该是什么样
const [loading, setLoading] = useState(true);
return <div className={loading ? 'loading' : ''}>
  {loading ? 'Loading...' : 'Loaded'}
</div>;

3. 虚拟 DOM:在内存中维护 UI 的快照,通过 Diff 算法最小化真实 DOM 操作

State 变化 → 新虚拟 DOM → Diff 旧虚拟 DOM → 最小化真实 DOM 更新

这类似于后端的"缓存 + 增量更新"思想。


三、函数组件与 Hooks

3.1 函数组件基础

现代 React 推荐函数组件,抛弃了 Class 组件:

// 函数组件 = 一个返回 JSX 的 JavaScript 函数
function Greeting({ name }) {  // props 解构
  return <h1>Hello, {name}!</h1>;
}

// 箭头函数写法
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;

3.2 useState:组件的局部状态

import { useState } from 'react';

function Counter() {
  // 类似后端:在方法内声明一个可变变量
  const [count, setCount] = useState(0);  // 初始值 0

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

关键规则

  • setCount 触发重新渲染(类似调用 render() 方法)
  • State 是不可变的——不能 count++,必须 setCount(count + 1)
  • 每次渲染时,count 是当前渲染的快照值
// ❌ 后端常见的错误写法
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26;  // ❌ 不会触发重新渲染!
// ✅ React 正确写法
setUser({ ...user, age: 26 });  // 创建新对象

3.3 useEffect:副作用处理

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // useEffect 类似后端的 @PostConstruct + 监听器
  useEffect(() => {
    // 组件挂载后执行
    setLoading(true);

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });

    // 返回清理函数(可选)
    return () => {
      console.log('组件卸载,取消请求等清理操作');
    };
  }, [userId]); // 依赖数组:userId 变化时重新执行

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

依赖数组的三种情况

// 1. 无依赖数组:每次渲染都执行
useEffect(() => {
  console.log('Every render');
});

// 2. 空依赖数组:只在首次挂载执行(类似 @PostConstruct)
useEffect(() => {
  console.log('Only on mount');
}, []);

// 3. 有依赖:依赖变化时执行
useEffect(() => {
  console.log('userId changed:', userId);
}, [userId]);

3.4 useContext:跨组件共享数据

// 1. 创建 Context(类似定义全局变量)
const AuthContext = createContext(null);

// 2. Provider 提供数据(类似在 Spring 容器中注册 Bean)
function App() {
  const [user, setUser] = useState(null);

  return (
    <AuthContext.Provider value={{ user, setUser }}>
      <Header />
      <Main />
    </AuthContext.Provider>
  );
}

// 3. 任何子组件都可以使用(类似 @Autowired)
function Header() {
  const { user, setUser } = useContext(AuthContext);  // 直接获取

  return user
    ? <div>Welcome, {user.name}!</div>
    : <button onClick={() => setUser({ name: 'Alice' })}>Login</button>;
}

四、状态管理:从简单到复杂

4.1 状态管理的演进

Props Drilling(逐层传递)
  ↓ 痛点:中间组件被迫接收不关心的 props
Context API(React 内置)
  ↓ 痛点:Provider 过大时性能问题
Zustand / Jotai(轻量级)
  ↓ 适用于中小型应用
Redux Toolkit(重型)
  ↓ 适用于大型复杂应用

4.2 Zustand:后端最容易理解的状态管理

Zustand 的设计很像一个"前端版的单例 Service":

// store.js — 类似后端的 Service 类
import { create } from 'zustand';

const useUserStore = create((set, get) => ({
  // State(类似类的字段)
  user: null,
  token: null,
  isAuthenticated: false,

  // Actions(类似类的方法)
  login: async (username, password) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    });
    const data = await response.json();
    set({ user: data.user, token: data.token, isAuthenticated: true });
  },

  logout: () => {
    set({ user: null, token: null, isAuthenticated: false });
  },

  // Getter(派生状态)
  get displayName() {
    return get().user?.name ?? 'Anonymous';
  }
}));

// 在任何组件中使用
function UserInfo() {
  const user = useUserStore(state => state.user);
  const login = useUserStore(state => state.login);
  const logout = useUserStore(state => state.logout);

  return user
    ? <div>{user.name} <button onClick={logout}>Logout</button></div>
    : <button onClick={() => login('alice', 'pass')}>Login</button>;
}

Zustand 的好处

  • 没有 Provider 包裹(避免深层嵌套)
  • 选择器精细化(只订阅需要的字段,类似懒加载)
  • 可以在组件外使用(类似静态方法)
  • API 少,学习成本低

4.3 Props Drilling 问题与解决

// ❌ Props Drilling:中间组件被迫传递
function App() {
  const [theme, setTheme] = useState('light');
  return <Header theme={theme} setTheme={setTheme} />;
}

function Header({ theme, setTheme }) {
  return <div>
    <Logo />
    <Nav theme={theme} setTheme={setTheme} />  {/* 不关心但必须传 */}
  </div>;
}

function Nav({ theme, setTheme }) {
  return <div>
    <NavItem />
    <ThemeToggle theme={theme} setTheme={setTheme} />  {/* 还是不关心 */}
  </div>;
}

// ✅ 解决方案1:Context
const ThemeContext = createContext();

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
    </ThemeContext.Provider>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
    {theme}
  </button>;
}

// ✅ 解决方案2:Zustand
const useThemeStore = create(set => ({
  theme: 'light',
  toggle: () => set(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' }))
}));

function ThemeToggle() {
  const { theme, toggle } = useThemeStore();
  return <button onClick={toggle}>{theme}</button>;
}

五、React Router:前端路由

import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/users">Users</Link>
        <Link to="/about">About</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users" element={<UserList />} />
        <Route path="/users/:id" element={<UserDetail />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

// 使用路由参数(类似 @PathVariable)
function UserDetail() {
  const { id } = useParams();  // /users/123 → id = "123"
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then(res => res.json())
      .then(setUser);
  }, [id]);

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

// 编程式导航(类似 redirect)
function UserList() {
  const navigate = useNavigate();

  return (
    <div>
      <h2>Users</h2>
      <button onClick={() => navigate('/users/new')}>Create User</button>
      {/* 点击行跳转到详情 */}
      {users.map(u => (
        <div key={u.id} onClick={() => navigate(`/users/${u.id}`)}>
          {u.name}
        </div>
      ))}
    </div>
  );
}

六、前后端数据交互

6.1 Fetch API 封装

// api.js — 封装 HTTP 请求(类似 RestTemplate 的封装)
const BASE_URL = '/api';

class ApiClient {
  constructor() {
    this.token = localStorage.getItem('token');
  }

  setToken(token) {
    this.token = token;
    localStorage.setItem('token', token);
  }

  clearToken() {
    this.token = null;
    localStorage.removeItem('token');
  }

  async request(path, options = {}) {
    const url = `${BASE_URL}${path}`;
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token && { 'Authorization': `Bearer ${this.token}` }),
      ...options.headers,
    };

    const response = await fetch(url, { ...options, headers });

    // 统一的错误处理
    if (response.status === 401) {
      this.clearToken();
      window.location.href = '/login';
      throw new Error('认证已过期');
    }

    const data = await response.json();

    if (data.code !== 0) {
      throw new ApiError(data.code, data.message);
    }

    return data.data;
  }

  get(path, params) {
    const query = params ? '?' + new URLSearchParams(params) : '';
    return this.request(`${path}${query}`);
  }

  post(path, body) {
    return this.request(path, { method: 'POST', body: JSON.stringify(body) });
  }

  put(path, body) {
    return this.request(path, { method: 'PUT', body: JSON.stringify(body) });
  }

  patch(path, body) {
    return this.request(path, { method: 'PATCH', body: JSON.stringify(body) });
  }

  delete(path) {
    return this.request(path, { method: 'DELETE' });
  }
}

class ApiError extends Error {
  constructor(code, message) {
    super(message);
    this.code = code;
  }
}

export const api = new ApiClient();

6.2 在组件中使用

import { api } from './api';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // 通用数据加载 Hook
  const loadUsers = async () => {
    setLoading(true);
    setError(null);
    try {
      const data = await api.get('/users', { page: 1, pageSize: 20 });
      setUsers(data.records);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadUsers();
  }, []);

  if (loading) return <Spinner />;
  if (error) return <ErrorAlert message={error} onRetry={loadUsers} />;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
}

6.3 自定义 Hook:提取复用逻辑

// useApi.js — 通用的数据加载 Hook
function useApi(fetcher, deps = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const execute = async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await fetcher();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    execute();
  }, deps);

  return { data, loading, error, refetch: execute };
}

// 使用
function UserList() {
  const { data: users, loading, error, refetch } = useApi(
    () => api.get('/users', { page: 1 }),
    []
  );

  // ...
}

七、实战:搭建一个调用 Spring Boot API 的 React 页面

7.1 项目结构

frontend/
├── index.html
├── package.json
├── vite.config.js
├── src/
│   ├── main.jsx
│   ├── App.jsx
│   ├── api/
│   │   └── client.js
│   ├── components/
│   │   ├── Layout.jsx
│   │   ├── UserList.jsx
│   │   └── UserForm.jsx
│   └── stores/
│       └── useAuthStore.js

7.2 Vite 配置代理

// vite.config.js - 开发环境代理到 Spring Boot
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',  // Spring Boot 后端
        changeOrigin: true,
      }
    }
  }
};

7.3 用户列表 + 增删改

// UserList.jsx
import { useState, useEffect } from 'react';
import { api } from '../api/client';

export default function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [editing, setEditing] = useState(null); // 正在编辑的用户
  const [formData, setFormData] = useState({ name: '', email: '' });

  useEffect(() => {
    loadUsers();
  }, []);

  const loadUsers = async () => {
    setLoading(true);
    try {
      const data = await api.get('/users');
      setUsers(data.records || data);
    } catch (err) {
      alert('加载失败: ' + err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleCreate = async () => {
    try {
      await api.post('/users', formData);
      setFormData({ name: '', email: '' });
      loadUsers(); // 刷新列表
    } catch (err) {
      alert('创建失败: ' + err.message);
    }
  };

  const handleUpdate = async (id) => {
    try {
      await api.put(`/users/${id}`, editing);
      setEditing(null);
      loadUsers();
    } catch (err) {
      alert('更新失败: ' + err.message);
    }
  };

  const handleDelete = async (id) => {
    if (!confirm('确定删除?')) return;
    try {
      await api.delete(`/users/${id}`);
      loadUsers();
    } catch (err) {
      alert('删除失败: ' + err.message);
    }
  };

  if (loading) return <div>加载中...</div>;

  return (
    <div>
      <h2>用户管理</h2>

      {/* 创建表单 */}
      <div style={{ marginBottom: 20, padding: 10, border: '1px solid #ddd' }}>
        <h3>添加用户</h3>
        <input
          placeholder="姓名"
          value={formData.name}
          onChange={e => setFormData({ ...formData, name: e.target.value })}
        />
        <input
          placeholder="邮箱"
          value={formData.email}
          onChange={e => setFormData({ ...formData, email: e.target.value })}
        />
        <button onClick={handleCreate}>创建</button>
      </div>

      {/* 用户列表 */}
      <table border="1" cellPadding="8">
        <thead>
          <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>邮箱</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {users.map(user => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>
                {editing?.id === user.id ? (
                  <input
                    value={editing.name}
                    onChange={e => setEditing({ ...editing, name: e.target.value })}
                  />
                ) : user.name}
              </td>
              <td>
                {editing?.id === user.id ? (
                  <input
                    value={editing.email}
                    onChange={e => setEditing({ ...editing, email: e.target.value })}
                  />
                ) : user.email}
              </td>
              <td>
                {editing?.id === user.id ? (
                  <>
                    <button onClick={() => handleUpdate(user.id)}>保存</button>
                    <button onClick={() => setEditing(null)}>取消</button>
                  </>
                ) : (
                  <>
                    <button onClick={() => setEditing(user)}>编辑</button>
                    <button onClick={() => handleDelete(user.id)}>删除</button>
                  </>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

八、后端最常碰到前端的坑

坑1:异步数据 + 条件渲染

// ❌ 常见错误:数据未加载时就访问属性
function UserPage({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  return <h1>{user.name}</h1>;  // ❌ user 是 null 时报错!
}

// ✅ 正确处理
function UserPage({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => { /* 同上 */ }, [userId]);

  if (!user) return <div>Loading...</div>;  // 空白状态
  return <h1>{user.name}</h1>;
}

坑2:State 闭包陷阱

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);  // ❌ count 始终是 0!
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖,只执行一次

  return <div>{count}</div>;
  // 结果:永远显示 1
}

// ✅ 方案1:使用函数式更新
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);  // c 始终是最新值
  }, 1000);
  return () => clearInterval(timer);
}, []);

// ✅ 方案2:添加依赖
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);  // 每次 count 变化重设定时器

坑3:useEffect 的依赖问题

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  // ❌ Effect 用了 query 但没声明依赖
  useEffect(() => {
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(setResults);
  }, []); // ← ESLint 警告:缺少 query 依赖

  // ✅ 声明所有依赖
  useEffect(() => {
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(setResults);
  }, [query]);
}

坑4:列表渲染缺少 key

// ❌ 缺少 key
{users.map(user => <li>{user.name}</li>)}

// ✅ 使用唯一稳定的 key(通常用数据库 ID)
{users.map(user => <li key={user.id}>{user.name}</li>)}

// ❌ 不要把 index 作为 key(数据顺序变化时会出错)
{users.map((user, index) => <li key={index}>{user.name}</li>)}

坑5:直接修改 State 对象

// ❌ 后端直觉:修改对象的属性
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26;       // 直接修改
user.city = 'NYC';   // 添加属性
setUser(user);       // React 可能不会重新渲染(引用没变)

// ✅ React 方式:创建新对象
setUser({ ...user, age: 26 });
setUser({ ...user, city: 'NYC' });

// 或者用 immer(简化不可变更新)
import { produce } from 'immer';
setUser(produce(draft => { draft.age = 26; }));

九、总结

后端 → 前端 思维转换对照表

后端思维前端思维
Spring Bean 由容器管理生命周期组件由 React 管理生命周期(挂载、更新、卸载)
Service 调用 → 返回数据异步 fetch → setState → 重新渲染
数据库是数据源State + Props 是 UI 的数据源
多线程处理并发JavaScript 单线程,异步用 Promise/async-await
接口定义与实现分离组件定义即实现(JSX 混合了逻辑和模板)
依赖注入 DIProps 传递 + Context
异常处理 try-catch异步错误 .catch() + ErrorBoundary

学习路线建议(给后端)

  1. JSX 语法(1天):HTML-in-JavaScript 的写法
  2. 函数组件 + Props(1天):理解组件间的数据传递
  3. useState + useEffect(2天):组件的数据与副作用
  4. React Router(半天):前端页面切换
  5. 数据获取(1天):fetch/axios + 自定义 Hook
  6. 状态管理(1天):Zustand 或 Context
  7. 做一个完整项目(3天):CRUD + 登录 + 路由

最后的话

作为后端,你不需要成为 React 专家。但理解 React 的核心概念会让你:

  • 更好地设计 API(知道你返回的数据前端怎么用)
  • 更高效地与前端同事沟通
  • 在必要时能自己写前端页面(小工具、管理后台)

记住:React 只是工具,它解决的是"UI 如何响应数据变化"的问题。你作为一个写过后端复杂逻辑的人,掌握它只是时间问题。


本文基于 React 18 + React Router 6 + Zustand 4 编写。

上次编辑于:
贡献者: zhengtianqi