React进阶之路——MobX项目实战

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/feng_zhiyu/article/details/82989949

本博客pdf链接: https://pan.baidu.com/s/1QFyYm_hJhZE4EWgq6yvpAg 提取码: 5mgx

React进阶之路系列学习笔记:React进阶之路系列学习笔记

github源码:React_learning

11.0 MoBX知识结构与核心概念

1.MobX知识结构

2.MobX数据流管理状态

 

3.MobX核心API

1、observable        P204-211

使用方式: observable(value)

 @observable classProperty=value 【建议使用】

2、computed              P211-212

使用方式:computed(() => expression)

@computed get classProperty(){return expression;}【建议使用】

3、reaction              P212-214

使用方式: observer((props, context) => ReactElement)【建议使用】

observer(class MyComponent extends React.Component{…})

@observer class MyComponent extends React.Component{…}【建议使用】

4、action             P215-216

使用方式: action(fn)

       @action classMethod 【建议使用】

11.1组织项目结构

  1. api:网络请求        【将网络请求封装成接口】
  2. components:组件 【各个页面的渲染组件】
  3. images:项目图片
  4. models:model实体类 【与数据库表对应】
  5. stores:存放三类state数据
  6. utils:工具模块,如:加密、 发起请求、异步请求等

11.2设计store

Store的职责:将组件使用的业务逻辑和状态封装到单独的模块中,从而组件专注于UI渲染。

state的分类:

1.与领域直接相关的领域状态数据

2.反映应用行为的应用状态数据

3.代表UI状态的UI状态数据

后两种state一般不会设计太多逻辑,仅仅是关于应用、UI的一些松散状态的读取和简单修改,封装这两种state的store实现很直观。领域store的数据结构较为复杂,且往往涉及较多的逻辑处理。

//普通对象
var todo={id: 1, title: "Todo1", finished: false}

//class
class Todo{
 
id;
 
title;
 
finished;
}

class描述的优势:

  1. Class内部可以定方法,可以自己保存上下文信息而不依赖外部,容易被单独使用
  2. Class内部可以方便的混合使用可观测属性和费观测属性
  3. Class描述的state辨识度高且易于进行类型检验  

    稍复杂的领域store建议用class描述。

   一个领域的state和state的管理都由领域store负责。

具体来说,领域store职责:

  1. 实例化领域store,并保证领域store知道它的所属state
  2. 每一个领域store应用中只能有一个实例对象
  3. 更新领域state(服务端获取、纯客户端修改)

本项目中store设计如下:

领域store: PostStore、CommentsStore

应用状态store:AppStore、AuthStore

UI store: UIStore

领域state: PostModel、CommentModel

说明:MobX中的props来源自inject和Provider的结合使用,从Provider提供的state中选取所需数据,作为props传递给目标组件;而state则通过observable来声明

项目部分模块详解

下面对stores中的PostStore的整个流程代码详解,该部分可作为后期使用Readct+MobX开发的一个组织结构的参考。

先来看一个大致的思维导图:

stores模块的index.js

/**

 * 此JS文件将stores中的所有store整合到一个stores并创建一个实例对象(只能有唯一一个实例对象)

 * import *Store from "./*Store"; 导入新建对象所在的文件

 * import *Api from "./api/Api"; 导入api模块下的api文件

 */

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";



/// 创建实例对象,将*Api和*Store作为参数传入构造器使对应的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();



///整合到一个stores对象中方便根目录下的Provider的引入

const stores = {

  appStore,

  authStore,

  postsStore,

  commentsStore,

  uiStore

};

/// 默认导出接口

export default stores;

 

PostModel.js

import { observable, action } from "mobx";



/**

 * PostModel类中的可观测属性与数据库表中的字段对应,认为是一个中间类

 */

class PostModel {

  // store与id为不可观测属性, MobX中不可观测属性是指对MobX数据流不会产生影响

  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;

 

api模块中的postApi.js

import { get, post, put } from "../utils/request";

import url from "../utils/url"; ///导入网络请求对应的utils模块的url文件

/// 导出方法的默认接口,其他模块直接调用getPostList()、getPostById()等四个方法

export default {

  /// 箭头函数实现接口方法与url的一个映射

 getPostList: () => get(url.getPostList()),

  getPostById: id => get(url.getPostById(id)),

  createPost: data => post(url.createPost(), data),

  updatePost: (id, data) => put(url.updatePost(id), data)

};

 

PostStore.js

import { observable, action, toJS } from "mobx";

import PostModel from "../models/PostModel";//导入需要的PostModel类



class PostsStore {

  api;

  appStore;

  authStore;

  @observable posts = [];  // 数组的元素是PostModel的实例,存储帖子

    ///此构造器在index.js实例化时使用;后面可以使用api、appStore、authStore的属性和方法

  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);///获取当前store中帖子id等于id即item.id===id的帖子

  }



  // 从服务器获取帖子列表

  @action fetchPostList() {

    this.appStore.increaseRequest();///requestQuantity+1,当前进行的请求数量+1

      /**

       * .then()是异步处理,先执行this.api.getPostList()获取帖子列表,然后执行then()

       */

    return this.api.getPostList().then(

      action(data => {

        this.appStore.decreaseRequest();///requestQuantity-1,当前进行的请求数量-1

        if (!data.error) {///如果获取的data没有出错

          this.posts.clear();///清空帖子列表

            /**

             * 先执行箭头函数部分,将post作为参数传入PostModel.fromJS()

             * 再调用PostModel.fromJS(this, post)创建PostModel实例,

             * 然后执行this.posts.push()添加到帖子列表中

             * data.forEach()是对data的所有帖子进行一个遍历

             */

          data.forEach(post => this.posts.push(PostModel.fromJS(this, post)));

            /**

             * 创建一个Promise对象并返回a resolved promise,data没出错时调用

             */

          return Promise.resolve();

        } else {//数据出错

          this.appStore.setError(data.error);//数据出错,调用appStore设置错误信息

            /**

             * 创建一个Promise对象,返回a rejected promise,出错时调用这个方法

             */

          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) {///数据没有出错且数据长度为1(因为id是唯一的)

          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) {

      //新建帖子,使用扩展运算符...添加到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对象。 */

              /**

               * toJS()是MobX提供的,这里将可观测对象转换为toJS()对象是将可观测对象转化为json格式便于

               * 调用updateFromJS(data)作为参数传入

               * 在MobX2.2前的命名为toJSON()

               */

            data.author = toJS(oldPost.author);



            oldPost.updateFromJS(data);

          } 

          return Promise.resolve();

        } else {

          this.appStore.setError(data.error);

          return Promise.reject();

        }

      })

    );

  }

}



export default PostsStore;

 

11.3 视图层重构

视图层的重构工作主要是observer/@observer将组件转换成一个个reaction,同时使用mobx-react提供的inject/@inject注入组件所需的store。

首先,在组件树的最外层使用mobx-react提供的Provider组件注入合并后的store。

import React from "react";

import ReactDOM from "react-dom";

import { useStrict } from 'mobx';///导入useStrict

import { Provider } from "mobx-react";//Provider组件注入合并后的store

import App from "./components/App";

import stores from "./stores";

///在严格模式下,运行MobX

useStrict(true);



ReactDOM.render(

  <Provider {...stores}>

    <App />

  </Provider>,

  document.getElementById("root")

);

组件App:

import React, { Component } from "react";

import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

import { inject, observer } from "mobx-react";//使用@inject和@observer

import asyncComponent from "../../utils/AsyncComponent";

import ModalDialog from "../../components/ModalDialog";

import Loading from "../../components/Loading";

import connectRoute from "../../utils/connectRoute";



/**

 * 使用高阶组件实现异步加载

 */

const AsyncHome = connectRoute(asyncComponent(() => import("../Home")));

const AsyncLogin = connectRoute(asyncComponent(() => import("../Login")));



@inject("appStore")

@observer// 将App组件转化为一个reaction,自动响应state的变化

class App extends Component {

  //在开发环境下,添加MobX调试工具

  renderDevTool() {

    if (process.env.NODE_ENV !== "production") {

      const DevTools = require("mobx-react-devtools").default;

      return <DevTools />;

    }

  }



  render() {

    const { error, isLoading, removeError } = this.props.appStore;

    // 错误弹窗

    const errorDialog = error && (

      <ModalDialog onClose={removeError}>{error.message || error}</ModalDialog>

    );



    return (

      <div>

        <Router>

          <Switch>

            {/*exact属性限定室友访问根路径时,第一个Route才会破匹配成功*/}

            <Route exact path="/" component={AsyncHome} />

            <Route path="/login" component={AsyncLogin} />

            <Route path="/posts" component={AsyncHome} />

          </Switch>

        </Router>

        {/*调用错误弹窗*/}

        {errorDialog}

        {/*调用加载组件*/}

        {isLoading && <Loading />}

        {/* 添加MobX调试组件 */}

        {this.renderDevTool()}

      </div>

    );

  }

}



export default App;

 

11.4MObX调试工具

mobx-react-devtools是一个用于调试MobX+React项目的工具,它可以追踪组件的渲染以及组件依赖的可观测数据。

安装:npm install mobx-react-devtools  --save-dev

将mobx-react-devtools提供的调试组件添加到App组件。 代码及解析见11.3 视图层部分。

运行程序,界面右上角会多出三个小图标,这是mobx-react-devtools的功能键。

点击第一个图标,当组件发生渲染行为时,对应的组件会被高亮显示,高亮组件右上角显示的3个数字分别代表截止当前组件渲染的次数、组件render方法执行的时间、组件从render方法开始到渲染到浏览器界面使用的时间。

点击第二个图标后,再用鼠标选择任意一个组件,可以查看该组件会对哪些数据的变化做出响应。下图显示的是一个PostItem组件实例所依赖的数据。

点击第三个图标,控制台会输出发生的action、响应的reaction等调试日志信息。

 

11.5优化建议

  1. 尽可能多的使用小组件

Observer/@observer包装的组件会跟踪render方法中使用的所有可观测对象,所以组件越小,组件追踪的独享越少,引起组件重新渲染的可能性也越小。

  1. 在单独的组件中渲染列表数据

列表数据的渲染是比较耗费性能的,尤其是在列表数据量大的情况下。

  1. 尽可能晚地解引用对象属性

MobX通过追踪对象属性的访问来追踪值的变化,所以在层级越低的组件中解引用对象属性,由这个属性的变化导致的重新渲染的组价的数量越少。(只有解引用对象属性的组件及其子组件会重新渲染)

  1. 提前绑定函数

11.6Redux与MobX的比较

1.Store

Redux是单一数据源,整个应用共享一个store对象,而MobX可以使用多个Store。当应用越来越复杂时,在维护store在组件间的数据共享上Mobx相对Redux更复杂。

2.State

Redux使用普通JS对象存储state,并且state是不可变的,每次状态变更必须重新创建新的state。MobX中的state是可观测对象,并且state是可以直接改变的

3. 编程范式

Redux是基于函数式的编程思想,MobX是基于面向对象的编程思想,对于传统OOP开发者而言,MobX更友好。Redux有严格的规范约束,MobX更灵活,开发者可以更随意编写代码。对于大型项目而言,有严格规范更易于后期维护和扩展。

4. 代码量

因为Redux有严格的规范,所以往往需要写更多的代码来执行这些规范。MobX相对Redux的代码量更少。

5.学习曲线

因为MobX的OOP的编程思想以及没有太多规范约束,学习曲线平缓,容易上手;而Readux相对更难一些。

基于以上比较,一般建议开发相对简单的应用时,选择使用MobX,它易于学习、上手快、代码量少;当团队规模较大或应用复杂度较高时,选择使用Redux。它严格的规范有利于保障项目代码的可维护性和可扩展性。但是,技术选择没有绝对,最终要根据实际业务场景选择。

11.7本章小结

本章结合项目实例,从项目结构的组织方式、store的设计等方面详细介绍了如何在真实项目中使用MobX,还介绍了MobX应用中常用的调试工具和常用的性能优化方法,最后针对Redux和MobX进行了比较,并且对技术选择给予一定建议。

 

猜你喜欢

转载自blog.csdn.net/feng_zhiyu/article/details/82989949