当涉及到构建大型应用程序时,应用程序的状态是良好的结构和清晰的定义是至关重要的。在这篇文章中,我们将介绍如何将MobX用于大规模应用。我们将专注于如何结构化应用程序的状态,定义数据之间的关系,进行网络调用,并在存储中加载数据。
因为这篇文章专注于状态管理,所以我们不会花太多时间来创建UI/造型组件。
前提条件
- 对面向对象编程的基本理解
- 对React和React Hooks的基本理解
- MobX的基本知识
准备好了吗?让我们开始吧。
在React和MobX中设置你的企业应用
首先,用create-react-app创建一个TypeScript React应用。
$ npx create-react-app react-mobx-app --template=typescript
现在,进入app文件夹并安装以下内容。
$ yarn add react-router-dom mobx mobx-react axios
其中一些需要类型,所以让我们也安装它们。
$ yarn add -D @types/react-router-dom
让我们删除那些我们不需要的文件。我们可以删除App.tsx,App.test.tsx,App.css, 和logo.svg。我们以后会再次创建必要的文件。
在我们开始构建应用程序之前,让我们看一下我们要构建的东西和商店的结构。我们将建立一个简单的博客应用。该应用程序将有三个实体,即用户、帖子和评论。下面是它们之间的关系。
- 用户(有许多帖子)
- 帖子(有很多评论,属于一个用户)
- 评论(属于一个帖子)
- 帖子(有很多评论,属于一个用户)
创建实体类型
现在我们知道了结构,让我们为它创建类型。我们将把类型放在src下的一个名为types的文件夹中。
让我们先在types 下创建user.ts。这就是它的样子。
export default interface IUser {
id: number;
name: string;
username: string;
email: string;
}
然后,types*/post.ts*。
export default interface IPost {
id: number;
userId: number;
title: string;
body: string;
}
最后是types/comment.ts。
export default interface IComment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
应用商店
现在我们需要创建模型。这些模型将实现上述类型,同时定义其他实体之间的关系。为了定义这些关系,这些模型将需要访问应用商店。但我们还没有创建应用商店。让我们现在就做吧。
在s stores文件夹下的src下创建一个名为stores的文件夹。现在,创建一个名为app.ts的文件。这个文件将包含应用程序的所有商店。现在,让我们把它变成一个空类,像这样。
export default class AppStore {}
创建模型
现在我们有了应用商店,让我们在src下创建一个名为models的文件夹,开始实现我们的模型。
首先,用户模型*(models/user.ts*)。
import AppStore from "../stores/app";
import IUser from "../types/user";
export default class User implements IUser {
id: number;
name: string;
username: string;
email: string;
constructor(private store: AppStore, user: IUser) {
this.id = user.id;
this.name = user.name;
this.username = user.username;
this.email = user.email;
}
}
我们的用户模型实现了用户类型,构造函数需要两个参数。第一,用于定义关系的AppStore ,第二,用户类型来实例化成员变量。
以同样的方式,让我们创建一个帖子模型和一个评论模型。
这里是我们的帖子模型*(models/post.ts*)的代码。
import AppStore from "../stores/app";
import IPost from "../types/post";
export default class Post implements IPost {
id: number;
userId: number;
title: string;
body: string;
constructor(private store: AppStore, post: IPost) {
this.id = post.id;
this.userId = post.userId;
this.title = post.title;
this.body = post.body;
}
}
这里是我们的评论模型*(models/comment.ts*)。
import AppStore from "../stores/app";
import IComment from "../types/comment";
export default class Comment implements IComment {
id: number;
postId: number;
name: string;
email: string;
body: string;
constructor(private store: AppStore, comment: IComment) {
this.id = comment.id;
this.postId = comment.postId;
this.name = comment.name;
this.email = comment.email;
this.body = comment.body;
}
}
但是我们的模型缺少一些东西--关系。我们还没有定义它们之间的任何关系,但我们在创建商店后会这样做。
在MobX中创建商店
让我们创建商店,从用户商店开始。在商店文件夹中创建一个名为user.ts的文件。粘贴下面的代码。
import User from "../models/user";
import IUser from "../types/user";
import AppStore from "./app";
export default class UserStore {
byId = new Map<number, User>();
constructor(private store: AppStore) {}
load(users: IUser[]) {
users.forEach((it) => this.byId.set(it.id, new User(this.store, it)));
}
get all() {
return Array.from(this.byId.values());
}
}
让我解释一下这里发生了什么。
首先,我们有一个名为byId 的地图。它将存储我们所有的用户记录,关键是id 。为什么是地图?因为它更容易获得、更新和删除一个记录。
构造函数再次接受了AppStore ,这样它就可以把它传递给模型实例。
接下来,我们有一个load 方法,它接收了一个IUser 类型的数组,并通过实例化用户模型将其加载到byId 地图中。
最后,我们有一个名为all 的getter属性。它返回商店中所有可用的用户记录。
注意,我们的商店是普通的类。我们还没有把它变成一个MobX商店。
为了做到这一点,我们将对我们的商店做如下修改。
- 将
byId成员变量类型从Map改为MobX的observable.map。通过制作一个属性observable,我们告诉MobX观察变化并在必要时重新渲染组件。在我们的例子中,我们使用observable.map,这意味着当一条记录被添加、更新或从map,我们将收到一个更新。 load方法是将数据加载到商店中。我们必须通过用action装饰方法来告诉MobX,我们正在更新观测器。allgetter属性实际上是从byIdobservable派生的。我们必须让MobX知道这是一个计算的属性,用computed来装饰它。- 最后,我们必须在构造函数中通过这个实例来调用
makeObservable函数,以使一切正常工作。
在做了上述改变之后,我们的商店将是这样的。
import {
action,
computed,
makeObservable,
observable,
ObservableMap,
} from "mobx";
import User from "../models/user";
import IUser from "../types/user";
import AppStore from "./app";
export default class UserStore {
byId = observable.map<number, User>();
constructor(private store: AppStore) {
makeObservable(this);
}
@action load(users: IUser[]) {
users.forEach((it) => this.byId.set(it.id, new User(this.store, it)));
}
@computed get all() {
return Array.from(this.byId.values());
}
}
以同样的方式,让我们创建PostStore 和CommentStore 。
对于stores/post.ts。
import {
action,
computed,
makeObservable,
observable,
ObservableMap,
} from "mobx";
import Post from "../models/post";
import IPost from "../types/post";
import AppStore from "./app";
export default class PostStore {
byId = new observable.map<number, Post>();
constructor(private store: AppStore) {
makeObservable(this);
}
@action load(posts: IPost[]) {
posts.forEach((it) => this.byId.set(it.id, new Post(this.store, it)));
}
@computed get all() {
return Array.from(this.byId.values());
}
}
现在,对于stores/comment.ts:
import {
action,
computed,
makeObservable,
observable,
ObservableMap,
} from "mobx";
import IComment from "../types/comment";
import Comment from "../models/comment";
import AppStore from "./app";
export default class CommentStore {
byId = new observable.map<number, Comment>();
constructor(private store: AppStore) {
makeObservable(this);
}
@action load(comments: IComment[]) {
comments.forEach((it) => this.byId.set(it.id, new Comment(this.store, it)));
}
@computed get all() {
return Array.from(this.byId.values());
}
}
由于我们的商店被创建了,让我们像这样在AppStore 中把它们实例化。
import CommentStore from "./comment";
import PostStore from "./post";
import UserStore from "./user";
export default class AppStore {
user = new UserStore(this);
post = new PostStore(this);
comment = new CommentStore(this);
}
现在,让我们回到定义我们模型之间的关系。
用户模型。正如我们前面所讨论的,用户将有许多帖子。让我们为之编码一个关系。
import AppStore from "../stores/app";
import IUser from "../types/user";
export default class User implements IUser {
id: number;
name: string;
username: string;
email: string;
constructor(private store: AppStore, user: IUser) {
this.id = user.id;
this.name = user.name;
this.username = user.username;
this.email = user.email;
}
get posts() {
return this.store.post.all.filter((it) => it.userId === this.id);
}
}
如果你看一下我们的帖子关系,你会注意到它是一个来自post.all 计算属性的计算属性。我们必须用computed 装饰器来装饰它,以便MobX计算它,并在构造函数中调用makeObservable ,告诉MobX这个类有observable 属性。
这就是最终的版本。
import { computed, makeObservable } from "mobx";
import AppStore from "../stores/app";
import IUser from "../types/user";
export default class User implements IUser {
id: number;
name: string;
username: string;
email: string;
constructor(private store: AppStore, user: IUser) {
this.id = user.id;
this.name = user.name;
this.username = user.username;
this.email = user.email;
makeObservable(this);
}
@computed get posts() {
return this.store.post.all.filter((it) => it.userId === this.id);
}
}
现在让我们把其他两个模型的代码也写出来。
帖子模型。
import { computed, makeObservable } from "mobx";
import AppStore from "../stores/app";
import IPost from "../types/post";
export default class Post implements IPost {
id: number;
userId: number;
title: string;
body: string;
constructor(private store: AppStore, post: IPost) {
this.id = post.id;
this.userId = post.userId;
this.title = post.title;
this.body = post.body;
makeObservable(this);
}
@computed get user() {
return this.store.user.byId.get(this.userId);
}
@computed get comments() {
return this.store.comment.all.filter((it) => it.postId === this.id);
}
}
评论模型。
import { computed, makeObservable } from "mobx";
import AppStore from "../stores/app";
import IComment from "../types/comment";
export default class Comment implements IComment {
id: number;
postId: number;
name: string;
email: string;
body: string;
constructor(private store: AppStore, comment: IComment) {
this.id = comment.id;
this.postId = comment.postId;
this.name = comment.name;
this.email = comment.email;
this.body = comment.body;
makeObservable(this);
}
@computed get post() {
return this.store.post.byId.get(this.postId);
}
}
就这样,我们已经成功地为我们的应用程序创建了整个商店
编码网络层
到目前为止,我们已经为我们的应用程序创建了商店,但网络层还没有完成。现在让我们对我们的应用程序的网络层进行编码,它将负责进行网络调用和加载商店中的数据。
我们要将网络层与商店完全分开。商店将不知道数据是从哪里加载的。
让我们首先在src/apis文件夹下的app.ts文件中创建一个主AppApi 类。它将包含其他资源的网络调用。
下面是src/apis/app.ts的代码。
import axios from "axios";
import AppStore from "../stores/app";
export default class AppApi {
client = axios.create({ baseURL: "https://jsonplaceholder.typicode.com" });
constructor(store: AppStore) {}
}
我们还将创建一个axios 客户端成员变量,它将被用来进行网络调用。
我们将使用JSONPlaceholderFake REST API来获取数据,并为我们的axios 客户端设置基础URL为jsonplaceholder.typicode.com。
AppStore 是在构造函数中传递给我们的。我们将使用AppStore ,在从API获得数据后将数据加载到商店。
现在,我们有了我们的AppApi 。让我们为你的个人资源编写网络调用。我们将从用户开始。首先,在src/apis文件夹下创建一个名为user.ts的文件,并粘贴以下代码。
import AppStore from "../stores/app";
import AppApi from "./app";
export default class UserApi {
constructor(private api: AppApi, private store: AppStore) {}
async getAll() {
const res = await this.api.client.get(`/users`);
this.store.user.load(res.data);
}
async getById(id: number) {
const res = await this.api.client.get(`/users/${id}`);
this.store.user.load([res.data]);
}
}
让我们了解一下这里发生了什么。
首先,我们创建一个UserApi 类,将AppApi 和AppStore 作为构造参数。然后,我们将使用AppApi 实例来获取axios 客户端并进行网络调用。在获得数据后,我们使用AppStore ,将数据加载到存储中。
让我们单独看看这些方法。
getAll - 向/users 发出一个获取请求。响应会以用户对象数组的形式返回,使用商店的加载方法将其加载到商店中。
getById - 向/users/$id 发送一个获取请求,以获取一个单一的用户记录。响应是以单个用户对象的形式返回的,所以我们在将用户对象传递给商店的加载方法之前,用方括号将其包裹在一个数组中。
类似地,我们将为另外两个资源,即帖子和评论,创建另外两个API客户端。
下面是我们的postapis/post.ts的代码。
import AppStore from "../stores/app";
import AppApi from "./app";
export default class PostApi {
constructor(private api: AppApi, private store: AppStore) {}
async getAll() {
const res = await this.api.client.get(`/posts`);
this.store.post.load(res.data);
}
async getById(id: number) {
const res = await this.api.client.get(`/posts/${id}`);
this.store.post.load([res.data]);
}
async getByUserId(userId: number) {
const res = await this.api.client.get(`/posts?userId=${userId}`);
this.store.post.load(res.data);
}
}
而对于评论apis/comment.ts。
import AppStore from "../stores/app";
import AppApi from "./app";
export default class CommentApi {
constructor(private api: AppApi, private store: AppStore) {}
async getByPostId(postId: number) {
const res = await this.api.client.get(`/posts/${postId}/comments`);
this.store.comment.load(res.data);
}
}
注意,我只为所需的API调用写了方法。你可以根据你的要求增加或删除调用。
现在,我们已经完成了API客户端的编写,所以让我们在AppApi 类中注册它们。
import axios from "axios";
import AppStore from "../stores/app";
import CommentApi from "./comment";
import PostApi from "./post";
import UserApi from "./user";
export default class AppApi {
client = axios.create({ baseURL: "https://jsonplaceholder.typicode.com" });
user: UserApi;
post: PostApi;
comment: CommentApi;
constructor(store: AppStore) {
this.user = new UserApi(this, store);
this.post = new PostApi(this, store);
this.comment = new CommentApi(this, store);
}
}
我们已经完成了为我们的整个应用程序创建网络层的工作。
使用商店和API的应用程序上下文
我们为我们的博客应用创建了商店和网络层,但必须有一种方法让我们在React组件中使用它们和API。为此,我们将使用React Context来为我们的组件提供商店和API。
import React, { useContext } from "react";
import AppApi from "./apis/app";
import AppStore from "./stores/app";
interface AppContextType {
store: AppStore;
api: AppApi;
}
const AppContext = React.createContext<null | AppContextType>(null);
export const useAppContext = () => {
const context = useContext(AppContext);
return context as AppContextType;
};
export default AppContext;
这是很直接的。我们为context 创建一个类型,它有两个属性:store 和api 。
然后我们创建一个名为AppContext 的React Context,为我们的应用程序提供store 和api 。最后,我们有一个useAppContext 的自定义React钩子来利用我们组件中的store 和api 。
让我们把上述代码放在src文件夹下的app-context.ts文件中。
使用MobX的组件
因为我们不专注于UI,所以组件是直接的。我们将使用基本的React概念,如用于组件安装回调的useEffect,从react-router-dom ,以获得URL的参数,我们将把模型实例传递给组件作为道具来显示数据。
让我们从创建评论组件开始。我们将把组件文件放在src/components下,名称为comment.tsx。
import CommentModel from "../models/comment";
const Comment: React.FC<{ comment: CommentModel }> = ({ comment }) => {
return (
<div>
<strong>
{comment.name} • {comment.email}
</strong>
<p>{comment.body}</p>
<br />
</div>
);
};
export default Comment;
我们的组件缺少非常重要的东西。我们必须让MobX知道,我们的组件将观察来自商店的可观察对象的变化。
为了做到这一点,我们将用mobx-react 包中的observer 来包装我们的React组件。下面是更新后的组件的样子。
import { observer } from "mobx-react";
import CommentModel from "../models/comment";
const Comment: React.FC<{ comment: CommentModel }> = observer(({ comment }) => {
return (
<div>
<strong>
{comment.name} • {comment.email}
</strong>
<p>{comment.body}</p>
<br />
</div>
);
});
export default Comment;
以及post组件*(components/post.tsx*)。
import { observer } from "mobx-react";
import React from "react";
import { Link } from "react-router-dom";
import PostModel from "../models/post";
const Post: React.FC<{ post: PostModel; ellipsisBody?: boolean }> = observer(
({ post, ellipsisBody = true }) => {
return (
<div>
<h2>{post.title}</h2>
<p>
{ellipsisBody ? post.body.substr(0, 100) : post.body}
{ellipsisBody && (
<span>
...<Link to={`/post/${post.id}`}>read more</Link>
</span>
)}
</p>
<p>
Written by <Link to={`/user/${post.userId}`}>{post.user?.name}</Link>
</p>
</div>
);
}
);
export default Post;
在我们的React应用程序中构建页面
我们的React应用程序将有三个页面。
- 主页 - 显示一个帖子列表
- 帖子页--显示帖子的内容和它收到的评论
- 用户页--显示用户信息和用户写的帖子列表
主页
把主页文件放在pages/home.tsx下。
import { observer } from "mobx-react";
import { useEffect, useState } from "react";
import { useAppContext } from "../app-context";
import Post from "../components/post";
const HomePage = observer(() => {
const { api, store } = useAppContext();
const [loading, setLoading] = useState(false);
const load = async () => {
try {
setLoading(true);
await api.post.getAll();
await api.user.getAll();
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
if (loading) {
return <div>loading...</div>;
}
return (
<div>
<h1>Posts</h1>
{store.post.all.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
);
});
export default HomePage;
这里,我们使用useAppContext 钩子来获取store 和api 。注意我们有一个useState钩子来存储我们API调用的加载状态。之后,我们有一个load函数,它将加载设置为true ,所有的帖子和用户,并设置加载false 。
最后,用一个空数组作为deps ,创建一个组件的加载效果,并在其中调用load 函数。
让我们对其他剩余的页面进行编码。
帖子页面 (pages/post.tsx)
import { observer } from "mobx-react";
import { useEffect, useState } from "react";
import { useParams } from "react-router";
import { useAppContext } from "../app-context";
import Post from "../components/post";
const PostPage = observer(() => {
const { api, store } = useAppContext();
const [loading, setLoading] = useState(false);
const params = useParams<{ postId: string }>();
const postId = Number(params.postId);
const load = async () => {
try {
setLoading(true);
await api.post.getById(postId);
await api.comment.getByPostId(postId);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
if (loading) {
return <div>loading...</div>;
}
const post = store.post.byId.get(Number(params.postId));
if (!post) {
return <div>Post not found</div>;
}
return (
<div>
<Post ellipsisBody={false} post={post} />
<h2>Comments </h2>
{post.comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
</div>
);
});
export default PostPage;
我们在这里的唯一区别是。
- 我们使用
useParams钩子获得postId。react-router-dom - 我们正在加载帖子和帖子的评论
- 在渲染中,我们使用我们在模型中创建的
post.comments关系来获取和渲染所有的帖子的评论。
最后,让我们对用户页面进行编码。
import { observer } from "mobx-react";
import { useEffect, useState } from "react";
import { useParams } from "react-router";
import { useAppContext } from "../app-context";
import Post from "../components/post";
const UserPage = observer(() => {
const { api, store } = useAppContext();
const [loading, setLoading] = useState(false);
const params = useParams<{ userId: string }>();
const userId = Number(params.userId);
const load = async () => {
try {
setLoading(true);
await api.user.getById(userId);
await api.post.getByUserId(userId);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
if (loading) {
return <div>loading...</div>;
}
const user = store.user.byId.get(userId);
if (!user) {
return <div>User not found</div>;
}
return (
<div>
<h3>
{user.name} • {user.username}
</h3>
<p>{user.email}</p>
<h2>Posts</h2>
{user.posts.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
);
});
export default UserPage;
这样,我们就成功地完成了我们的整个应用
现在只剩下一个项目了:根应用程序组件。记住,我们在开始时删除了App.tsx文件。让我们把它加回到src文件夹下。
app.tsx
import { BrowserRouter, Route, Switch } from "react-router-dom";
import AppContext from "./app-context";
import AppStore from "./stores/app";
import AppApi from "./apis/app";
import HomePage from "./pages/home";
import PostPage from "./pages/post";
import UserPage from "./pages/user";
const store = new AppStore();
const api = new AppApi(store);
function App() {
return (
<AppContext.Provider value={{ store, api }}>
<BrowserRouter>
<Switch>
<Route path="/user/:userId" component={UserPage} />
<Route path="/post/:postId" component={PostPage} />
<Route path="/" component={HomePage} />
</Switch>
</BrowserRouter>
</AppContext.Provider>
);
}
export default App;
我们正在实例化商店和API,并通过AppContext.Provider向我们的应用程序提供它。
使用BrowserRouter fromreact-router-dom ,我们将渲染我们的页面。这就是了。如果你在浏览器中运行并打开该应用,你应该看到该应用正在工作。
总结
在本教程中,我们学习了如何使用MobX来管理大规模的React状态。谢谢你的阅读,如果你有任何问题,请联系我!
你可以在这里获得整个项目的源代码,或者测试一下实时应用程序。
varunpvp/react-mobx-app
你现在不能执行该操作。您在另一个标签或窗口中登录。您在另一个标签或窗口中签出。重新加载以刷新您的会话。重新加载以刷新您的会话。
The postUsing MobX for large-scale enterprise state managementappeared first onLogRocket Blog.