设计store
Store 的职责是将组件使用的业务逻辑和状态封装到单独的模块中,这样组件就可以专注于 UI 渲染。第 10 章介绍 state 时已经提到,state 可以分为三类:与领域直接相关的领域状态数据、反映应用行为(登录状态、当前是否有 API 请求等)的应用状态数据和代表 UI 状态的 UI 状态数据。后两种 state 一般不会涉及太多逻辑,仅仅是关于应用、UI 的一些松散状态的读取和简单修改,封装这两种 state 的 store 实现也很直观。领域 state 的数据结构较复杂,且往往涉及较多的逻辑处理。领域 state 可以使用普通对象来描述,也可以使用 class 来描述,例如描述一个待办任务 todo:
// 使用普通对象
var todo = { id: 1, title: "Todo1", finished: false }
// 使用class
class Todo {
id;
title;
finished;
}
使用 class 比使用普通对象描述 state 有一些优势:
-
class 内可以定义方法,可以自己保存上下文信息而不依赖外部,因此 class 描述的 state 比普通对象描述的 state 更容易被单独使用。
-
class 内可以方便地混合使用可观测属性和非可观测属性,例如,在 Todo class 中,我们希望只有 title 和 finished 是可观测的,那么只需要在这两个属性前使用 @observable,id 继续作为不可观测属性使用。
-
class 描述的 state 辨识度高且容易进行类型校验。
所以,稍复杂的领域 state 都建议大家使用 class 来描述。
一个领域 store 对应应用中的一个简单的领域概念,这个领域的 state 和 state 的管理都由这个 store 负责。具体来讲,领域 store 的职责有:
-
实例化领域 state,并且保证领域 state 知道它属于哪一个 store。
-
每一个领域 store 在应用中只能有一个实例对象,例如,应用中不能有两个 todoList store。
-
更新领域 state,无论是通过服务器端获取,还是来自纯客户端的修改。
根据上面的介绍,我们可以为 BBS 创建 5 个 store:AppStore、AuthStore、UIStore、PostsStore、CommentsStore。AppStore 和 AuthStore 是应用状态 store,UIStore 是 UI store,PostsStore 和 CommentsStore 是领域 store。另外,还可以创建 PostModel 和 CommentModel 两个 class,代表领域 state。下面就来逐一分析每个 store。
AppStore
AppStore 管理的 state 包括应用当前的请求数量 requestQuantity 和应用的错误信息 error:
class AppStore {
@observable requestQuantity = 0;
@observable error = null;
//...
}
requestQuantity 间接决定界面上是否需要显示 Loading 效果,因此可以创建一个 computed value 直接标识是否需要显示 Loading 效果:
class AppStore {
@computed get isLoading() {
return this.requestQuantity > 0;
}
// ...
}
最后,为 AppStore 添加修改 state 的 action,完整代码如下:
import {observable, computed, action} from "mobx";
class AppStore {
@observable requestQuantity = 0;
@observable error = null;
@computed get isLoading() {
return this.requestQuantity > 0;
}
// 当前进行的请求数量加1
@action increaseRequest() {
this.requestQuantity++;
}
// 当前进行的请求数量减1
@action decreaseRequest() {
if (this.requestQuantity > 0)
this.requestQuantity--;
}
// 设置错误信息
@action setError(error) {
this.error = error;
}
// 删除错误信息
@action.bound removeError() {
this.error = null;
}
}
export default AppStore;
这里,removeError 使用 @action.bound 绑定 this,因为存在 removeError 不是通过 AppStore 实例调用的场景,而是直接作为组件的回调函数被使用。为了保证 removeError 中的 this 一直指向的是 AppStore 的实例对象,所以使用了 @action.bound。
AuthStore
AuthStore 负责用户的登录认证,使用到的 state 包括 userId、username 和 password,除了包含直接修改这几个 state 的 action 外,还定义了登录和注销两个 action,这两个 action 涉及网络请求,所以又会改变 AppStore 的 state,我们通过构造函数把 AppStore 的实例和登录 API 传递给 AuthStore,代码如下:
import {observable, action} from "mobx";
class AuthStore {
api;
appStore;
@observable userId = sessionStorage.getItem("userId");
@observable username = sessionStorage.getItem("username") || "jack";
@observable password = "123456";
constructor(api, appStore) {
this.api = api;
this.appStore = appStore;
}
@action setUsername(username) {
this.username = username;
}
@action setPassword(password) {
this.password = password;
}
@action login() {
this.appStore.increaseRequest();
const params = {username: this.username, password: this.password};
return this.api.login(params).then(action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
this.userId = data.userId;
sessionStorage.setItem("userId", this.userId);
sessionStorage.setItem("username", this.username);
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
}));
}
@action.bound logout() {
this.userId = undefined;
this.username = 'jack';
this.password = '123456';
sessionStorage.removeItem("userId");
sessionStorage.removeItem("username");
}
}
export default AuthStore;
UIStore
UIStore 负责新建帖子和编辑帖子两个 UI 状态的控制,代码很简单,不多做解释:
import {observable, action} from "mobx";
class UIStore {
@observable addDialogOpen = false;
@observable editDialogOpen = false;
// 设置新建帖子编辑框的状态
@action setAddDialogStatus(status) {
this.addDialogOpen = status
}
// 设置修改帖子编辑框的状态
@action setEditDialogStatus(status) {
this.editDialogOpen = status
}
}
export default UIStore;
PostsStore
PostsStore 负责帖子对应的领域,我们单独创建一个 class PostModel,用来描述帖子对应的 state:
import {observable, action} from "mobx";
class PostModel {
store; // PostModel 实例对象所属的 store
id;
@observable title;
@observable content;
@observable vote;
@observable author;
@observable createdAt;
@observable updatedAt;
constructor(store, id, title, content, vote, author, createdAt, updatedAt) {
this.store = store;
this.id = id;
this.title = title;
this.content = content;
this.vote = vote;
this.author = author;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// 根据JSON对象更新帖子
@action updateFromJS(json) {
this.title = json.title;
this.content = json.content;
this.vote = json.vote;
this.author = json.author;
this.createdAt = json.createdAt;
this.updatedAt = json.updatedAt;
}
// 静态方法,创建新的PostModel实例
static fromJS(store, object) {
return new PostModel(
store,
object.id,
object.title,
object.content,
object.vote,
object.author,
object.createdAt,
object.updatedAt
);
}
}
export default PostModel;
PostModel 包含帖子标题、内容、作者等可观测属性,action updateFromJS 用于根据服务器端返回的数据更新 PostModel 实例,静态方法 fromJS 用于根据服务器端返回的数据构造 PostModel 的实例,这两个方法在 PostsStore 中都要用到。
PostsStore 中保存一个可观测的数组 posts,posts 的元素是 PostModel 的实例,PostsStore 的 action 包括获取帖子列表、获取帖子详情、新建帖子和修改帖子,代码如下:
import {observable, action, toJS} from "mobx";
import PostModel from "../models/PostModel";
class PostsStore {
api;
appStore;
authStore;
@observable posts = []; // 数组的元素是PostModel的实例
constructor(api, appStore, authStore) {
this.api = api;
this.appStore = appStore;
this.authStore = authStore;
}
// 根据帖子id,获取当前store中的帖子
getPost(id) {
return this.posts.find(item => item.id === id);
}
// 从服务器获取帖子列表
@action fetchPostList() {
this.appStore.increaseRequest();
return this.api.getPostList().then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
this.posts.clear();
data.forEach(post => this.posts.push(PostModel.fromJS(this, post)));
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
// 从服务器获取帖子详情
@action fetchPostDetail(id) {
this.appStore.increaseRequest();
return this.api.getPostById(id).then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error && data.length === 1) {
const post = this.getPost(id);
// 如果store中当前post已存在,更新post;否则,添加post到store
if (post) {
post.updateFromJS(data[0]);
} else {
this.posts.push(PostModel.fromJS(this, data[0]));
}
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
// 新建帖子
@action createPost(post) {
const content = {...post, author: this.authStore.userId, vote: 0};
this.appStore.increaseRequest();
return this.api.createPost(content).then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
this.posts.unshift(PostModel.fromJS(this, data));
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
// 更新帖子
@action updatePost(id, post) {
this.appStore.increaseRequest();
return this.api.updatePost(id, post).then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
const oldPost = this.getPost(id);
if (oldPost) {
/* 更新帖子的API,返回数据中的author只包含authorId,
因此需要从原来的post对象中获取完整的author数据。
toJS是MobX提供的函数,用于把可观测对象转换成普通的JS对象。 */
data.author = toJS(oldPost.author);
oldPost.updateFromJS(data);
}
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
}
export default PostsStore;
CommentsStore
CommentsStore 负责评论对应的领域,与 PostsStore 相同,先创建 class CommentModel,描述评论对应的 state:
import {observable} from "mobx";
class CommentModel {
store;
id;
@observable content;
@observable author;
@observable createdAt;
@observable updatedAt;
constructor(store, id, content, author, createdAt, updatedAt) {
this.store = store;
this.id = id;
this.content = content;
this.author = author;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
static fromJS(store, object) {
return new CommentModel(
store,
object.id,
object.content,
object.author,
object.createdAt,
object.updatedAt
);
}
}
export default CommentModel;
CommentsStore 中保存一个可观测的数组 comments,comments 的元素是 CommentModel 的实例。CommentsStore 中定义的 action 包括获取某个帖子的评论列表和新建评论,代码如下:
import {observable, action} from "mobx";
import CommentModel from "../models/CommentModel";
class CommentsStore {
api;
appStore;
authStore;
@observable comments = []; // 数组的元素是CommentModel的实例
constructor(api, appStore, authStore) {
this.api = api;
this.appStore = appStore;
this.authStore = authStore;
}
// 获取评论列表
@action fetchCommentList(postId) {
this.appStore.increaseRequest();
return this.api.getCommentList(postId).then(action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
this.comments.clear();
data.forEach(item => this.comments.push(CommentModel.fromJS(this, item)));
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
}));
}
// 新建评论
@action createComment(content) {
this.appStore.increaseRequest();
return this.api.createComment(content).then(action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
this.comments.unshift(CommentModel.fromJS(this, data));
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
}));
}
}
export default CommentsStore;
合并store
通常会把使用到的多个 store 再次合并成一个根 store,利用 mobx-react 提供的高阶组件 Provider 将根 store 注入组件树中。我们在 stores/index.js 中完成这项工作:
import AppStore from "./AppStore";
import AuthStore from "./AuthStore";
import PostsStore from "./PostsStore";
import CommentsStore from "./CommentsStore";
import UIStore from "./UIStore";
import authApi from "../api/authApi";
import postApi from "../api/postApi";
import commentApi from "../api/commentApi";
// 每个 store 在应用中只存在一个实例对象
const appStore = new AppStore();
const authStore = new AuthStore(authApi, appStore);
const postsStore = new PostsStore(postApi, appStore, authStore);
const commentsStore = new CommentsStore(commentApi, appStore, authStore);
const uiStore = new UIStore();
const stores = {
appStore,
authStore,
postsStore,
commentsStore,
uiStore
};
export default stores;
在 stores/index.js 中,对每一个 store 都进行实例化,然后合并成一个对象导出。这样既保证每一个 store 只有一个实例,又便于外部组件的使用。