Redux 实战:真实项目案例盘点与基于 Redux 的认证登录完整实现
【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux
本篇基于官方 FAQ 的
Miscellaneous章节,围绕两个开发者最关心的问题展开:Redux 在真实生产环境中有哪些大型应用案例,以及如何在 Redux 中按标准模式实现用户认证。通过结合本仓库源码(src/下的中间件与工具函数实现、examples/下的官方示例),读者可以掌握认证功能的完整落地方案:Action 常量与 Action Creator 的组织、基于 Redux Thunk 的异步登录流程、token 持久化,以及 Reducer 对登录各阶段状态的响应式管理。
一、有哪些大型“真实”的 Redux 项目?
Redux 自发布以来被广泛应用于各类生产级应用。官方 FAQ 明确回答:有,而且很多。FAQ 中列举了以下代表性项目(均为官方文档所引用的历史案例,可用于了解 Redux 的应用广度):
- Twitter 移动端网站(Twitter 的 mobile site)
- WordPress 的新版管理后台(wp-calypso 项目)
- Firefox 的新版调试器(Firefox DevTools debugger)
- HyperTerm 终端应用
除了上述项目,官方 FAQ 还指出,Redux Addons Catalog 中维护了一份持续更新的Redux 应用与示例清单,收录了大量大大小小的真实应用,可供学习参考。
1.1 本仓库自带的“真实”示例:examples/
对于读者而言,与其只读名单,不如直接查看本仓库examples/目录下随 Redux 一起分发的官方示例——它们都是可运行、可复现的真实代码,覆盖了从入门到复杂的各种实战模式:
| 示例目录 | 核心看点 |
|---|---|
| examples/counter-vanilla | 不依赖构建工具与视图框架,直接以 ES5 演示最原始的 Redux API |
| examples/counter | Redux + React 的最基础组合,包含测试 |
| examples/todos | 理解 state 更新如何与组件协作:reducer 委托、容器组件生成 |
| examples/todos-with-undo | 用redux-undo包裹 reducer,几行代码实现撤销/重做 |
| examples/shopping-cart | 规范化实体存储、多层级 reducer 组合、selector 封装、Redux Thunk 条件派发 |
| examples/async | 异步 API 读取、按用户输入拉取数据、loading 指示、响应缓存与失效 |
| examples/real-world | 最复杂的示例:normalizr 规范化缓存、自定义 API 中间件、分页、路由与 Redux DevTools |
| examples/universal | 服务端渲染(SSR):服务端初始化 store 状态并传递给客户端 |
官方 Introduction: Examples 文档对这些示例做了逐一说明。其中examples/real-world被官方称为"最先进的示例"——它展示了一个中型真实应用所需的全部关键模式,尤其值得研究的是其自定义 API 中间件(见下文第四节),这一模式与 FAQ 中"认证"章节的异步处理思路一脉相承。
说明:FAQ 中列举的 Twitter、WordPress、Firefox 等案例链接指向外部站点,出于引用规范,此处仅保留其名称与背景;若希望研究真实可运行的 Redux 代码,建议直接阅读本仓库
examples/目录下的源码与配套测试。
二、如何在 Redux 中实现认证?
认证是任何真实应用都不可或缺的功能。FAQ 给出了一个非常关键的前提判断:
认证不会改变你组织应用的方式——你应当像实现任何其他功能一样来实现认证。
这句话的含义是:不要为认证单独发明一套"特殊"架构。它仍然遵循 Redux 的三条铁律(单一数据源、state 只读、用纯函数 reducer 修改 state),仍然由 action 描述"发生了什么",由 reducer 计算"下一个状态是什么"。认证状态(是否已登录、token、错误信息)只是全局 state 树中一个普通的auth分支而已。
FAQ 将实现路径归纳为四个步骤,下面逐一步骤展开,并结合仓库源码给出可直接落地的完整代码。
2.1 第一步:创建 Action 常量
为认证流程定义语义清晰的 action 类型常量,例如LOGIN_SUCCESS、LOGIN_FAILURE等。在本仓库examples/async/src/actions/index.js中可以看到同样的做法——将REQUEST_POSTS、RECEIVE_POSTS等字符串常量集中导出,避免拼写错误并在多个模块间复用:
// actions/auth.js —— 认证相关 action 常量 export const LOGIN_REQUEST = 'LOGIN_REQUEST' export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' export const LOGIN_FAILURE = 'LOGIN_FAILURE' export const LOGOUT = 'LOGOUT'可以看到,我们将登录扩展成了三个阶段:LOGIN_REQUEST(请求发出)、LOGIN_SUCCESS(成功返回 token)、LOGIN_FAILURE(失败返回错误)。这正是 Redux 处理异步流程的标准三段式命名,与examples/async中的REQUEST_POSTS / RECEIVE_POSTS如出一辙。
2.2 第二步:创建 Action Creator
Action Creator 是返回 action 对象的纯函数,payload 可以是凭据(credentials)、认证是否成功的标志、token 或错误消息。本仓库 src/types/actions.ts 对ActionCreator的类型定义如下(Action 必须包含type字段):
// actions/auth.js —— action creator export const loginRequest = credentials => ({ type: LOGIN_REQUEST, credentials }) export const loginSuccess = token => ({ type: LOGIN_SUCCESS, token }) export const loginFailure = error => ({ type: LOGIN_FAILURE, error }) export const logout = () => ({ type: LOGOUT })对比examples/async/src/actions/index.js中的requestPosts、receivePosts,结构完全一致:纯函数、返回普通对象、type 必填、其余字段按需携带数据。
2.3 第三步:创建异步 Action Creator(中间件 + 网络请求 + 持久化)
FAQ 明确指出:使用 Redux Thunk 中间件(或任何你选定的中间件)发起网络请求——若凭据有效,API 返回 token,将其保存到本地存储;若失败则向用户展示响应。这些副作用(网络请求、localStorage 写入)都可以在第二步编写的 action creator 中执行。
2.3.1 中间件在 Redux 中如何工作?
Redux 的 dispatch 是同步的,reducer 必须是纯函数,因此副作用必须放到中间件层。本仓库 src/applyMiddleware.ts 是applyMiddleware的核心实现:
export default function applyMiddleware(...middlewares: Middleware[]): StoreEnhancer<any> { return createStore => (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose<typeof dispatch>(...chain)(store.dispatch) return { ...store, dispatch } } }关键机制:
- 每个中间件都会拿到
{ getState, dispatch }两个命名参数(对应 src/types/middleware.ts 中的MiddlewareAPI接口); - 中间件链通过 src/compose.ts 从右到左组合,最终包裹
store.dispatch; - 中间件的签名是
({ getState, dispatch }) => next => action => {...}——即:先拿到 store API,再拿到"下一个 dispatch",最后处理 action; - 在构造中间件期间 dispatch 是被禁用的(抛错),这是为了防止中间件尚未全部就位时就派发 action。
redux-thunk正是这种中间件的一个典型实现(源码注释在 src/applyMiddleware.ts 中直接指明:"Seeredux-thunkpackage as an example of the Redux middleware"):它检查 action 是否为函数,若是则调用它并传入dispatch与getState,从而让 action creator 可以返回函数(即 thunk)来承载异步逻辑。
2.3.2 用 Redux Thunk 编写异步登录
// api/auth.js —— 模拟认证 API const authApi = { login(credentials) { return fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }).then(response => { if (!response.ok) { return Promise.reject(new Error('用户名或密码错误')) } return response.json() // 期望返回 { token: '...' } }) } }// actions/auth.js —— thunk action creator export const login = credentials => dispatch => { dispatch(loginRequest(credentials)) return authApi.login(credentials).then( ({ token }) => { // 将 token 持久化到本地存储(副作用放在这里) localStorage.setItem('token', token) dispatch(loginSuccess(token)) }, error => { // 失败时向用户展示错误信息 dispatch(loginFailure(error.message || '登录失败,请稍后重试')) } ) } export const logout = () => dispatch => { localStorage.removeItem('token') dispatch({ type: LOGOUT }) }这段代码完整覆盖了 FAQ 第三步的要求:
- 用 Redux Thunk 让 action creator 返回函数
(dispatch, getState) => {...}; - 内部发起网络请求;
- 成功则
localStorage.setItem('token', token)保存 token,并派发LOGIN_SUCCESS; - 失败则派发
LOGIN_FAILURE携带错误信息。
本仓库中与之一致的最佳实践随处可见:
- examples/async/src/actions/index.js:
fetchPosts返回dispatch => {...}的函数,先派发requestPosts,再fetch网络数据,成功后派发receivePosts;fetchPostsIfNeeded更是利用(dispatch, getState)双参数在派发前检查缓存是否需要刷新; - examples/shopping-cart/src/actions/index.js:
addToCart通过getState()读取库存后有条件地派发action——这正是认证场景中"token 是否过期、是否需要重新登录"这类判断的标准写法。
2.3.3 在 store 中挂载中间件
// store/index.js import { createStore, applyMiddleware } from 'redux' import { thunk } from 'redux-thunk' import rootReducer from './reducers' const store = createStore(rootReducer, applyMiddleware(thunk)) export default storeapplyMiddleware返回的是一个store enhancer(src/applyMiddleware.ts 的多个重载签名也体现了这一点),它可以与 DevTools 等其他 enhancer 通过compose组合。参考官方 examples/real-world/src/store/configureStore.dev.js:
const store = createStore( rootReducer, preloadedState, compose(applyMiddleware(thunk, api, createLogger()), DevTools.instrument()) )2.4 第四步:创建 Reducer 处理每一种认证状态
Reducer 是一个纯函数,接收(state, action)返回下一个 state。认证 reducer 需要为LOGIN_SUCCESS、LOGIN_FAILURE等每一种情况返回对应的新状态:
// reducers/auth.js const initialState = { isAuthenticated: false, token: localStorage.getItem('token'), // 应用启动时从本地存储恢复会话 error: null } const auth = (state = initialState, action) => { switch (action.type) { case LOGIN_REQUEST: return { ...state, error: null } case LOGIN_SUCCESS: return { ...state, isAuthenticated: true, token: action.token, error: null } case LOGIN_FAILURE: return { ...state, isAuthenticated: false, error: action.error } case LOGOUT: return { ...state, isAuthenticated: false, token: null } default: return state } } export default auth要点:
- 不可变更新:使用对象展开运算符
{ ...state, ... }生成新对象,绝不直接修改原 state; - default 分支必须返回原 state:对于未知 action 返回
state本身; - 初始状态不能为
undefined。
在examples/async/src/reducers/index.js中可以看到完全一致的 reducer 写法:posts子 reducer 分别处理INVALIDATE_SUBREDDIT、REQUEST_POSTS、RECEIVE_POSTS三种情况,全部用展开语法返回新对象。
2.4.1 用 combineReducers 将 auth 并入根状态
认证只是应用的一个切片,最终通过combineReducers与其他 reducer 组合成根 reducer。本仓库 src/combineReducers.ts 的注释说明了其职责:把"值是 reducer 函数的对象"合并为单个 reducer,调用每个子 reducer 并将结果汇总为与键对应的 state 对象。
// reducers/index.js import { combineReducers } from 'redux' import auth from './auth' import user from './user' import posts from './posts' const rootReducer = combineReducers({ auth, user, posts }) export default rootReducer需要注意combineReducers的两条硬性约束(见 src/combineReducers.ts 的assertReducerShape实现):
- 子 reducer 在初始化时(
ActionTypes.INIT)不得返回undefined,否则抛出异常; - 用随机 action 探测时,子 reducer 对未知 action 必须返回当前 state(或初始 state),不能返回
undefined。若不需要某个值,可返回null而非undefined。
我们的authreducer 对未知 action 返回state,满足以上约束。
2.4.2 在组件中便捷派发:bindActionCreators
在容器组件中,可以借助 src/bindActionCreators.ts 把 action creator 自动包装成已绑定dispatch的函数,避免到处手写dispatch(login(creds)):
import { bindActionCreators } from 'redux' import * as authActions from '../actions/auth' // 返回 { login: (...args) => dispatch(login(...args)), logout: (...args) => ... } const boundActions = bindActionCreators(authActions, dispatch) boundActions.login({ username, password })源码实现说明:它会把对象中每个函数类型的值包装为dispatch(actionCreator.apply(this, args))的形式;若传入的是单个函数则直接返回包装后的函数;若传入的不是对象或函数则抛出错误(并提示是否误用了import ActionCreators from而非import * as ActionCreators from)。
三、完整链路:一次登录请求在 Redux 中如何流转
将上述四步串起来,一次login派发的完整数据流为:
- 组件调用
login(credentials)(一个 thunk); applyMiddleware(thunk)拦截到函数类型的 action,调用它并注入dispatch/getState;- thunk 内部先
dispatch(loginRequest(credentials)),同步派发普通 action; authreducer 收到LOGIN_REQUEST,返回{ ...state, error: null };- thunk 发起
fetch('/api/login'); - 成功后写入
localStorage并dispatch(loginSuccess(token));失败则dispatch(loginFailure(error)); authreducer 根据 action 类型更新isAuthenticated/token/error;- 订阅了 store 的 UI 层通过
getState().auth感知状态变化,决定渲染登录表单、加载态还是主界面。
这一"请求 → 成功/失败"的流转模式与官方 examples/async 的 fetch 流程、examples/shopping-cart 的 checkout 流程完全同构。
四、进阶:将认证请求封装为自定义中间件
FAQ 提到"使用 Redux Thunk 中间件或任何你见合适的中间件"。当应用中有大量同类异步请求时,可以学习 examples/real-world/src/middleware/api.js 的做法——把"发起请求 + 派发三阶段 action"抽象成统一的自定义中间件:
// middleware/api.js —— 借鉴 real-world 示例的三段式 API 中间件 export const CALL_API = 'Call API' const callApi = (endpoint, options) => { return fetch(endpoint, options).then(response => { if (!response.ok) { return Promise.reject(new Error(`请求失败:${response.status}`)) } return response.json() }) } export default store => next => action => { const callAPI = action[CALL_API] if (typeof callAPI === 'undefined') { return next(action) // 非 API action,直接放行 } let { endpoint } = callAPI const { types, ...rest } = callAPI // 支持以函数形式动态计算 endpoint(可读取 getState) if (typeof endpoint === 'function') { endpoint = endpoint(store.getState()) } if (typeof endpoint !== 'string') { throw new Error('必须提供字符串形式的 endpoint URL。') } if (!Array.isArray(types) || types.length !== 3) { throw new Error('types 必须是由三个 action type 组成的数组。') } const [requestType, successType, failureType] = types const actionWith = data => { const finalAction = Object.assign({}, action, data) delete finalAction[CALL_API] return finalAction } // 先派发"请求中"action next(actionWith({ type: requestType })) // 再根据结果派发"成功/失败"action return callApi(endpoint, rest).then( response => next(actionWith({ type: successType, response })), error => next(actionWith({ type: failureType, error: error.message })) ) }这种模式的优点是把"认证请求"变成声明式的:业务侧只需描述端点与三个 action type,无需重复编写 fetch 逻辑:
export const login = credentials => ({ [CALL_API]: { endpoint: '/api/login', method: 'POST', body: JSON.stringify(credentials), types: [LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE] } })api中间件的三层嵌套签名store => next => action => {...}与 src/types/middleware.ts 中Middleware接口的类型定义一一对应,是理解 Redux 中间件组合机制的绝佳样本。
五、安全与工程实践提醒
FAQ 没有展开,但基于仓库中的实现事实,有几点工程实践值得强调:
- token 的存储位置:官方示例将请求结果保存到本地存储(对应 FAQ 原文 "save the token in the local storage")。在真实生产环境中,请根据安全模型权衡
localStorage、sessionStorage或httpOnly cookie的取舍;若存储于localStorage,注意防范 XSS 风险。 - 应用启动时的会话恢复:如第四节 reducer 所示,可在初始 state 中读取已持久化的 token,避免刷新页面即丢失登录态。
- 统一的错误处理:
LOGIN_FAILURE分支必须携带可展示的错误信息(参考 real-world 中间件中error.message || 'Something bad happened'的兜底写法)。 - 不要改变应用组织方式:认证状态就放在
auth切片中,与user、posts等切片平级,通过combineReducers组合——这正是 FAQ 开篇"用实现任何其他功能的方式实现认证"的落地体现。
六、延伸阅读
- 官方入门示例总览:Introduction: Examples
- 本仓库随附的可运行示例源码:examples(
real-world覆盖认证所需的中间件、规范化与 DevTools 全套模式) - 中间件设计原理:Middleware 深度解析、编写自定义中间件
- 异步逻辑与 thunk:Fundamentals: Async Logic、Writing Logic with Thunks
- 核心 API 源码:
applyMiddleware实现见 src/applyMiddleware.ts、reducer 组合见 src/combineReducers.ts、action creator 绑定见 src/bindActionCreators.ts - FAQ 原文中引用的外部讨论(Reddit/HN 上的大型项目征集帖)、JWT 认证文章(Auth0)与 Redux Addons Catalog 清单,可作为背景资料按名检索,此处不再列出外部链接
【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考