手把手从零开始搭建第一个MobX+React入门示例应用

  • 文中的蓝色字体是相关内容的超链接,网址不另外列出,请放心点击。
  • 本文内容适合 MobX 和 React 新手,也欢迎 MobX 和 React 专家指导点评。

摘要

阅读本文并实际上手编码运行,你将解决如下几个疑问:

  • MobX如何进行状态管理
  • MobX如何管理异步操作对状态的改变(fetch的使用)
  • 如何创建一个简单的MobX+React 示例应用(无路由)

工具

  • JetBrains WebStorm
  • Node.js

预备知识

  1. 熟悉 ES6 相关知识
  2. 了解 React 相关知识
  3. 会使用 create-react-app 脚手架创建一个 react 应用

MobX API

在开始搭建我们的第一个 MobX+React 应用前,首先需要大致地认识下 MobX 的 API ,了解 MobX 的核心概念,明白 MobX 的工作流程以及 常见陷阱 。有了相关知识储备后再进行开发,往往能使我们编码更加得心应手,少走弯路,不用再劳心劳力和 bug 斗智斗勇。故还请初次接触 MobX 的读者仔细阅读 API 文档。

请务必熟悉以下标签的概念和作用

  • @observable / observable()
  • @observer / observer()
  • @action / action()
  • @computed
  • @inject

示例应用需求以及效果展示

示例应用需求

本示例应用需求是实现通过输入股票代码查询到相关股票信息并展示出来的功能。关于获取股票相关信息,则通过新浪财经的证券股票数据接口进行获取。由于该接口并未实现 cors 跨域资源共享标准 ,会存在跨域访问的问题,所以我们在 自己编写的后端项目中 获取该接口返回的数据并实现cors跨域资源共享标准后传递给前端示例应用。(这只是一种跨域问题的解决方案,如果读者有其他跨域问题的解决方案请自行修改实现。)
本示例应用为了简单起见,并未添加相关css样式文件,如读者有兴趣,可自行添加。

效果展示

启动应用后界面如下(就是辣么粗犷……)


启动应用界面

点击查询按钮后如下所示(依旧辣么粗犷甚至有点不羁……)


点击查询后页面

第一步:使用 create-react-app 脚手架创建一个React应用

MobX采用的是ES7的装饰器语法,目前还是一种实验性的语法,使用 create-react-app 脚手架默认创建的项目是没有开启装饰器语法的,故使用 custom-react-scripts 这种方式来创建项目。
命令行内输入 npx create-react-app my-app --scripts-version custom-react-scripts 创建项目。
其创建的项目根目录路径下有一个拓展名为 .env 的文件,这个文件中定义了 custom-react-scripts 为项目新增的特性。
打开该文件可以看到 REACT_APP_DECORATORS = true; 表示启用了装饰器语法。

第二步:安装相关依赖

查看项目目录下的 package.json 文件,此时仅安装了 reactreact-dom依赖。
我们需要手动安装mobxmobx-react依赖,以及 MobX 开发调试工具。
在终端命令行切入到我们的项目目录:

  • cd my-app
    在终端命令行输入以下命令进行安装:
  • npm install mobx
  • npm install mobx-react
  • npm install mobx-react-devtools --save-dev

安装完相关依赖后我们就可以正式进入第一个入门实例项目的编写了。

第三步:构造项目目录

我们可以构造如下所示的项目目录:

根目录
|--src #开发文件目录
  | |---components # react 组件目录
  | |   |--index.js # 组件文件
  | |--models # 领域模型目录
  | |   |--StockModel.js # 领域state文件
  | |--stores # 保存state的Store目录
  | |   |--index.js # 根Store目录
  | |   |--StockStore.js # 领域Store目录
  | |--index.js

MobX 中的 state 一般会封装在不同的 store 中,store 不仅保存了 state ,还保存了操作 state 的方法。对于与领域直接相关的 state ,一般会创建专门的 model 实体类,用于描述 state 。

第四步:设计 store 和 state

store 的职责是将组件使用的业务逻辑和状态封装到单独的模块,这样组件就可以专注于UI渲染。

首先设计我们的model实体类StockModel,用于描述股票信息的state。
股票信息接口返回的是是一个字符串,我们决定在领域store中把它的数据解析出来并保存在数组里,所以在实体类中我们决定使用了一个fromArray方法来创建我们的StockModel实体。

//领域state
import {observable} from "mobx";

class StockModel {
  store;//领域state所属的领域store
  @observable code;//股票代码
  @observable stockName;//股票名称
  @observable tPrice;//今日开盘价
  @observable yPrice;//昨日收盘价
  @observable nPrice;//今日当前价格
  @observable hPrice;//今日最高价
  @observable lPrice;//今日最低价

  constructor(store,code,stockName,tPrice,yPrice,nPrice,hPrice,lPrice){
    this.store = store;
    this.code = code;
    this.stockName = stockName;
    this.tPrice = tPrice;
    this.yPrice = yPrice;
    this.nPrice = nPrice;
    this.hPrice = hPrice;
    this.lPrice = lPrice;
  }

  static fromArray(store,code,arr){
    return new StockModel(
      store,
      code,
      arr[0],
      arr[1],
      arr[2],
      arr[3],
      arr[4],
      arr[5],
      arr[6]);
  }
}

export default StockModel;

设计完了具有相关领域 state 的实体类,我们需要创建一个保存state和相关操作 state 的领域 Store 。
在该领域Store内我们定义了一个 state [stocks] 用以保存将要从服务获取到的股票信息实体。我们还定义了一个动作 [fetchStockByCode] 用于从后端获取股票信息。需要特别注意的是,fetch是一个异步操作,所以需要编写 异步action 来进行对state的操作。这里我们采用action关键字来包装promises回调函数。即在获取到数据后再发送一个action操作 state [stocks] 的变更。

//领域state
import { observable, action,} from "mobx";
import StockModel from "../models/StockModel";

class StockStore{

  @observable stocks=[]; //数组元素是PostModel的实例

  //从服务器获取股票信息
  @action fetchStockByCode(code){

    //跨域访问
    const headers = new Headers({
      "Access-Control-Allow-Origin":"*"
    });
    
    return fetch('http://127.0.0.1:8080/myapp/api/getStockInfo?code='+code,{method:"GET",headers:headers,mode:"cors"})
      .then(function (response){
        return response.text();
      })
      .then(
      action(
        data =>{ 
          const info = data.match(/".+"/)[0];
          const target = info.replace(/"/g,"");
          const item = target.split(","); //目标信息数组
          this.stocks.clear();
          this.stocks.push(StockModel.fromArray(this,code,item));
          return Promise.resolve();
        }
      )
    )
  }
}

export default StockStore;

每一个应用中不能初始化多个相同的领域 Store ,除非你想使得你的应用中的state变得相当混乱。
我们可以创建一个根 Store,来管理和初始化我们的各个领域 Store 或其他的 Store 比如应用状态 Store 、UIStore 等。(为了使我们这个示例应用更加简洁明了,故我们只有一个领域Store,即StockStore)。

//根Store
import StockStore from "./StockStore";
const stockStore = new StockStore();

const stores = {stockStore,};

export default stores;

第五步:绘制视图层

store 和 state 设计好了自然要开始设计我们的展示的视图层了。

在视图层,首先要明晰我们的交互逻辑,输入股票代码,触发拉取股票信息的动作,获取到股票信息后触发更新StockStore中保存的 state [ stocks] 的状态,从而自动触发 Computed value 对 state 变更的响应 获取到最新的股票信息数据,接着再自动触发 Reactions 对 state 变更的响应 [ 即组件内render()方法 ] 使得UI重新渲染。

为了使得渲染更有效率,我们最好尽量地使用小组件。

此时也要特别注意一些使用MobX的陷阱,比如从 observable 属性中提取数据并存储,这样的数据是不会被追踪的。

所有使用到@observable的组件都要加上@observer。别担心,@observer 越多,渲染效率越高。

@inject将组件需要用到的具体store从根store中注入进来,具体理解需要结合下一步查看。

inject 是一个高阶组件( 注意:高阶组件不是React组件而是个函数 ),它和 Provider 结合使用,用于从 Provider 提供的 state 中选取所需数据,作为 props 传递给目标组件。

import React,{ Component } from 'react';
import { observable, action, computed } from "mobx";
import { inject, observer } from "mobx-react";

@inject("stockStore")
@observer
class StockPage extends Component{

  render(){
      if(this.props.stockStore.stocks.length ===0 ){
        return (
          <StockInput/>
        );
      }
      return(
        <div>
          <StockInput/>
          <StockInfoView  />
        </div>
      );
  }
}

@inject("stockStore")
@observer class StockInput extends Component{

  @observable input="";

  render(){
    return(
      <div>
        <input value={this.input} onChange={this.onChange}/>
        <button onClick={this.onSubmit}>查询</button>
      </div>
    );
  }
  @action onChange=(e)=>{
    this.input = e.target.value;
  };

  @action onSubmit = () =>{
      this.props.stockStore.fetchStockByCode(this.input);
  }
}

@inject("stockStore")
@observer class StockInfoView extends Component{

  //常见陷阱——常见的错误的是从 observable 属性中提取数据并存储,这样的数据是不会被追踪的
  //不要拷贝observables 属性并存储在本地
  //Observer 组件只会追踪在 render 方法中存取的数据。
  @computed get stockModel(){
    return this.props.stockStore.stocks[0];
  }

  render(){
    const {code,stockName,tPrice,nPrice,yPrice,hPrice,lPrice} = this.stockModel;
    return(
      <ul>
        <li>股票代码:{code}</li>
        <li>股票名称:{stockName}</li>
        <li>今日开盘价:{tPrice}</li>
        <li>昨日收盘价:{yPrice}</li>
        <li>当前价格:{nPrice}</li>
        <li>今日最高价:{hPrice}</li>
        <li>今日最低价:{lPrice}</li>
      </ul>
    );
  }
}

export  default StockPage;

第六步:连接 Store 和视图层并加入 mobx-react-devtools

React开发的视图层和 MobX开发的Store 现在都已开发完毕。
视图层只负责 UI 的展示,Store 也会集中管理 state 。
现在我们需要将其连接起来使得视图层中能获取到 Store 保存的 state 值,以及视图层能触发 Store 中定义的操作 state 的 action 。
通过使用mobx-react中提供的 Provider 组件来在React中使用MobX。

Provider 是一个 React 组件,利用 React 的 context 机制把应用所需要的 state 传递给子组件。
它的作用与 react-redux 提供的 Provider 组件是相同的。

import { Provider } from "mobx-react";
import React from "react";
import ReactDOM from 'react-dom';
import DevTools from  'mobx-react-devtools';
import StockPage from "./components";
import stores from "./stores";

const App = ()=>(
  <div>
    <StockPage />
    <DevTools/>
  </div>
);

ReactDOM.render(
  <Provider {...stores}>
    <App />
  </Provider>,
  document.getElementById("root"));

第七步:运行我们的第一个示例应用

进入my-app的目录:
在终端命令行输入:cd my-app
运行我们的应用:
在终端命令行输入:npm start

等待服务启动完毕后,在浏览器地址栏输入localhost:3000/ 就可以看到我们的应用啦!
现在我们试试输入柳钢股份的股票代码601003,点击查询按钮就可以看到柳钢股份的相关股票信息啦!( 没错!这可能是篇软广…… )

写在最后

相关前端代码和后端代码近期将会上传至 github 以供大家参考运行,还请大家耐心等候。

这仅仅是一个简单的 MobX+React 简单示例应用,如果想了解更多 MobX 的高级用法,请参阅 MobX API

如有任何疑问,敬请留言或者私信。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容