手把手從零開(kāi)始搭建第一個(gè)MobX+React入門(mén)示例應(yīng)用

  • 文中的藍(lán)色字體是相關(guān)內(nèi)容的超鏈接,網(wǎng)址不另外列出,請(qǐng)放心點(diǎn)擊。
  • 本文內(nèi)容適合 MobX 和 React 新手,也歡迎 MobX 和 React 專(zhuān)家指導(dǎo)點(diǎn)評(píng)。

摘要

閱讀本文并實(shí)際上手編碼運(yùn)行,你將解決如下幾個(gè)疑問(wèn):

  • MobX如何進(jìn)行狀態(tài)管理
  • MobX如何管理異步操作對(duì)狀態(tài)的改變(fetch的使用)
  • 如何創(chuàng)建一個(gè)簡(jiǎn)單的MobX+React 示例應(yīng)用(無(wú)路由)

工具

  • JetBrains WebStorm
  • Node.js

預(yù)備知識(shí)

  1. 熟悉 ES6 相關(guān)知識(shí)
  2. 了解 React 相關(guān)知識(shí)
  3. 會(huì)使用 create-react-app 腳手架創(chuàng)建一個(gè) react 應(yīng)用

MobX API

在開(kāi)始搭建我們的第一個(gè) MobX+React 應(yīng)用前,首先需要大致地認(rèn)識(shí)下 MobX 的 API ,了解 MobX 的核心概念,明白 MobX 的工作流程以及 常見(jiàn)陷阱 。有了相關(guān)知識(shí)儲(chǔ)備后再進(jìn)行開(kāi)發(fā),往往能使我們編碼更加得心應(yīng)手,少走彎路,不用再勞心勞力和 bug 斗智斗勇。故還請(qǐng)初次接觸 MobX 的讀者仔細(xì)閱讀 API 文檔。

請(qǐng)務(wù)必熟悉以下標(biāo)簽的概念和作用

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

示例應(yīng)用需求以及效果展示

示例應(yīng)用需求

本示例應(yīng)用需求是實(shí)現(xiàn)通過(guò)輸入股票代碼查詢(xún)到相關(guān)股票信息并展示出來(lái)的功能。關(guān)于獲取股票相關(guān)信息,則通過(guò)新浪財(cái)經(jīng)的證券股票數(shù)據(jù)接口進(jìn)行獲取。由于該接口并未實(shí)現(xiàn) cors 跨域資源共享標(biāo)準(zhǔn) ,會(huì)存在跨域訪(fǎng)問(wèn)的問(wèn)題,所以我們?cè)?自己編寫(xiě)的后端項(xiàng)目中 獲取該接口返回的數(shù)據(jù)并實(shí)現(xiàn)cors跨域資源共享標(biāo)準(zhǔn)后傳遞給前端示例應(yīng)用。(這只是一種跨域問(wèn)題的解決方案,如果讀者有其他跨域問(wèn)題的解決方案請(qǐng)自行修改實(shí)現(xiàn)。)
本示例應(yīng)用為了簡(jiǎn)單起見(jiàn),并未添加相關(guān)css樣式文件,如讀者有興趣,可自行添加。

效果展示

啟動(dòng)應(yīng)用后界面如下(就是辣么粗獷……)


啟動(dòng)應(yīng)用界面

點(diǎn)擊查詢(xún)按鈕后如下所示(依舊辣么粗獷甚至有點(diǎn)不羈……)


點(diǎn)擊查詢(xún)后頁(yè)面

第一步:使用 create-react-app 腳手架創(chuàng)建一個(gè)React應(yīng)用

MobX采用的是ES7的裝飾器語(yǔ)法,目前還是一種實(shí)驗(yàn)性的語(yǔ)法,使用 create-react-app 腳手架默認(rèn)創(chuàng)建的項(xiàng)目是沒(méi)有開(kāi)啟裝飾器語(yǔ)法的,故使用 custom-react-scripts 這種方式來(lái)創(chuàng)建項(xiàng)目。
命令行內(nèi)輸入 npx create-react-app my-app --scripts-version custom-react-scripts 創(chuàng)建項(xiàng)目。
其創(chuàng)建的項(xiàng)目根目錄路徑下有一個(gè)拓展名為 .env 的文件,這個(gè)文件中定義了 custom-react-scripts 為項(xiàng)目新增的特性。
打開(kāi)該文件可以看到 REACT_APP_DECORATORS = true; 表示啟用了裝飾器語(yǔ)法。

第二步:安裝相關(guān)依賴(lài)

查看項(xiàng)目目錄下的 package.json 文件,此時(shí)僅安裝了 reactreact-dom依賴(lài)。
我們需要手動(dòng)安裝mobxmobx-react依賴(lài),以及 MobX 開(kāi)發(fā)調(diào)試工具。
在終端命令行切入到我們的項(xiàng)目目錄:

  • cd my-app
    在終端命令行輸入以下命令進(jìn)行安裝:
  • npm install mobx
  • npm install mobx-react
  • npm install mobx-react-devtools --save-dev

安裝完相關(guān)依賴(lài)后我們就可以正式進(jìn)入第一個(gè)入門(mén)實(shí)例項(xiàng)目的編寫(xiě)了。

第三步:構(gòu)造項(xiàng)目目錄

我們可以構(gòu)造如下所示的項(xiàng)目目錄:

根目錄
|--src #開(kāi)發(fā)文件目錄
? | |---components # react 組件目錄
? | | ??|--index.js # 組件文件
? | |--models # 領(lǐng)域模型目錄
? | | ??|--StockModel.js # 領(lǐng)域state文件
? | |--stores # 保存state的Store目錄
? | | ??|--index.js # 根Store目錄
? | | ??|--StockStore.js # 領(lǐng)域Store目錄
? | |--index.js

MobX 中的 state 一般會(huì)封裝在不同的 store 中,store 不僅保存了 state ,還保存了操作 state 的方法。對(duì)于與領(lǐng)域直接相關(guān)的 state ,一般會(huì)創(chuàng)建專(zhuān)門(mén)的 model 實(shí)體類(lèi),用于描述 state 。

第四步:設(shè)計(jì) store 和 state

store 的職責(zé)是將組件使用的業(yè)務(wù)邏輯和狀態(tài)封裝到單獨(dú)的模塊,這樣組件就可以專(zhuān)注于UI渲染。

首先設(shè)計(jì)我們的model實(shí)體類(lèi)StockModel,用于描述股票信息的state。
股票信息接口返回的是是一個(gè)字符串,我們決定在領(lǐng)域store中把它的數(shù)據(jù)解析出來(lái)并保存在數(shù)組里,所以在實(shí)體類(lèi)中我們決定使用了一個(gè)fromArray方法來(lái)創(chuàng)建我們的StockModel實(shí)體。

//領(lǐng)域state
import {observable} from "mobx";

class StockModel {
  store;//領(lǐng)域state所屬的領(lǐng)域store
  @observable code;//股票代碼
  @observable stockName;//股票名稱(chēng)
  @observable tPrice;//今日開(kāi)盤(pán)價(jià)
  @observable yPrice;//昨日收盤(pán)價(jià)
  @observable nPrice;//今日當(dāng)前價(jià)格
  @observable hPrice;//今日最高價(jià)
  @observable lPrice;//今日最低價(jià)

  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;

設(shè)計(jì)完了具有相關(guān)領(lǐng)域 state 的實(shí)體類(lèi),我們需要?jiǎng)?chuàng)建一個(gè)保存state和相關(guān)操作 state 的領(lǐng)域 Store 。
在該領(lǐng)域Store內(nèi)我們定義了一個(gè) state [stocks] 用以保存將要從服務(wù)獲取到的股票信息實(shí)體。我們還定義了一個(gè)動(dòng)作 [fetchStockByCode] 用于從后端獲取股票信息。需要特別注意的是,fetch是一個(gè)異步操作,所以需要編寫(xiě) 異步action 來(lái)進(jìn)行對(duì)state的操作。這里我們采用action關(guān)鍵字來(lái)包裝promises回調(diào)函數(shù)。即在獲取到數(shù)據(jù)后再發(fā)送一個(gè)action操作 state [stocks] 的變更。

//領(lǐng)域state
import { observable, action,} from "mobx";
import StockModel from "../models/StockModel";

class StockStore{

  @observable stocks=[]; //數(shù)組元素是PostModel的實(shí)例

  //從服務(wù)器獲取股票信息
  @action fetchStockByCode(code){

    //跨域訪(fǎng)問(wèn)
    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(","); //目標(biāo)信息數(shù)組
          this.stocks.clear();
          this.stocks.push(StockModel.fromArray(this,code,item));
          return Promise.resolve();
        }
      )
    )
  }
}

export default StockStore;

每一個(gè)應(yīng)用中不能初始化多個(gè)相同的領(lǐng)域 Store ,除非你想使得你的應(yīng)用中的state變得相當(dāng)混亂。
我們可以創(chuàng)建一個(gè)根 Store,來(lái)管理和初始化我們的各個(gè)領(lǐng)域 Store 或其他的 Store 比如應(yīng)用狀態(tài) Store 、UIStore 等。(為了使我們這個(gè)示例應(yīng)用更加簡(jiǎn)潔明了,故我們只有一個(gè)領(lǐng)域Store,即StockStore)。

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

const stores = {stockStore,};

export default stores;

第五步:繪制視圖層

store 和 state 設(shè)計(jì)好了自然要開(kāi)始設(shè)計(jì)我們的展示的視圖層了。

在視圖層,首先要明晰我們的交互邏輯,輸入股票代碼,觸發(fā)拉取股票信息的動(dòng)作,獲取到股票信息后觸發(fā)更新StockStore中保存的 state [ stocks] 的狀態(tài),從而自動(dòng)觸發(fā) Computed value 對(duì) state 變更的響應(yīng) 獲取到最新的股票信息數(shù)據(jù),接著再自動(dòng)觸發(fā) Reactions 對(duì) state 變更的響應(yīng) [ 即組件內(nèi)render()方法 ] 使得UI重新渲染。

為了使得渲染更有效率,我們最好盡量地使用小組件。

此時(shí)也要特別注意一些使用MobX的陷阱,比如從 observable 屬性中提取數(shù)據(jù)并存儲(chǔ),這樣的數(shù)據(jù)是不會(huì)被追蹤的。

所有使用到@observable的組件都要加上@observer。別擔(dān)心,@observer 越多,渲染效率越高。

@inject將組件需要用到的具體store從根store中注入進(jìn)來(lái),具體理解需要結(jié)合下一步查看。

inject 是一個(gè)高階組件( 注意:高階組件不是React組件而是個(gè)函數(shù) ),它和 Provider 結(jié)合使用,用于從 Provider 提供的 state 中選取所需數(shù)據(jù),作為 props 傳遞給目標(biāo)組件。

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}>查詢(xún)</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{

  //常見(jiàn)陷阱——常見(jiàn)的錯(cuò)誤的是從 observable 屬性中提取數(shù)據(jù)并存儲(chǔ),這樣的數(shù)據(jù)是不會(huì)被追蹤的
  //不要拷貝observables 屬性并存儲(chǔ)在本地
  //Observer 組件只會(huì)追蹤在 render 方法中存取的數(shù)據(jù)。
  @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>股票名稱(chēng):{stockName}</li>
        <li>今日開(kāi)盤(pán)價(jià):{tPrice}</li>
        <li>昨日收盤(pán)價(jià):{yPrice}</li>
        <li>當(dāng)前價(jià)格:{nPrice}</li>
        <li>今日最高價(jià):{hPrice}</li>
        <li>今日最低價(jià):{lPrice}</li>
      </ul>
    );
  }
}

export  default StockPage;

第六步:連接 Store 和視圖層并加入 mobx-react-devtools

React開(kāi)發(fā)的視圖層和 MobX開(kāi)發(fā)的Store 現(xiàn)在都已開(kāi)發(fā)完畢。
視圖層只負(fù)責(zé) UI 的展示,Store 也會(huì)集中管理 state 。
現(xiàn)在我們需要將其連接起來(lái)使得視圖層中能獲取到 Store 保存的 state 值,以及視圖層能觸發(fā) Store 中定義的操作 state 的 action 。
通過(guò)使用mobx-react中提供的 Provider 組件來(lái)在React中使用MobX。

Provider 是一個(gè) React 組件,利用 React 的 context 機(jī)制把應(yīng)用所需要的 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"));

第七步:運(yùn)行我們的第一個(gè)示例應(yīng)用

進(jìn)入my-app的目錄:
在終端命令行輸入:cd my-app
運(yùn)行我們的應(yīng)用:
在終端命令行輸入:npm start

等待服務(wù)啟動(dòng)完畢后,在瀏覽器地址欄輸入localhost:3000/ 就可以看到我們的應(yīng)用啦!
現(xiàn)在我們?cè)囋囕斎肓摴煞莸墓善贝a601003,點(diǎn)擊查詢(xún)按鈕就可以看到柳鋼股份的相關(guān)股票信息啦?。?沒(méi)錯(cuò)!這可能是篇軟廣…… )

寫(xiě)在最后

相關(guān)前端代碼和后端代碼近期將會(huì)上傳至 github 以供大家參考運(yùn)行,還請(qǐng)大家耐心等候。

這僅僅是一個(gè)簡(jiǎn)單的 MobX+React 簡(jiǎn)單示例應(yīng)用,如果想了解更多 MobX 的高級(jí)用法,請(qǐng)參閱 MobX API

如有任何疑問(wèn),敬請(qǐng)留言或者私信。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 項(xiàng)目地址 從頭開(kāi)始建立一個(gè)React App - 項(xiàng)目基本配置 npm init 生成 package.json ...
    瘦人假?lài)?/span>閱讀 89,749評(píng)論 33 78
  • 本文首發(fā)于:CSDN「前端開(kāi)發(fā)者說(shuō)」公眾號(hào)。CSDN「前端開(kāi)發(fā)者說(shuō)」公眾號(hào)(ID:bigfrontend),專(zhuān)注前...
    RachelQG閱讀 4,896評(píng)論 2 20
  • 在能力與理想相匹配之前, 一切的舒適都是絆腳石。 你創(chuàng)造的價(jià)值,歲月會(huì)回報(bào)給你; 你虧欠的努力,歲月也會(huì)拿回去。 ...
    修行無(wú)為閱讀 152評(píng)論 0 0
  • 財(cái)富自由是蕓蕓眾生的終極追求吧,我們把它當(dāng)作目標(biāo),為之努力,然而靜蘭通過(guò)李笑來(lái)的書(shū)《財(cái)富自由之路》,落字有力,語(yǔ)不...
    索亞之聲閱讀 430評(píng)論 0 2
  • 一直想寫(xiě)些文字 但 詞窮的我 總也書(shū)不起 請(qǐng)?jiān)彑o(wú)知的孩兒 把您忘記 偶爾在深夜無(wú)聲的夢(mèng)里 偷偷哭泣 但您依舊如昔...
    壟上行云閱讀 169評(píng)論 0 3

友情鏈接更多精彩內(nèi)容