使用MobX进行大规模企业状态管理

302 阅读6分钟

当涉及到构建大型应用程序时,应用程序的状态是良好的结构和清晰的定义是至关重要的。在这篇文章中,我们将介绍如何将MobX用于大规模应用。我们将专注于如何结构化应用程序的状态,定义数据之间的关系,进行网络调用,并在存储中加载数据。

因为这篇文章专注于状态管理,所以我们不会花太多时间来创建UI/造型组件。

前提条件

准备好了吗?让我们开始吧。

在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商店。

为了做到这一点,我们将对我们的商店做如下修改。

  1. byId 成员变量类型从Map 改为MobX的observable.map 。通过制作一个属性observable ,我们告诉MobX观察变化并在必要时重新渲染组件。在我们的例子中,我们使用observable.map ,这意味着当一条记录被添加、更新或从map ,我们将收到一个更新。
  2. load 方法是将数据加载到商店中。我们必须通过用action装饰方法来告诉MobX,我们正在更新观测器。
  3. all getter属性实际上是从byId observable派生的。我们必须让MobX知道这是一个计算的属性,用computed 来装饰它。
  4. 最后,我们必须在构造函数中通过这个实例来调用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());
  }
}

以同样的方式,让我们创建PostStoreCommentStore

对于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 类,将AppApiAppStore 作为构造参数。然后,我们将使用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 创建一个类型,它有两个属性:storeapi

然后我们创建一个名为AppContext 的React Context,为我们的应用程序提供storeapi 。最后,我们有一个useAppContext 的自定义React钩子来利用我们组件中的storeapi

让我们把上述代码放在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应用程序将有三个页面。

  1. 主页 - 显示一个帖子列表
  2. 帖子页--显示帖子的内容和它收到的评论
  3. 用户页--显示用户信息和用户写的帖子列表

主页

把主页文件放在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 钩子来获取storeapi 。注意我们有一个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;

我们在这里的唯一区别是。

  1. 我们使用useParams 钩子获得postIdreact-router-dom
  2. 我们正在加载帖子和帖子的评论
  3. 在渲染中,我们使用我们在模型中创建的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.