设计模块
在 9.1 节中已经介绍过,一个功能相关的 reducer、action types、action creators 将定义到一个文件中,作为一个 Redux 模块。根据 state 的结构,我们可以拆分出 app、auth、posts、comments、users、ui 和 index 七个模块。下面就来逐一介绍。
app模块
app 模块负责标记 API 请求的开始和结束以及应用全局错误信息的设置,app.js 代码如下:
const initialState = {
requestQuantity: 0, // 当前应用中正在进行的API请求数
error: null // 应用全局错误信息
};
// action types
export const types = {
START_REQUEST: "APP/START_REQUEST", // 开始发送请求
FINISH_REQUEST: "APP/FINISH_REQUEST", // 请求结束
SET_ERROR: "APP/SET_ERROR", // 设置错误信息
REMOVE_ERROR: "APP/REMOVE_ERROR" // 删除错误信息
};
// action creators
export const actions = {
startRequest: () => ({
type: types.START_REQUEST
}),
finishRequest: () => ({
type: types.FINISH_REQUEST
}),
setError: error => ({
type: types.SET_ERROR,
error
}),
removeError: () => ({
type: types.REMOVE_ERROR
})
};
// reducers
const reducer = (state = initialState, action) => {
switch (action.type) {
case types.START_REQUEST:
// 每接收一个API请求开始的action,requestQuantity加1
return { ...state, requestQuantity: state.requestQuantity + 1 };
case types.FINISH_REQUEST:
// 每接收一个API请求结束的action,requestQuantity减1
return { ...state, requestQuantity: state.requestQuantity - 1 };
case types.SET_ERROR:
return { ...state, error: action.error };
case types.REMOVE_ERROR:
return { ...state, error: null };
default:
return state;
}
};
export default reducer;
这里需要注意,app 模块中的 action creators 会被其他模块调用。例如,其他模块用于请求 API 的异步 action 中,需要在发送请求的开始和结束时分别调用 startRequest 和 finishRequest;在 API 返回错误信息时,需要调用 setError 设置错误信息。这也说明,我们定义的模块并非只能被 UI 组件使用,各个模块之间也是可以互相调用的。
auth模块
auth 模块负责应用的登录和注销。登录会调用服务器 API 做认证,这时就涉及异步 action,我们使用前面介绍的 redux-thunk 定义异步 action。注销逻辑仍然简化处理,只是清除客户端的登录用户信息。auth.js 的主要代码如下:
import { post } from "../../utils/request";
import url from "../../utils/url";
import { actions as appActions } from "./app";
const initialState = {
userId: null,
username: null
};
// action types
export const types = {
LOGIN: "AUTH/LOGIN", //登录
LOGOUT: "AUTH/LOGOUT" //注销
};
// action creators
export const actions = {
// 异步action,执行登录验证
login: (username, password) => {
return dispatch => {
// 每个API请求开始前,发送app模块定义的startRequest action
dispatch(appActions.startRequest());
const params = { username, password };
return post(url.login(), params).then(data => {
// 每个API请求结束后,发送app模块定义的finishRequest action
dispatch(appActions.finishRequest());
// 请求返回成功,保存登录用户的信息,否则,设置全局错误信息
if (!data.error) {
dispatch(actions.setLoginInfo(data.userId, username));
} else {
dispatch(appActions.setError(data.error));
}
});
};
},
logout: () => ({
type: types.LOGOUT
}),
setLoginInfo: (userId, username) => ({
type: types.LOGIN,
userId: userId,
username: username
})
};
// reducers
const reducer = (state = initialState, action) => {
switch (action.type) {
case types.LOGIN:
return { ...state, userId: action.userId, username: action.username };
case types.LOGOUT:
return { ...state, userId: null, username: null };
default:
return state;
}
};
export default reducer;
posts模块
posts 模块负责与帖子相关的状态管理,包括获取帖子列表、获取帖子详情、新建帖子和修改帖子。使用到的 action types 定义如下:
// action types
export const types = {
CREATE_POST: "POSTS/CREATE_POST", //新建帖子
UPDATE_POST: "POSTS/UPDATE_POST", //修改帖子
FETCH_ALL_POSTS: "POSTS/FETCH_ALL_POSTS", //获取帖子列表
FETCH_POST: "POSTS/FETCH_POST" //获取帖子详情
};
相应地,我们需要定义如下 action creators:
// action creators
export const actions = {
// 获取帖子列表
fetchAllPosts: () => {
return (dispatch, getState) => {
if (shouldFetchAllPosts(getState())) {
dispatch(appActions.startRequest());
return get(url.getPostList()).then(data => {
dispatch(appActions.finishRequest());
if (!data.error) {
const { posts, postsIds, authors } = convertPostsToPlain(data);
dispatch(fetchAllPostsSuccess(posts, postsIds, authors));
} else {
dispatch(appActions.setError(data.error));
}
});
}
};
},
// 获取帖子详情
fetchPost: id => {
return (dispatch, getState) => {
if (shouldFetchPost(id, getState())) {
dispatch(appActions.startRequest());
return get(url.getPostById(id)).then(data => {
dispatch(appActions.finishRequest());
if (!data.error && data.length === 1) {
const { post, author } = convertSinglePostToPlain(data[0]);
dispatch(fetchPostSuccess(post, author));
} else {
dispatch(appActions.setError(data.error));
}
});
}
};
},
// 新建帖子
createPost: (title, content) => {
return (dispatch, getState) => {
const state = getState();
const author = state.auth.userId;
const params = {
author,
title,
content,
vote: 0
};
dispatch(appActions.startRequest());
return post(url.createPost(), params).then(data => {
dispatch(appActions.finishRequest());
if (!data.error) {
dispatch(createPostSuccess(data));
} else {
dispatch(appActions.setError(data.error));
}
});
};
},
// 更新帖子
updatePost: (id, post) => {
return dispatch => {
dispatch(appActions.startRequest());
return put(url.updatePost(id), post).then(data => {
dispatch(appActions.finishRequest());
if (!data.error) {
dispatch(updatePostSuccess(data));
} else {
dispatch(appActions.setError(data.error));
}
});
};
}
};
// 获取帖子列表成功
const fetchAllPostsSuccess = (posts, postIds, authors) => ({
type: types.FETCH_ALL_POSTS,
posts,
postIds,
users: authors
});
// 获取帖子详情成功
const fetchPostSuccess = (post, author) => ({
type: types.FETCH_POST,
post,
user: author
});
// 新建帖子成功
const createPostSuccess = post => ({
type: types.CREATE_POST,
post: post
});
// 更新帖子成功
const updatePostSuccess = post => ({
type: types.UPDATE_POST,
post: post
});
这里有几个地方需要注意:
-
每一个 action type 实际上对应两个 action creator,一个创建异步 action 发送 API 请求,例如 fetchAllPosts;另一个根据 API 返回的数据创建普通的 action,例如 fetchAllPostsSuccess。
-
供外部使用的 action creators 定义在常量对象 actions 中,例如 fetchAllPosts、fetchPost、createPost 和 updatePost。仅在模块内部使用的 action creators 不需要被导出,例如 fetchAllPostsSuccess、fetchPostSuccess、createPostSuccess 和 updatePostSuccess。
-
Redux 的缓存作用。fetchAllPosts 中调用了 shouldFetchAllPosts,用于判断当前的 state 中是否已经有帖子列表数据,如果没有才会发送 API 请求。之所以可以这么处理,正是基于 Redux 使用一个全局 state 管理应用状态,这种缓存机制可以提高应用的性能。fetchPost 中调用的 shouldFetchPost 也是同样的作用。
-
API 返回的数据结构往往有嵌套,我们需要把嵌套的数据结构转换成扁平的结构,这样才能方便地被扁平化的 state 所使用。fetchAllPosts 中的 convertPostsToPlain 和 fetchPost中convertSinglePostToPlain 两个函数就用于执行这个转换过程的。转换过程的实现依赖于 API 返回的数据结构和业务逻辑,具体代码参考项目源代码。另外,还可以使用 normalizr(https://github.com/paularmstrong/normalizr)这个库将嵌套的数据结构转换成扁平结构。
posts 模块的 state 又拆分成 allIds 和 byId 两个子 state,每个子 state 使用一个 reducer 处理,最后通过 Redux 提供的 combineReducers 把两个 reducer 合并成一个。posts 模块的 reducer 定义如下:
// reducers
const allIds = (state = initialState.allIds, action) => {
switch (action.type) {
case types.FETCH_ALL_POSTS:
return action.postIds;
case types.CREATE_POST:
return [action.post.id, ...state];
default:
return state;
}
};
const byId = (state = initialState.byId, action) => {
switch (action.type) {
case types.FETCH_ALL_POSTS:
return action.posts;
case types.FETCH_POST:
case types.CREATE_POST:
case types.UPDATE_POST:
return {
...state,
[action.post.id]: action.post
};
default:
return state;
}
};
const reducer = combineReducers({
allIds,
byId
});
export default reducer;
至此,我们完成了 posts 模块的设计,这也是所有模块中最复杂的一个模块。posts.js 的完整代码可参考项目源代码。
comments模块
comments 模块负责获取帖子的评论列表和创建新评论,与 posts 模块功能很相近,这里不再详细分析,只给出主要逻辑代码:
const initialState = {
byPost: {},
byId: {}
};
// action types
export const types = {
FETCH_COMMENTS: "COMMENTS/FETCH_COMMENTS", // 获取评论列表
CREATE_COMMENT: "COMMENTS/CREATE_COMMENT" // 新建评论
};
// action creators
export const actions = {
// 获取评论列表
fetchComments: postId => {
return (dispatch, getState) => {
if (shouldFetchComments(postId, getState())) {
dispatch(appActions.startRequest());
return get(url.getCommentList(postId)).then(data => {
dispatch(appActions.finishRequest());
if (!data.error) {
const { comments, commentIds, users } = convertToPlainStructure(data);
dispatch(fetchCommentsSuccess(postId, commentIds, comments, users));
} else {
dispatch(appActions.setError(data.error));
}
});
}
};
},
// 新建评论
createComment: comment => {
return dispatch => {
dispatch(appActions.startRequest());
return post(url.createComment(), comment).then(data => {
dispatch(appActions.finishRequest());
if (!data.error) {
dispatch(createCommentSuccess(data.post, data));
} else {
dispatch(appActions.setError(data.error));
}
});
};
}
};
// 获取评论列表成功
const fetchCommentsSuccess = (postId, commentIds, comments, users) => ({
type: types.FETCH_COMMENTS,
postId,
commentIds,
comments,
users
});
// 新建评论成功
const createCommentSuccess = (postId, comment) => ({
type: types.CREATE_COMMENT,
postId,
comment
});
const shouldFetchComments = (postId, state) => {
return !state.comments.byPost[postId];
};
const convertToPlainStructure = comments => {
let commentsById = {};
let commentIds = [];
let authorsById = {};
comments.forEach(item => {
commentsById[item.id] = { ...item, author: item.author.id };
commentIds.push(item.id);
if (!authorsById[item.author.id]) {
authorsById[item.author.id] = item.author;
}
});
return {
comments: commentsById,
commentIds,
users: authorsById
};
};
// reducers
const byPost = (state = initialState.byPost, action) => {
switch (action.type) {
case types.FETCH_COMMENTS:
return { ...state, [action.postId]: action.commentIds };
case types.CREATE_COMMENT:
return {
...state,
[action.postId]: [action.comment.id, ...state[action.postId]]
};
default:
return state;
}
};
const byId = (state = initialState.byId, action) => {
switch (action.type) {
case types.FETCH_COMMENTS:
return { ...state, ...action.comments };
case types.CREATE_COMMENT:
return { ...state, [action.comment.id]: action.comment };
default:
return state;
}
};
const reducer = combineReducers({
byPost,
byId
});
export default reducer;
users模块
users 模块负责维护用户信息。这个模块有些特殊,因为它不需要定义 action types 和 action creators,它响应的 action 都来自 posts 模块和 comments 模块。例如,当 posts 模块获取帖子列表数据时,users 模块也需要把帖子列表数据中的用户(作者)信息保存到自身 state 中。users.js 的主要代码如下:
import { types as commentTypes } from "./comments";
import { types as postTypes } from "./posts";
const initialState = {};
// reducers
const reducer = (state = initialState, action) => {
switch (action.type) {
// 获取评论列表和帖子列表时,更新列表数据中包含的所有作者信息
case commentTypes.FETCH_COMMENTS:
case postTypes.FETCH_ALL_POSTS:
return { ...state, ...action.users };
// 获取帖子详情时,只需更新当前帖子的作者信息
case postTypes.FETCH_POST:
return { ...state, [action.user.id]: action.user };
default:
return state;
}
};
export default reducer;
action 和 reducer 之间并不存在一对一的关系。一个 action 是可以被多个模块的 reducer 处理的,尤其是当模块之间存在关联关系时,这种场景更为常见。 |
ui模块
ui 模块的功能很简单,这里只给出主要代码:
import { types as postTypes } from "./posts";
const initialState = {
addDialogOpen: false,
editDialogOpen: false
};
// action types
export const types = {
OPEN_ADD_DIALOG: "UI/OPEN_ADD_DIALOG", // 打开新建帖子状态
CLOSE_ADD_DIALOG: "UI/CLOSE_ADD_DIALOG", // 关闭新建帖子状态
OPEN_EDIT_DIALOG: "UI/OPEN_EDIT_DIALOG", // 打开编辑帖子状态
CLOSE_EDIT_DIALOG: "UI/CLOSE_EDIT_DIALOG" // 关闭编辑帖子状态
};
// action creators
export const actions = {
// 打开新建帖子的编辑框
openAddDialog: () => ({
type: types.OPEN_ADD_DIALOG
}),
// 关闭新建帖子的编辑框
closeAddDialog: () => ({
type: types.CLOSE_ADD_DIALOG
}),
// 打开编辑帖子的编辑框
openEditDialog: () => ({
type: types.OPEN_EDIT_DIALOG
}),
// 关闭编辑帖子的编辑框
closeEditDialog: () => ({
type: types.CLOSE_EDIT_DIALOG
})
};
// reducers
const reducer = (state = initialState, action) => {
switch (action.type) {
case types.OPEN_ADD_DIALOG:
return { ...state, addDialogOpen: true };
case types.CLOSE_ADD_DIALOG:
case postTypes.CREATE_POST:
return { ...state, addDialogOpen: false };
case types.OPEN_EDIT_DIALOG:
return { ...state, editDialogOpen: true };
case types.CLOSE_EDIT_DIALOG:
case postTypes.UPDATE_POST:
return { ...state, editDialogOpen: false };
default:
return state;
}
};
export default reducer;
index模块
在 redux/modules 路径下,我们还会创建一个 index.js,作为 Redux 的根模块。在 index.js 中做的事情很简单,只是将其余模块中的 reducer 合并成一个根 reducer。index.js 的代码如下:
import { combineReducers } from "redux";
import app from "./app";
import auth from "./auth";
import ui from "./ui";
import comments, { getCommentIdsByPost, getCommentById } from "./comments";
import posts, { getPostIds, getPostById } from "./posts";
import users, { getUserById } from "./users";
// 合并所有模块的reducer成一个根reducer
const rootReducer = combineReducers({
app,
auth,
ui,
posts,
comments,
users
});
export default rootReducer;