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:用户主页
进阶细节:
- /editor/:slug 在 /editor 之前:React Router 的 Switch 从上到下匹配,带参数的路径必须写在前面,否则 /editor 会"吃掉"所有编辑路由
- /@:username 模式:使用 @ 前缀模仿 Medium/Twitter 的用户主页 URL 设计,:username 作为动态参数
- /@: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();
}
}
这套机制的设计思路:
- routerMiddleware 拦截 push/replace 等路由 Action,转化为真正的 history 操作
- 任何需要页面跳转的地方(登录成功、文章发布成功),只需在 Reducer 中设置 redirectTo
- App 组件监听 redirectTo,通过 dispatch(push(url)) 完成跳转
- 跳转后立即 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)
};
设计亮点:
- 统一 API 基地址:所有请求拼接 API_ROOT,切换环境只改一处
- Token 自动注入:通过 superagent 的 plugin 机制(.use(tokenPlugin)),每个请求自动携带 Authorization 头
- 响应统一提取:.then(responseBody) 只提取 res.body,上层代码不需要关心 HTTP 细节
- 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
设计精妙之处:
- 组件完全不关心异步:只需要 dispatch({ type: LOGIN, payload: somePromise })
- loading 状态自动管理:ASYNC_START/ASYNC_END 全局处理
- 错误统一处理:通过 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; // 用户已经离开该页面,丢弃结果
}
用户快速切换页面时,旧页面的请求返回后不会污染新页面的状态。这是一个容易被忽略但非常重要的细节。
八、值得注意的不足与改进方向
- Class 组件 → Hooks:项目使用 Class 组件 + 生命周期方法(componentWillMount 已废弃),现代项目应使用 useEffect
- Redux 模板代码过多:Action Types、Action Creators、Reducers 大量重复,现代项目推荐 Redux Toolkit
- 无路由守卫:/settings、/editor 等页面没有鉴权保护,未登录用户可以直接访问
- 无请求取消:虽然有 viewChangeCounter 防竞态,但没有真正取消网络请求(可用 AbortController)
- 无 Code Splitting:所有页面打包在一起,可用 React.lazy() + Suspense 做路由级代码分割
总结: 这个项目虽然使用了较旧的 API(Class 组件、react-router-redux),但其架构设计思想——API 层解耦、中间件处理异步、路由与状态联动——在现代 React 项目中依然适用。理解这些模式,对于使用 React Router v6 + Redux Toolkit + React Query 的现代项目开发同样有价值。