dva理論到實踐——幫你掃清dva的知識盲點

dva數(shù)據(jù)流圖

筆者升級了 dva 的版本冀偶,同時新增了 umi 的使用捌议,具體可以參考這篇文章 dva理論到實踐——幫你掃清dva的知識盲點

本文中我會介紹一下相應的dva的相應知識點和實戰(zhàn)練習拣凹。

同時我也會介紹使用dva的流程狼渊,以及介紹使用dva中的坑测暗。希望大家通過這篇文章宠漩,能大致了解dva的使用流程柜蜈。

一仗谆,Dva簡介

1,借鑒 elm 的概念淑履,Reducer, Effect 和 Subscription

2隶垮,框架,而非類庫

3秘噪,基于 redux, react-router, redux-saga 的輕量級封裝


二狸吞,Dva的特性

1,僅有 5 個 API,僅有5個主要的api蹋偏,其用法我們會在第三節(jié)詳細介紹便斥。

2,支持 HMR威始,支持模塊的熱更新椭住。

3,支持 SSR (ServerSideRender)字逗,支持服務器端渲染京郑。

4,支持 Mobile/ReactNative葫掉,支持移動手機端的代碼編寫些举。

5,支持 TypeScript俭厚,支持TypeScript户魏,個人感覺這個會是javascript的一個趨勢。

6挪挤,支持路由和 Model 的動態(tài)加載叼丑。

7,…...


三扛门,Dva的5個API

dva_api.png

1鸠信,app = dva(Opts):創(chuàng)建應用,返回 dva 實例论寨。(注:dva 支持多實例)?

opts可以配置所有的hooks ?

const app = dva({
     history,
     initialState,
     onError,
     onAction,
     onStateChange,
     onReducer,
     onEffect,
     onHmr,
     extraReducers,
     extraEnhancers,
});

這里比較常用的是星立,history的配置,一般默認的是hashHistory葬凳,如果要配置 history 為 browserHistory绰垂,可以這樣:

import createHistory from 'history/createBrowserHistory';
const app = dva({
  history: createHistory(),
});
  • 關(guān)于react-router中的hashHistorybrowserHistory的區(qū)別大家可以看:react-router
  • initialState:指定初始數(shù)據(jù)火焰,優(yōu)先級高于 model 中的 state劲装,默認是 {},但是基本上都在modal里面設(shè)置相應的state昌简。

2占业,app.use(Hooks):配置 hooks 或者注冊插件。

這里最常見的就是dva-loading插件的配置江场,

import createLoading from 'dva-loading';
...
app.use(createLoading(opts));

?
但是一般對于全局的loading我們會根據(jù)業(yè)務的不同來顯示相應不同的loading圖標纺酸,我們可以根據(jù)自己的需要來選擇注冊相應的插件窖逗。
?

3址否,app.model(ModelObject):這個是你數(shù)據(jù)邏輯處理散休,數(shù)據(jù)流動的地方。

dva_modal.png

?
modaldva里面與我們真正進行項目開發(fā)晾蜘,邏輯處理扶欣,數(shù)據(jù)流動的地方。這里面涉及到的namespace音同、Modal词爬、effectsreducer等概念都很重要,我們會在第四部分詳細講解权均。
?

4顿膨,app.router(Function):注冊路由表,我們做路由跳轉(zhuǎn)的地方叽赊。

一般都是這么寫的

import { Router, Route } from 'dva/router';

app.router(({ history }) => {
  return (
    <Router history={history}>
      <Route path="/" component={App} />
    <Router>
  );
});

但是如果你的項目特別的龐大恋沃,我們就要考慮到相應的性能的問題,但是入門可以先看一下這個必指。對于如何做到按需加載大家可以看10分鐘 讓你dva從入門到精通囊咏,里面有簡單提到router按需加載的寫法。

5塔橡,app.start([HTMLElement], opts)

啟動應用梅割,即將我們的應用跑起來。


四葛家,Dva九個概念

1户辞,State(狀態(tài))

? 初始值,我們在 dva() 初始化的時候和在 modal 里面的 state 對其兩處進行定義癞谒,其中 modal 中的優(yōu)先級低于傳給 dva()opts.initialState

如下:

// dva()初始化
const app = dva({
  initialState: { count: 1 },
});

// modal()定義事件
app.model({
  namespace: 'count',
  state: 0,
});

?

2咆课,Action:表示操作事件,可以是同步扯俱,也可以是異步

action 的格式如下书蚪,它需要有一個 type ,表示這個 action 要觸發(fā)什么操作迅栅;payload 則表示這個 action 將要傳遞的數(shù)據(jù)

{
  type: String,
  payload: data,
}

我們通過 dispatch 方法來發(fā)送一個 action

Action
Action 表示操作事件殊校,可以是同步,也可以是異步
 {
  type: String,
  payload: data
}
格式
dispatch(Action);
dispatch({ type: 'todos/add', payload: 'Learn Dva' });

?
其實我們可以構(gòu)建一個Action 創(chuàng)建函數(shù)读存,如下

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

//我們直接dispatch(addTodo()),就發(fā)送了一個action为流。
dispatch(addTodo())

具體可以查看文檔:redux——action
?

3,Model

modeldva 中最重要的概念让簿,ModelMVC 中的 M敬察,而是領(lǐng)域模型,用于把數(shù)據(jù)相關(guān)的邏輯聚合到一起尔当,幾乎所有的數(shù)據(jù)莲祸,邏輯都在這邊進行處理分發(fā)

  • state

    這里的 state 跟我們剛剛講的 state 的概念是一樣的,只不過她的優(yōu)先級比初始化的低,但是基本上項目中的 state 都是在這里定義的锐帜。

  • namespace

    model 的命名空間田盈,同時也是他在全局 state 上的屬性,只能用字符串缴阎,我們發(fā)送在發(fā)送 action 到相應的 reducer 時允瞧,就會需要用到 namespace

  • Reducer

    key/value 格式定義 reducer蛮拔,用于處理同步操作述暂,唯一可以修改 state 的地方。由 action 觸發(fā)建炫。其實一個純函數(shù)贸典。

  • Effect

    用于處理異步操作和業(yè)務邏輯,不直接修改 state踱卵,簡單的來說廊驼,就是獲取從服務端獲取數(shù)據(jù),并且發(fā)起一個 action 交給 reducer 的地方惋砂。

    其中它用到了redux-saga妒挎,里面有幾個常用的函數(shù)。

    *add(action, { call, put }) {
        yield call(delay, 1000);
        yield put({ type: 'minus' });
    },
    
effect_api圖

在項目中最主要的會用到的是 putcall西饵。

  • Subscription

    subscription 是訂閱酝掩,用于訂閱一個數(shù)據(jù)源,然后根據(jù)需要 dispatch 相應的 action眷柔。在 app.start() 時被執(zhí)行期虾,數(shù)據(jù)源可以是當前的時間、當前頁面的url驯嘱、服務器的 websocket 連接镶苞、history 路由變化等等。

4鞠评,Router

? Router 表示路由配置信息茂蚓,項目中的 router.js

export default function({ history }){
  return(
    <Router history={history}>
      <Route path="/" component={App} />
    </Router>
  );
}
  • RouteComponent

    ? RouteComponent 表示 Router 里匹配路徑的 Component剃幌,通常會綁定 model 的數(shù)據(jù)聋涨。如下:

import { connect } from 'dva';

function App() {
  return <div>App</div>;
}

function mapStateToProps(state) {
  return { todos: state.todos };
}

export default connect(mapStateToProps)(App);

?


五,整體架構(gòu)

dva數(shù)據(jù)流圖

我簡單的分析一下這個圖:

首先我們根據(jù) url 訪問相關(guān)的 Route-Component负乡,在組件中我們通過 dispatch 發(fā)送 actionmodel 里面的 effect 或者直接 Reducer

當我們將action發(fā)送給Effect牍白,基本上是取服務器上面請求數(shù)據(jù)的,服務器返回數(shù)據(jù)之后抖棘,effect 會發(fā)送相應的 actionreducer茂腥,由唯一能改變 statereducer 改變 state 狸涌,然后通過connect重新渲染組件。

當我們將action發(fā)送給reducer础芍,那直接由 reducer 改變 state杈抢,然后通過 connect 重新渲染組件数尿。

這樣我們就能走完一個流程了仑性。


六,項目案例

這一節(jié)我們會根據(jù)dva的快速搭建一個計數(shù)器右蹦。官方的例子是都把所有的邏輯寫在了入口文件HomePage.js里诊杆,我會在下面的demo中,把例子中的各個模塊抽出來何陆,放在相應的文件夾中晨汹。讓大家能更加清楚每一個模塊的作用。
dva_demo.gif

?
1贷盲,首先全局安裝dva-cli淘这,我的操作在桌面進行的,大家可以自行選擇項目目錄巩剖。

$ npm install -g dva-cli

?
2铝穷,接著使用dva-cli創(chuàng)建我們的項目文件夾

$ dva new myapp

?
3,進入myapp目錄佳魔,安裝依賴曙聂,執(zhí)行如下操作。

$ cd myapp
$ npm start

?
瀏覽器會自動打開一個窗口鞠鲜,如下圖宁脊。

dva_cli.png

?

4,目錄結(jié)構(gòu)介紹

.
├── mock    // mock數(shù)據(jù)文件夾
├── node_modules // 第三方的依賴
├── public  // 存放公共public文件的文件夾
├── src  // 最重要的文件夾贤姆,編寫代碼都在這個文件夾下
│   ├── assets // 可以放圖片等公共資源
│   ├── components // 就是react中的木偶組件
│   ├── models // dva最重要的文件夾榆苞,所有的數(shù)據(jù)交互及邏輯都寫在這里
│   ├── routes // 就是react中的智能組件,不要被文件夾名字誤導霞捡。
│   ├── services // 放請求借口方法的文件夾
│   ├── utils // 自己的工具方法可以放在這邊
│   ├── index.css // 入口文件樣式
│   ├── index.ejs // ejs模板引擎
│   ├── index.js // 入口文件
│   └── router.js // 項目的路由文件
├── .eslintrc // bower安裝目錄的配置
├── .editorconfig // 保證代碼在不同編輯器可視化的工具
├── .gitignore // git上傳時忽略的文件
├── .roadhogrc.js // 項目的配置文件语稠,配置接口轉(zhuǎn)發(fā),css_module等都在這邊弄砍。
├── .roadhogrc.mock.js // 項目的配置文件
└── package.json // 當前整一個項目的依賴

?
5仙畦,首先是前端的頁面,我們使用 class 形式來創(chuàng)建組件音婶,原例子中是使用無狀態(tài)來創(chuàng)建的慨畸。react 創(chuàng)建組件的各種方式,大家可以看React創(chuàng)建組件的三種方式及其區(qū)別

?
我們先修改route/IndexPage.js

import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';

class IndexPage extends React.Component {
  render() {
    const { dispatch } = this.props;

    return (
      <div className={styles.normal}>
        <div className={styles.record}>Highest Record: 1</div>
        <div className={styles.current}>2</div>
        <div className={styles.button}>
          <button onClick={() => {}}>+</button>
        </div>
      </div>
    );
  }
}

export default connect()(IndexPage);

?
同時修改樣式routes/IndexPage.css

.normal {
  width: 200px;
  margin: 100px auto;
  padding: 20px;
  border: 1px solid #ccc;
  box-shadow: 0 0 20px #ccc;
}
.record {
  border-bottom: 1px solid #ccc;
  padding-bottom: 8px;
  color: #ccc;
}
.current {
  text-align: center;
  font-size: 40px;
  padding: 40px 0;
}
.button {
  text-align: center;
  button {
    width: 100px;
    height: 40px;
    background: #aaa;
    color: #fff;
  }
}

?
此時你的頁面應該是如下圖所示

dva_2.png

?

6衣式,在 model 處理 state 寸士,在頁面里面輸出 model 中的 state

?
首先我們在index.js中將models/example.js檐什,即將model下一行的的注釋打開。

import dva from 'dva';
import './index.css';

// 1. Initialize
const app = dva();

// 2. Plugins
// app.use({});

// 3. Model
app.model(require('./models/example')); // 打開注釋

// 4. Router
app.router(require('./router'));

// 5. Start
app.start('#root');

?
接下來我們進入 models/example.js弱卡,將namespace 名字改為 count乃正,state 對象加上 recordcurrent 屬性。如下:

export default {
  namespace: 'count',
  state: {
    record: 0,
    current: 0,
  },
  ...
};

?

接著我們來到 routes/indexpage.js 頁面婶博,通過的 mapStateToProps 引入相關(guān)的 state瓮具。

import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';

class IndexPage extends React.Component {
  render() {
    const { dispatch, count } = this.props;
    
    return (
      <div className={styles.normal}>
        <div className={styles.record}>
         Highest Record: {count.record} // 將count的record輸出
        </div>
        <div className={styles.current}>
         {count.current}
        </div>
        <div className={styles.button}>
          <button onClick={() => {} } >
                 +
          </button>
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return { count: state };
} // 獲取state

export default connect(mapStateToProps)(IndexPage);

?
打開網(wǎng)頁:你應該能看到下圖:

dva_4.png

?
7,通過 + 發(fā)送 action凡人,通過 reducer 改變相應的 state

?
首先我們在 models/example.js名党,寫相應的 reducer

export default {
  ...
  reducers: {
    add1(state) {
      const newCurrent = state.current + 1;
      return { ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1 };
    },
  },
};

?
在頁面的模板 routes/IndexPage.js+ 號點擊的時候挠轴,dispatch 一個 action

import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';

class IndexPage extends React.Component {
  render() {
    const { dispatch, count } = this.props;
    return (
      <div className={styles.normal}>
        <div className={styles.record}>Highest Record: {count.record}</div>
        <div className={styles.current}>{count.current}</div>
        <div className={styles.button}>
          <button 
+            onClick={() => { dispatch({ type: 'count/add1' });}
          }>+</button>
        </div>
      </div>
    );
  }
}
function mapStateToProps(state) {
  return { count: state.count };
}

export default connect(mapStateToProps)(IndexPage);

?
效果如下圖:

dva_add1.gif

?

8传睹,接下來我們來使用 effect 模擬一個數(shù)據(jù)接口請求,返回之后岸晦,通過 yield put() 改變相應的 state
?
首先我們替換相應的 models/example.jseffect

effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
},

?
這里的 delay欧啤,是我這邊寫的一個延時的函數(shù),我們在 utils 里面編寫一個 utils.js 启上,一般請求接口的函數(shù)都會寫在 servers 文件夾中邢隧。

export function delay(timeout) {
  return new Promise((resolve) => {
    setTimeout(resolve, timeout);
  });
}

?
接著我們在 models/example.js 導入這個 utils.js

import { delay } from '../utils/utils';

?
9,訂閱訂閱鍵盤事件碧绞,使用 subscriptions府框,當用戶按住 command+up 時候觸發(fā)添加數(shù)字的 action

models/example.js 中作如下修改

+import key from 'keymaster';
...
app.model({
  namespace: 'count',
+ subscriptions: {
+   keyboardWatcher({ dispatch }) {
+     key('?+up, ctrl+up', () => { dispatch({type:'add'}) });
+   },
+ },
});

在這里你需要安裝 keymaster 這個依賴

npm install keymaster --save

現(xiàn)在你可以按住 command+up 就可以使 current 加1了。
?
10讥邻,例子中我們看到當我們不斷點擊+按鈕之后迫靖,我們會看到current會不斷加一,但是1s過后兴使,他會自動減到零系宜。

官方的demo的代買沒有實現(xiàn)gif圖里面的效果,大家看下圖:

effect.png

?

要做到gif里面的效果发魄,我們應該在effect中發(fā)送一個關(guān)于添加的action盹牧,但是我們在effect中不能直接這么寫:

effects: {
    *add(action, { call, put }) {
      yield put({ type: 'add' });
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },

?
因為如果這樣的話,effectreducers中的add方法重合了励幼,這里會陷入一個死循環(huán)汰寓,因為當組件發(fā)送一個dispatch的時候,model會首先去找effect里面的方法苹粟,當又找到add的時候有滑,就又會去請求effect里面的方法。

?
我們應該更改reducers里面的方法嵌削,使它不與effect的方法一樣毛好,將reducers中的add改為add1望艺,如下:

reducers: {
    add1(state) {
      const newCurrent = state.current + 1;
      return { ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1};
    },
  },
  effects: {
    *add(action, { call, put }) {
      yield put({ type: 'add1' });
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },

?
這樣我們就實現(xiàn)了gif圖中的效果:

dva-test.gif

?
至此我們的簡單的demo就結(jié)束了,通過這個例子大家可以基本上了解dva的基本概念肌访。

如果還想深入了解dva的各個文件夾中文件的特性找默,大家可以看快速上手dva的一個簡單demo,這里面會很詳細的講到我們該怎么寫 model吼驶、怎么使用effect請求接口數(shù)據(jù)等等惩激。

?
這段時間我也利用業(yè)余時間,使用dva+thinkphp構(gòu)建一個類似boss直聘的手機端web應用旨剥,項目還沒全部做完咧欣,大家如果感興趣的話浅缸,可以下載下來看看轨帜,一起探討相關(guān)思路哦。

?


?

七衩椒,參考網(wǎng)址

  1. 初識 Dva
  2. Dva-React 應用框架在螞蟻金服的實踐
  3. Dva-api文檔
  4. 10分鐘 讓你dva從入門到精通
  5. roadhog介紹
  6. 基于dva-cli&antd的react項目實戰(zhàn)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚌父,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子毛萌,更是在濱河造成了極大的恐慌苟弛,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阁将,死亡現(xiàn)場離奇詭異膏秫,居然都是意外死亡,警方通過查閱死者的電腦和手機做盅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門缤削,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吹榴,你說我怎么就攤上這事亭敢。” “怎么了图筹?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵帅刀,是天一觀的道長。 經(jīng)常有香客問我远剩,道長扣溺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任瓜晤,我火速辦了婚禮锥余,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘活鹰。我一直安慰自己哈恰,他們只是感情好只估,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著着绷,像睡著了一般蛔钙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荠医,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天吁脱,我揣著相機與錄音,去河邊找鬼彬向。 笑死兼贡,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的娃胆。 我是一名探鬼主播遍希,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼里烦!你這毒婦竟也來了凿蒜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤胁黑,失蹤者是張志新(化名)和其女友劉穎废封,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丧蘸,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡漂洋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了力喷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刽漂。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖冗懦,靈堂內(nèi)的尸體忽然破棺而出爽冕,到底是詐尸還是另有隱情,我是刑警寧澤披蕉,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布颈畸,位于F島的核電站,受9級特大地震影響没讲,放射性物質(zhì)發(fā)生泄漏眯娱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一爬凑、第九天 我趴在偏房一處隱蔽的房頂上張望徙缴。 院中可真熱鬧,春花似錦、人聲如沸于样。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽穿剖。三九已至蚤蔓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間糊余,已是汗流浹背秀又。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贬芥,地道東北人吐辙。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像蘸劈,于是被迫代替她去往敵國和親昏苏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容