Redux 应用架构与状态管理详解
1. Redux 中动作的创建与测试
在 Redux 中,我们已经为用户相关操作、评论、帖子、加载状态和错误处理创建了动作。这些动作代表了用户与应用程序交互的基本方式,是应用程序原始功能的主要组成部分。不过,还需要使用归约器(reducers)让 Redux 对状态变化做出响应,并将其与 React 连接起来。
1.1 测试动作
在进入归约器的学习之前,我们需要对这些动作进行一些快速测试。Redux 使得动作创建器、归约器和其他部分的测试变得简单,而且这些测试大多可以独立于前端框架进行。
1.1.1 测试同步动作
大多数动作创建器返回一个包含类型和有效负载信息的对象,因此很容易测试。以下是测试动作的示例代码:
jest.mock('../../src/shared/http');
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import initialState from '../../src/constants/initialState';
import * as types from '../../src/constants/types';
import {
showComments,
toggleComments,
updateAvailableComments,
createComment,
getCommentsForPost
} from '../../src/actions/comments';
import * as API from '../../src/shared/http';
const mockStore = configureStore([thunk]);
describe('login actions', () => {
let store;
beforeEach(() => {
store = mockStore(initialState);
});
test('showComments', () => {
const postId = 'id';
const actual = showComments(postId);
const expected = { type: types.comments.SHOW, postId };
expect(actual).toEqual(expected);
});
test('toggleComments', () => {
const postId = 'id';
const actual = toggleComments(postId);
const expected = { type: types.comments.TOGGLE, postId };
expect(actual).toEqual(expected);
});
test('updateAvailableComments', () => {
const comments = ['comments'];
const actual = updateAvailableComments(comments);
const expected = { type: types.comments.GET, comments };
expect(actual).toEqual(expected);
});
});
1.1.2 测试异步动作
对于异步动作创建器,需要使用
redux-mock-store
和
redux-thunk
进行配置。以下是测试异步动作的示例代码:
test('createComment', async () => {
const mockComment = { content: 'great post!' };
API.createComment = jest.fn(() => {
return Promise.resolve({
json: () => Promise.resolve([mockComment])
});
});
await store.dispatch(createComment(mockComment));
const actions = store.getActions();
const expectedActions = [{ type: types.comments.CREATE, comment: [mockComment] }];
expect(actions).toEqual(expectedActions);
});
test('getCommentsForPost', async () => {
const postId = 'id';
const comments = [{ content: 'great stuff' }];
API.fetchCommentsForPost = jest.fn(() => {
return Promise.resolve({
json: () => Promise.resolve(comments)
});
});
await store.dispatch(getCommentsForPost(postId));
const actions = store.getActions();
const expectedActions = [{ type: types.comments.GET, comments }];
expect(actions).toEqual(expectedActions);
});
1.2 创建自定义 Redux 中间件用于崩溃报告
在进入归约器之前,我们可以添加一些自定义的中间件。中间件是 Redux 让我们介入数据流过程的方式,它可以在动作被分发到存储、由归约器处理、状态更新和监听器通知的过程中发挥作用。
1.2.1 中间件的作用
中间件可以用于中断数据流、将数据发送到其他 API 或解决应用程序范围内的问题。常见的使用场景包括数据修改、流程中断和执行副作用。
1.2.2 创建崩溃报告中间件
以下是一个简单的崩溃报告中间件的示例代码:
// ... src/middleware/crash.js
import { createError } from '../actions/error';
export default store => next => action => {
try {
if (action.error) {
console.error(action.error);
console.error(action.info);
}
return next(action);
} catch (err) {
const { user } = store.getState();
console.error(err);
window.Raven.setUserContext(user);
window.Raven.captureException(err);
return store.dispatch(createError(err));
}
};
//... src/store/configureStore.prod.js
import thunk from 'redux-thunk';
import { createStore, compose, applyMiddleware } from 'redux';
import rootReducer from '../reducers/root';
import crashReporting from '../middleware/crash';
let store;
export default function configureStore(initialState) {
if (store) {
return store;
}
store = createStore(rootReducer, initialState, compose(
applyMiddleware(thunk, crashReporting)
));
return store;
}
1.3 概念匹配
以下是一些 Redux 概念及其定义的匹配:
| 术语 | 定义 |
| ---- | ---- |
| 存储(Store) | Redux 中的中央状态对象,是真理的来源。 |
| 归约器(Reducer) | Redux 用于根据发生的事情计算状态变化的函数。 |
| 动作(Action) | 包含与变化相关信息的对象,必须有一个类型,并且可以包含任何必要的额外信息。 |
| 动作创建器(Action creator) | 用于创建应用程序中发生的事情的类型和有效负载信息的函数。 |
1.4 Redux 中间件流程图
graph LR
A[Process start] --> B[Add, remove, modify data Middleware]
B --> C[Interrupt flow Middleware]
C --> D[Perform side effects Middleware]
D --> E[Process end]
B --> F[Other services]
C --> F
D --> F
2. Redux 归约器的工作原理
归约器是 Redux 中用于确定状态如何变化的关键部分。动作只是描述了发生的事情,但归约器负责指定存储状态如何响应这些动作而变化。
2.1 归约器的定义
归约器是纯函数,接收前一个状态和一个动作作为参数,并返回下一个状态。它们的方法签名类似于
Array.prototype.reduce
。
2.2 状态形状和初始状态
在开始使用归约器之前,需要确定存储的状态形状。通常,最好将“原始”数据与 UI 数据尽可能分开。以下是一个初始状态文件的示例:
// src/constants/initialState.js
export default {
error: null,
loading: false,
postIds: [],
posts: {},
commentIds: [],
comments: {},
pagination: {
first: `${process.env
.ENDPOINT}/posts?_page=1&_sort=date&_order=DESC&
_embed=comments&_expand=user&_embed=likes`,
next: null,
prev: null,
last: null
},
user: {
authenticated: false,
profilePicture: null,
id: null,
name: null,
token: null
}
};
2.3 设置归约器以响应传入动作
归约器通常使用
switch
语句来匹配传入的动作类型,并返回一个新的状态副本。以下是一些归约器的示例代码:
2.3.1 加载归约器
// src/reducers/loading.js
import initialState from '../constants/initialState';
import * as types from '../constants/types';
export function loading(state = initialState.loading, action) {
switch (action.type) {
case types.app.LOADING:
return true;
case types.app.LOADED:
return false;
default:
return state;
}
}
2.3.2 评论归约器
// src/reducers/comments.js
import initialState from '../constants/initialState';
import * as types from '../constants/types';
export function comments(state = initialState.comments, action) {
switch (action.type) {
case types.comments.GET: {
const { comments } = action;
let nextState = Object.assign({}, state);
for (let comment of comments) {
if (!nextState[comment.id]) {
nextState[comment.id] = comment;
}
}
return nextState;
}
case types.comments.CREATE: {
const { comment } = action;
let nextState = Object.assign({}, state);
nextState[comment.id] = comment;
return nextState;
}
default:
return state;
}
}
export function commentIds(state = initialState.commentIds, action) {
switch (action.type) {
case types.comments.GET: {
const nextCommentIds = action.comments.map(comment => comment.id);
let nextState = Array.from(state);
for (let commentId of nextCommentIds) {
if (!state.includes(commentId)) {
nextState.push(commentId);
}
}
return nextState;
}
case types.comments.CREATE: {
const { comment } = action;
let nextState = Array.from(state);
nextState.push(comment.id);
return nextState;
}
default:
return state;
}
}
2.3.3 帖子归约器
// src/reducers/posts.js
import initialState from '../constants/initialState';
import * as types from '../constants/types';
export function posts(state = initialState.posts, action) {
switch (action.type) {
case types.posts.GET: {
const { posts } = action;
let nextState = Object.assign({}, state);
for (let post of posts) {
if (!nextState[post.id]) {
nextState[post.id] = post;
}
}
return nextState;
}
case types.posts.CREATE: {
const { post } = action;
let nextState = Object.assign({}, state);
if (!nextState[post.id]) {
nextState[post.id] = post;
}
return nextState;
}
case types.comments.SHOW: {
let nextState = Object.assign({}, state);
nextState[action.postId].showComments = true;
return nextState;
}
case types.comments.TOGGLE: {
let nextState = Object.assign({}, state);
nextState[action.postId].showComments =
!nextState[action.postId].showComments;
return nextState;
}
case types.posts.LIKE: {
let nextState = Object.assign({}, state);
const oldPost = nextState[action.post.id];
nextState[action.post.id] = Object.assign({}, oldPost, action.post);
return nextState;
}
case types.posts.UNLIKE: {
let nextState = Object.assign({}, state);
const oldPost = nextState[action.post.id];
nextState[action.post.id] = Object.assign({}, oldPost, action.post);
return nextState;
}
case types.comments.CREATE: {
const { comment } = action;
let nextState = Object.assign({}, state);
nextState[comment.postId].comments.push(comment);
return state;
}
default:
return state;
}
}
export function postIds(state = initialState.postIds, action) {
switch (action.type) {
case types.posts.GET: {
const nextPostIds = action.posts.map(post => post.id);
let nextState = Array.from(state);
for (let post of nextPostIds) {
if (!state.includes(post)) {
nextState.push(post);
}
}
return nextState;
}
case types.posts.CREATE: {
const { post } = action;
let nextState = Array.from(state);
if (!state.includes(post.id)) {
nextState.push(post.id);
}
return nextState;
}
default:
return state;
}
}
2.4 其他归约器
除了上述归约器,还需要创建错误归约器和分页归约器。
2.4.1 错误归约器
// src/reducers/error.js
import initialState from '../constants/initialState';
import * as types from '../constants/types';
export function error(state = initialState.error, action) {
switch (action.type) {
case types.app.ERROR:
return action.error;
default:
return state;
}
}
2.4.2 分页归约器
// src/reducers/pagination.js
import initialState from '../constants/initialState';
import * as types from '../constants/types';
export function pagination(state = initialState.pagination, action) {
switch (action.type) {
case types.posts.UPDATE_LINKS:
const nextState = Object.assign({}, state);
for (let k in action.links) {
if (action.links.hasOwnProperty(k)) {
if (process.env.NODE_ENV === 'production') {
nextState[k] =
action.links[k].url.replace(/http:\/\//, 'https://');
} else {
nextState[k] = action.links[k].url;
}
}
}
return nextState;
default:
return state;
}
}
2.5 归约器工作流程图
graph LR
A[Event] --> B[Action creator]
B --> C[Action]
C --> D[Reducer]
D --> E[New store state]
E --> F[Components receive state as props]
G[Third party APIs] --> B
H[Thunk Middleware] --> B
I[Analytics] --> B
J[Logging] --> B
2.6 迁移到 Redux 的价值
虽然 Redux 的初始设置可能需要一些工作,但从长远来看,它通常是值得的。它可以帮助我们更快速地迭代产品,并且在应用程序的重构和扩展过程中减少状态管理和业务逻辑部分的更改。
通过以上内容,我们详细介绍了 Redux 中动作的创建、测试、中间件的使用以及归约器的工作原理。这些知识将帮助我们更好地管理应用程序的状态,提高代码的可维护性和可测试性。
3. Redux 与 React 的集成及应用
3.1 Redux 与 React 的协同工作
Redux 是一种与 React 配合良好的应用架构和库。它专注于可预测性,并强制采用严格的数据处理方式。Redux 的存储(Store)是应用程序的中央状态对象,是真理的来源,与 Flux 不同,Redux 只允许有一个存储。
在 React 应用中,Redux 的动作(Action)描述了发生的事情,归约器(Reducer)根据这些动作计算状态的变化。组件通过接收存储中的状态作为属性(props)来更新 UI。以下是一个简单的 React 组件与 Redux 集成的示例:
import React from 'react';
import { connect } from 'react-redux';
import { showComments } from '../actions/comments';
const CommentComponent = (props) => {
const { showComments, postId } = props;
return (
<button onClick={() => showComments(postId)}>Show Comments</button>
);
};
const mapStateToProps = (state) => {
return {
postId: state.posts.selectedPostId
};
};
const mapDispatchToProps = {
showComments
};
export default connect(mapStateToProps, mapDispatchToProps)(CommentComponent);
3.2 将应用迁移到 Redux 架构
将现有的 React 应用迁移到 Redux 架构时,需要遵循以下步骤:
1.
设计状态形状
:确定存储的状态形状,将“原始”数据与 UI 数据分离。可以参考前面提到的初始状态文件示例。
2.
创建动作
:为应用中的各种操作创建动作,如用户登录、评论、点赞等。
3.
编写归约器
:根据动作编写归约器,处理状态的变化。可以参考前面的归约器示例代码。
4.
集成中间件
:添加中间件,如崩溃报告中间件,以处理错误和副作用。
5.
连接 React 组件
:使用
react-redux
库将 React 组件与 Redux 存储连接起来,使组件能够获取状态和分发动作。
3.3 为应用添加点赞和评论功能
在 Redux 架构下,为应用添加点赞和评论功能可以按照以下步骤进行:
3.3.1 创建动作
// src/actions/posts.js
import * as types from '../constants/types';
export function likePost(post) {
return {
type: types.posts.LIKE,
post
};
}
export function unlikePost(post) {
return {
type: types.posts.UNLIKE,
post
};
}
export function createComment(comment) {
return {
type: types.comments.CREATE,
comment
};
}
3.3.2 更新归约器
在帖子归约器和评论归约器中添加相应的处理逻辑,以处理点赞、取消点赞和创建评论的动作。可以参考前面帖子归约器和评论归约器的示例代码。
3.3.3 连接 React 组件
在 React 组件中引入这些动作,并将其与按钮的点击事件绑定。以下是一个添加点赞功能的组件示例:
import React from 'react';
import { connect } from 'react-redux';
import { likePost } from '../actions/posts';
const PostComponent = (props) => {
const { post, likePost } = props;
return (
<div>
<h2>{post.title}</h2>
<button onClick={() => likePost(post)}>Like</button>
</div>
);
};
const mapStateToProps = (state) => {
return {
post: state.posts.selectedPost
};
};
const mapDispatchToProps = {
likePost
};
export default connect(mapStateToProps, mapDispatchToProps)(PostComponent);
3.4 Redux 与 React 集成流程图
graph LR
A[React Components] --> B[Action creators]
B --> C[Actions]
C --> D[Middleware]
D --> E[Reducers]
E --> F[Store]
F --> G[React Components]
H[Third party APIs] --> B
I[Thunk Middleware] --> B
J[Analytics] --> B
K[Logging] --> B
4. 总结与最佳实践
4.1 总结
通过前面的介绍,我们了解了 Redux 的核心概念和工作原理,包括动作的创建与测试、自定义中间件的使用、归约器的工作方式以及 Redux 与 React 的集成。以下是一些关键要点总结:
-
Redux 是一种可预测的状态管理库
:它通过动作和归约器来管理应用程序的状态,确保状态的变化是可追踪和可预测的。
-
中间件增强了 Redux 的功能
:可以使用中间件来处理异步操作、错误报告和其他副作用。
-
归约器是纯函数
:它们根据动作计算状态的变化,不产生副作用,确保状态的变化是可重复的。
-
Redux 与 React 集成良好
:通过
react-redux
库,React 组件可以轻松地获取存储中的状态并分发动作。
4.2 最佳实践
在使用 Redux 时,以下是一些最佳实践建议:
-
合理设计状态形状
:将“原始”数据与 UI 数据分离,避免状态过于复杂。
-
保持动作和归约器的简单性
:每个动作和归约器应该只负责一个特定的功能,避免过度耦合。
-
使用中间件处理异步操作
:如使用
redux-thunk
或
redux-promise
来处理异步动作。
-
编写测试用例
:对动作、归约器和中间件进行单元测试,确保代码的正确性。
-
遵循命名规范
:为动作类型、动作创建器和归约器使用清晰、一致的命名规范,提高代码的可读性。
4.3 Redux 使用建议表格
| 方面 | 建议 |
|---|---|
| 状态设计 | 分离原始数据与 UI 数据,确保状态结构清晰 |
| 动作和归约器 | 保持功能单一,避免复杂逻辑 |
| 异步操作 | 使用中间件处理,如 redux - thunk |
| 测试 | 对关键部分编写单元测试,保证代码质量 |
| 命名规范 | 采用一致、清晰的命名,提高可读性 |
通过遵循这些最佳实践,我们可以更好地利用 Redux 来管理应用程序的状态,提高代码的可维护性和可扩展性。在实际开发中,不断实践和总结经验,将有助于我们更熟练地使用 Redux 构建高质量的 React 应用。
超级会员免费看
1668

被折叠的 条评论
为什么被折叠?



