React Realworld 项目深度讲解 —— 路由配置与前后端交互

1 阅读6分钟

React Realworld 项目深度讲解 —— 路由配置与前后端交互

项目地址: github.com/gothinkster… 技术栈: React + Redux + React Router + Superagent 项目定位: Medium.com 的克隆版(代号 Conduit),RealWorld 规范的官方参考实现


一、项目概览与目录结构

src/
├── index.js                 # 应用入口,挂载 Provider + Router
├── store.js                 # Redux Store 创建 + 中间件配置
├── reducer.js               # 根 Reducer(combineReducers)
├── middleware.js             # 自定义中间件(Promise处理 + localStorage)
├── agent.js                 # ⭐ HTTP 请求层(前后端交互核心)
├── constants/
│   └── actionTypes.js       # 所有 Action Type 常量
├── reducers/                # 各模块 Reducer
│   ├── common.js            # 全局状态(登录态、重定向)
│   ├── auth.js              # 认证状态
│   ├── article.js           # 文章详情
│   ├── articleList.js       # 文章列表
│   ├── editor.js            # 编辑器
│   ├── home.js              # 首页
│   ├── profile.js           # 用户资料
│   └── settings.js          # 设置
└── components/              # 页面组件
    ├── App.js               # ⭐ 路由总配置
    ├── Header.js            # 导航栏
    ├── Login.js             # 登录页
    ├── Register.js          # 注册页
    ├── Editor.js            # 文章编辑
    ├── Settings.js          # 设置页
    ├── Profile.js           # 用户主页
    ├── Home/                # 首页(含 Banner, Tags, MainView)
    └── Article/             # 文章详情(含 Meta, Comment 等子组件)

架构模式: 经典的 React + Redux 单向数据流,以 agent.js 作为统一的 API 层与后端交互。


二、路由配置深度解析

2.1 路由的初始化(入口文件)

文件: src/index.js

import { Provider } from 'react-redux';
import { store, history } from './store';
import { ConnectedRouter } from 'react-router-redux';

ReactDOM.render((
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Switch>
        <Route path="/" component={App} />
      </Switch>
    </ConnectedRouter>
  </Provider>
), document.getElementById('root'));

关键点:

  • ConnectedRouter 而不是普通的 BrowserRouter:将路由状态同步到 Redux Store,实现路由与状态管理的联动
  • history 从 store.js 导出:确保 Router 和 Redux 共用同一个 history 实例
  • 根路由 path="/" 匹配所有路径,将一切交给 App 组件处理

2.2 核心路由表(App.js)

文件: src/components/App.js

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/login" component={Login} />
  <Route path="/register" component={Register} />
  <Route path="/editor/:slug" component={Editor} />
  <Route path="/editor" component={Editor} />
  <Route path="/article/:id" component={Article} />
  <Route path="/settings" component={Settings} />
  <Route path="/@:username/favorites" component={ProfileFavorites} />
  <Route path="/@:username" component={Profile} />
</Switch>

路由设计要点:

  • / → Home:首页,exact 精确匹配
  • /login → Login:登录页
  • /register → Register:注册页
  • /editor/:slug → Editor:编辑已有文章(带动态参数)
  • /editor → Editor:新建文章(无参数)
  • /article/:id → Article:文章详情(动态参数)
  • /settings → Settings:用户设置
  • /@:username/favorites → ProfileFavorites:用户收藏文章
  • /@:username → Profile:用户主页

进阶细节:

  1. /editor/:slug 在 /editor 之前:React Router 的 Switch 从上到下匹配,带参数的路径必须写在前面,否则 /editor 会"吃掉"所有编辑路由
  2. /@:username 模式:使用 @ 前缀模仿 Medium/Twitter 的用户主页 URL 设计,:username 作为动态参数
  3. /@:username/favorites 在 /@:username 之前:同理,更具体的路径必须放前面

2.3 路由与 Redux 的联动(编程式导航)

文件: src/store.js

import { routerMiddleware } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';

export const history = createHistory();
const myRouterMiddleware = routerMiddleware(history);

export const store = createStore(
  reducer,
  composeWithDevTools(
    applyMiddleware(myRouterMiddleware, promiseMiddleware, localStorageMiddleware)
  )
);

文件: src/components/App.js 中的重定向逻辑

import { push } from 'react-router-redux';

componentWillReceiveProps(nextProps) {
  if (nextProps.redirectTo) {
    store.dispatch(push(nextProps.redirectTo));
    this.props.onRedirect();
  }
}

这套机制的设计思路:

  1. routerMiddleware 拦截 push/replace 等路由 Action,转化为真正的 history 操作
  2. 任何需要页面跳转的地方(登录成功、文章发布成功),只需在 Reducer 中设置 redirectTo
  3. App 组件监听 redirectTo,通过 dispatch(push(url)) 完成跳转
  4. 跳转后立即 dispatch({ type: REDIRECT }) 清空 redirectTo,防止重复跳转

典型流转:

用户登录成功
  → LOGIN action 触发
  → common reducer 设置 redirectTo: '/'
  → App.componentWillReceiveProps 检测到 redirectTo
  → dispatch(push('/'))
  → 页面跳转到首页
  → dispatch(REDIRECT) 清空 redirectTo

三、前后端交互核心(agent.js)

3.1 HTTP 客户端封装

文件: src/agent.js

import superagentPromise from 'superagent-promise';
import _superagent from 'superagent';

const superagent = superagentPromise(_superagent, global.Promise);
const API_ROOT = 'https://conduit.productionready.io/api';

const responseBody = res => res.body;

let token = null;
const tokenPlugin = req => {
  if (token) {
    req.set('authorization', `Token ${token}`);
  }
};

const requests = {
  del: url =>
    superagent.del(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody),
  get: url =>
    superagent.get(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody),
  put: (url, body) =>
    superagent.put(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody),
  post: (url, body) =>
    superagent.post(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody)
};

设计亮点:

  1. 统一 API 基地址:所有请求拼接 API_ROOT,切换环境只改一处
  2. Token 自动注入:通过 superagent 的 plugin 机制(.use(tokenPlugin)),每个请求自动携带 Authorization 头
  3. 响应统一提取:.then(responseBody) 只提取 res.body,上层代码不需要关心 HTTP 细节
  4. RESTful 封装:get/post/put/del 对应 CRUD 操作

3.2 业务 API 按领域分组

const Auth = {
  current: () => requests.get('/user'),
  login: (email, password) =>
    requests.post('/users/login', { user: { email, password } }),
  register: (username, email, password) =>
    requests.post('/users', { user: { username, email, password } }),
  save: user => requests.put('/user', { user })
};

const Articles = {
  all: page => requests.get(`/articles?${limit(10, page)}`),
  byAuthor: (author, page) =>
    requests.get(`/articles?author=${encode(author)}&${limit(5, page)}`),
  byTag: (tag, page) =>
    requests.get(`/articles?tag=${encode(tag)}&${limit(10, page)}`),
  feed: () => requests.get('/articles/feed?limit=10&offset=0'),
  get: slug => requests.get(`/articles/${slug}`),
  create: article => requests.post('/articles', { article }),
  update: article =>
    requests.put(`/articles/${article.slug}`, { article: omitSlug(article) }),
  del: slug => requests.del(`/articles/${slug}`),
  favorite: slug => requests.post(`/articles/${slug}/favorite`),
  unfavorite: slug => requests.del(`/articles/${slug}/favorite`)
};

const Comments = {
  create: (slug, comment) =>
    requests.post(`/articles/${slug}/comments`, { comment }),
  delete: (slug, commentId) =>
    requests.del(`/articles/${slug}/comments/${commentId}`),
  forArticle: slug => requests.get(`/articles/${slug}/comments`)
};

const Profile = {
  follow: username => requests.post(`/profiles/${username}/follow`),
  get: username => requests.get(`/profiles/${username}`),
  unfollow: username => requests.del(`/profiles/${username}/follow`)
};

架构价值:

  • 组件层永远不直接发 HTTP 请求,只通过 agent.Auth.login() 这样的语义化调用
  • API 端点变更只影响 agent.js,不影响任何组件
  • 每个领域(Auth/Articles/Comments/Profile)职责清晰

四、Redux 中间件 —— 前后端交互的桥梁

4.1 Promise 中间件(核心!)

文件: src/middleware.js

const promiseMiddleware = store => next => action => {
  if (isPromise(action.payload)) {
    store.dispatch({ type: ASYNC_START, subtype: action.type });

    action.payload.then(
      res => {
        action.payload = res;
        store.dispatch({ type: ASYNC_END, promise: action.payload });
        store.dispatch(action);
      },
      error => {
        action.error = true;
        action.payload = error.response.body;
        store.dispatch({ type: ASYNC_END, promise: action.payload });
        store.dispatch(action);
      }
    );
    return;
  }
  next(action);
};

这是整个前后端交互的核心枢纽。工作流程:

组件 dispatch({ type: LOGIN, payload: agent.Auth.login(email, pwd) })
  ↓ payload 是一个 Promise
中间件拦截
  → dispatch(ASYNC_START)        → 通知 UI 显示 loading
  → 等待 Promise resolve/reject
  → 成功:payload 替换为真实数据  → dispatch(LOGIN) 带数据
  → 失败:payload 替换为错误信息  → dispatch(LOGIN) 带 error 标志
  → dispatch(ASYNC_END)          → 通知 UI 关闭 loading

设计精妙之处:

  1. 组件完全不关心异步:只需要 dispatch({ type: LOGIN, payload: somePromise })
  2. loading 状态自动管理:ASYNC_START/ASYNC_END 全局处理
  3. 错误统一处理:通过 action.error = true 标记,Reducer 可以统一判断

4.2 localStorage 中间件

const localStorageMiddleware = store => next => action => {
  if (action.type === REGISTER || action.type === LOGIN) {
    if (!action.error) {
      window.localStorage.setItem('jwt', action.payload.user.token);
      agent.setToken(action.payload.user.token);
    }
  } else if (action.type === LOGOUT) {
    window.localStorage.setItem('jwt', '');
    agent.setToken(null);
  }
  next(action);
};

职责: 登录/注册成功后自动持久化 Token,登出时清除。这样 agent.js 的 tokenPlugin 能自动在后续请求中带上认证信息。


五、完整数据流示例 —— 用户登录

以登录流程为例,串联所有知识点:

Step 1:组件层(Login.js)

const mapDispatchToProps = dispatch => ({
  onSubmit: (email, password) =>
    dispatch({ type: LOGIN, payload: agent.Auth.login(email, password) })
});

// 表单提交
this.submitForm = (email, password) => ev => {
  ev.preventDefault();
  this.props.onSubmit(email, password);
};

Step 2:API 层(agent.js)

Auth: {
  login: (email, password) =>
    requests.post('/users/login', { user: { email, password } })
}
// 发送 POST https://conduit.productionready.io/api/users/login
// Body: { user: { email, password } }

Step 3:Promise 中间件拦截

dispatch({ type: LOGIN, payload: Promise })
  → 中间件发现 payload 是 Promise
  → dispatch(ASYNC_START) → auth reducer 设 inProgress: true → 按钮变 disabled
  → Promise 成功 → payload 变为 { user: { token, username, ... } }
  → dispatch(LOGIN) 带真实数据

Step 4:localStorage 中间件

收到 LOGIN action,无 error
  → localStorage.setItem('jwt', token)
  → agent.setToken(token)
  → 后续所有请求自动带 Authorization 头

Step 5:Reducers 处理

auth reducer:

case LOGIN:
  return {
    ...state,
    inProgress: false,
    errors: action.error ? action.payload.errors : null
  };

common reducer:

case LOGIN:
  return {
    ...state,
    redirectTo: action.error ? null : '/',
    token: action.error ? null : action.payload.user.token,
    currentUser: action.error ? null : action.payload.user
  };

Step 6:路由跳转

common.redirectTo 变为 '/'
  → App.componentWillReceiveProps 检测到
  → dispatch(push('/'))
  → 页面跳转到首页
  → dispatch(REDIRECT) 清空 redirectTo

六、页面加载与数据获取模式

6.1 通用模式

每个页面组件遵循统一模式:

class SomePage extends React.Component {
  componentWillMount() {
    // 进入页面 → 发起数据请求
    this.props.onLoad(agent.SomeAPI.getData());
  }

  componentWillUnmount() {
    // 离开页面 → 清理状态
    this.props.onUnload();
  }
}

6.2 首页加载示例

// Home/index.js
componentWillMount() {
  const tab = this.props.token ? 'feed' : 'all';
  const articlesPromise = this.props.token
    ? agent.Articles.feed
    : agent.Articles.all;

  this.props.onLoad(
    tab,
    articlesPromise,
    Promise.all([agent.Tags.getAll(), articlesPromise()])
  );
}

逻辑: 根据是否登录,展示"我的订阅"或"全部文章"。用 Promise.all 并行请求标签和文章,优化加载性能。

6.3 文章详情加载

// Article/index.js
componentWillMount() {
  this.props.onLoad(Promise.all([
    agent.Articles.get(this.props.match.params.id),
    agent.Comments.forArticle(this.props.match.params.id)
  ]));
}

注意: this.props.match.params.id 来自路由参数 /article/:id,React Router 自动注入。


七、关键设计模式总结

7.1 单一数据源 + 单向数据流

用户操作 → dispatch(Action + Promise) → 中间件处理异步 → Reducer 更新 Store → 组件重新渲染

所有状态存在 Redux Store 中,组件只是状态的映射。

7.2 API 层完全解耦

组件 → agent.js(语义化 API) → superagent(HTTP 库) → RESTful 后端

组件不知道后端 URL 是什么,甚至不知道用的是哪个 HTTP 库。

7.3 中间件拦截模式

Promise 中间件就像一个"异步翻译器":

  • 输入:带 Promise 的 Action
  • 输出:带真实数据的 Action + ASYNC_START/END 生命周期事件

7.4 视图计数器防竞态

const currentView = store.getState().viewChangeCounter;
// ...Promise resolve 后检查...
if (currentState.viewChangeCounter !== currentView) {
  return; // 用户已经离开该页面,丢弃结果
}

用户快速切换页面时,旧页面的请求返回后不会污染新页面的状态。这是一个容易被忽略但非常重要的细节。


八、值得注意的不足与改进方向

  1. Class 组件 → Hooks:项目使用 Class 组件 + 生命周期方法(componentWillMount 已废弃),现代项目应使用 useEffect
  2. Redux 模板代码过多:Action Types、Action Creators、Reducers 大量重复,现代项目推荐 Redux Toolkit
  3. 无路由守卫:/settings、/editor 等页面没有鉴权保护,未登录用户可以直接访问
  4. 无请求取消:虽然有 viewChangeCounter 防竞态,但没有真正取消网络请求(可用 AbortController)
  5. 无 Code Splitting:所有页面打包在一起,可用 React.lazy() + Suspense 做路由级代码分割

总结: 这个项目虽然使用了较旧的 API(Class 组件、react-router-redux),但其架构设计思想——API 层解耦、中间件处理异步、路由与状态联动——在现代 React 项目中依然适用。理解这些模式,对于使用 React Router v6 + Redux Toolkit + React Query 的现代项目开发同样有价值。