dva 中的響應編程

剛纔在看到《騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開!》裏的一句話:css

「計算機領域任何一個問題,均可以經過引入中間層來解決。」html

D.Va

最開始聽同事提及 dva ,我還覺得他守望先鋒玩魔怔了 —— 後來才知道他說的 dva 是螞蟻金服 antd 組件庫的御用輕量級框架。不過那個時候咱們公司用的仍是基於 redux-thunk 體系的腳手架,多少是和 dva 有點差異。因此很長一段時間內,我只是知道有這麼個東西而已。前端

最近開始用它作項目,上手有點彆扭。react

做爲一個兼職前端(好像如今也不算兼職了),個人知識體系仍是比較混亂,老是知其然而不知其因此然。雖然 Angular 和 React 代碼寫的也算很多了,寫來寫去總缺乏臨門一腳的頓悟,最後仍是一團漿糊。git

前幾天翻譯了一篇講 Android 函數式響應型編程的文章,忽然間就理解了 dva 編碼思想。再回頭審視用 dva 寫的代碼,發現那篇文章裏講解的不少理論和 dva 不謀而合,終於有種摸到大門的欣慰。es6

提及來也滿狗血的。github

思惟盲區

我最開始學習使用 dva 是從《12 步 30 分鐘,完成用戶管理的 CURD 應用》開始的,這同時也是 dva 的官方教程。然而由於領悟能力太差,最開始徹底沒理解。前 4 步還跟得上,第 5 步建立 model 和改造 service 就懵逼了。硬着頭皮照抄代碼,抄到最後數據沒出來,我還不知道本身哪兒錯了。chrome

大寫的尷尬。編程

如今再看這篇教程,發現從第 5 步的 model 開始,dva 的做者就試圖推廣一種最近流行的理念:響應型編程redux

就目前來講,渲染數據流有至少兩種方式。由外界改變組件內部狀態的主動型,以及由組件監聽外界改變而渲染自身的響應型。不少人 —— 尤爲是 oop 重度患者 —— 的慣性思惟是第一種。不管把負責業務的 service 類扔的多遠,service 和 controller 都是直接鏈接的

以請求數據爲例,我還停留在拿取數據推送到組件中進行渲染的階段。有那麼一段時間,我對 container 和 component 之間的值交換還侷限在屬性傳值回調函數層面上。雖然回調函數實現了必定程度上的事件響應,但組件之間仍舊脫離不開互相直連的主動型編碼的怪圈。

吃火鍋的正確姿式

舉一個吃火鍋的例子來解釋主動型編程響應型編程的差異:

通常狀況下,吃火鍋的時候都是點了菜和肉擺在本身臉跟前,想吃什麼本身夾了往鍋裏扔,看看快熟了就撈出來吃掉。

這是主動型。

可是這麼吃火鍋有三個問題:

第一,一切都要親力親爲。想吃肉就要親自把肉放到鍋裏去,想吃菜也要親自把菜放進去。若是還想吃豆腐、蘑菇、粉條、羊尾魚丸…想吃的東西越多,操做就越複雜。

第二,既然是親自放東西,就得把東西擺在本身面前。桌子一共就那麼點兒地方,想吃的東西越多佔用的空間就越大。既不容易留出足夠的空間吃燙熟的茼蒿,也不容易把想吃的牛肉片從眼前一大堆的蔬菜裏挑出來。萬一要換桌,還得的把這一大堆吃的一塊兒打包帶走,漏掉同樣就吃不到。

第三,若是我想吃撒尿牛丸和蝦滑、魷魚,旁邊的哥們海鮮過敏。是應該我負責往鍋裏放而後燙熟撈出來,他從我這裏撈他能吃的;仍是我倆各放各的,本身撈本身想吃的東西?前者雖然一我的作了共同的事情,可是別人一塊兒吃的時候不免會撈錯;後者雖然看起來互不干擾,可是兩我的都在燙牛丸,多少是浪費。萬一是個鴛鴦鍋我還不吃辣怎麼辦?燙到最後全亂套了。

用編程的術語說,即是:低內聚,高耦合

或許正統的 oop 語言(好比 Java)能夠用封裝、繼承、多態來某種程度的緩解這個問題(僅僅是某種程度上),可是 JavaScript 想從語言的角度實現就會無比操蛋(JavaScript 用 prototype 模擬 oop 實現,而 es6 裏的 class 和 Java 裏的 class 又徹底不是一個東西)。

如今咱們換種方式吃火鍋:分出一我的來啥也不吃,把全部吃的都放在他面前。想吃蘑菇就對他說一聲,讓他替你把蘑菇放進火鍋燙熟,替你把熟蘑菇放進蘸料碟裏。

你惟一要作的事情就是吼一嗓子,而後從本身的蘸料碟裏夾蘑菇,吃。

哎呀,這個就太爽了。

想吃豬腦,「來盤豬腦」;想吃鴨血,「來盤鴨血」;想吃 10 盤地瓜片就大喊「來10盤地瓜片」,用不着本身費事一盤一盤的往鍋裏倒。

並且既然食材都堆在另外一個地方,本身面前留一個吃東西的蘸料碟就夠用,十干淨整潔。桌子隨便換,揮揮衣袖帶雙筷子走就能夠了。

一羣人組團吃,你們各點各的吃互不干擾。燙火鍋的也始終只有一我的,既不會形成資源浪費,又沒必要讓其餘人關心額外的東西。

這就是所謂的響應型編碼。

被分出去的那我的,在 React 體系裏就是 Redux(或者相同功能的庫);具體到 dva 框架中,就是 model。

(理想狀況下)全部的組件只和 model 鏈接,互相之間徹底沒有直接交集,這即是響應型編碼思想在 dva 框架中的體現。

dva 中的響應型編碼

有了響應型編碼的理論之後,我很容易的就理解第 5 步的操做。

此時個人狀況是:

經過 dva g model user 能夠很方便的建立 model/user.js 並註冊進 index.js 中(命令行萬歲!) ,雖然目前還什麼都沒有:

我須要作的事情就是把數據從 api/user接口拉下來,渲染進 route/user 裏(component 能夠等等再談)。

把大象...我是說數據渲染進 route/user 須要三步:

  1. 編寫請求接口的方法
  2. 使用 1 的方法得到數據
  3. 將 2 數據渲染進頁面

編寫請求接口的方法

dva 的新手大禮包裏已經提供了基礎的網絡請求函數 utils/resquest.js ,雖然大多數狀況下都會對其進行一些擴展才能知足現實項目的需求,可是就目前來講暫且是夠用的。

以 oop 觀點來看,utils/resquest.js 至關於項目全部請求函數的基類(base class)。若是須要進行具體業務的編寫,應該新建一個繼承 utils/resquest.js 的子類。但 JavaScript 不算是純種 oop 的語言,因此慣例都是新建一個具體的業務類 services/user.js,經過在 services/user.jsimport 的方式調用 utils/resquest.js

// 在 services 目錄下新建 services/user.js,負責具體的 user 業務

import request from '../utils/request';


export function getUserData() { // 偷懶,暫時把 example.js 的代碼拷貝過來
  return request('api/users'); // 這裏是一個 promise 對象
}複製代碼

實際上這個時候若是直接把請求函數寫在 route/user.js 裏已經能夠渲染頁面了。

// 這是一個錯誤的示範

import React, { Component, PropTypes } from 'react';
import * as userService from '../services/user';

class User extends Component {
    static propTypes = {
        className: PropTypes.string,
    };

    constructor(props) {
        super(props);
        this.state = {
          list : []
        }
    }

    componentDidMount() {
      this.getData();
    }

    getData = () => {
      userService.getUserData().then((res) => {
        this.setState({
          list: res.data
        });
      })
    }

    buildContent = () => {
      const {list} = this.state;
      return list.map( (itm, index) => {
        return <div key={index}>{itm.name}</div>
      })
    }
    render() {
        return (
          <div>
            {this.buildContent()}
          </div>

        );
    }
}

export default User;複製代碼

這明顯是主動型編程寫法,和 dva 的響應型理念背道而馳。也許簡單或者低交互度的界面這麼寫起來會很省事,可是可擴展性接近於零。一旦複雜度和交互度提高,組件的會變得愈來愈複雜,最後變成一個巨大的坑。

在 model 中使用 services 函數並得到數據

有了 services/user.js 函數,能夠進行具體的請求動做,在 model/user.js 請求數據了。

應該寫在 model/user.js 哪裏呢?

這裏可能又要多說一點所謂純函數的概念,即對於給定的輸入有惟一不變的輸出並不含任何明顯可見的反作用(side effects)的函數(可參考這篇英文文章或者中文版)。

請求網絡數據自帶反作用屬性(異步操做),而反作用(side effect)看起來確實和 model/user.js 裏的某個屬性有點類似...

dva 的官方說法是:

真實狀況最多見的反作用就是異步操做,因此 dva 提供了 effects 來專門放置反作用,不過能夠發現的是,因爲 effects 使用了 Generator Creator,因此將異步操做同步化,也是純函數。

dva 負責處理異步的是封裝後的 redux-saga 模塊。也就是說,須要使用 call 方法。因此 dva 的請求數據套路是這樣的:

effects: {
    *getData(action, { call, put }) { // 第一個參數是 dispatch 所發出的 action,第二個參數是 dva 封裝的 saga 函數。能夠直接用 es 6 的解構風格拿取所須要的具體函數。
      const temp = yield call(userService.getUserData, {}); // 由於如今沒有參數
      console.log('temp', temp); // 純函數中不該有反作用(把數據轉移到控制檯也算反作用),這裏只是方便在 chrome 裏查看,
    }
  },複製代碼

寫完了?並無。

讚美太陽...呸!dispatch!

我眼中 dva 裏 dispatch-atcion 與 model/effect 的原理有點像 Android 四大組件之一的廣播:

  1. 經過 dispatch 函數發出一個包含 type 屬性(此爲必須)的 action。
  2. dva 監聽到 action 後會根據 action 的 type 值尋找對應 model 的 effect 的方法(action.type 的值就是 effects 裏的方法名,在 model 外 dispatch 須要使用 modelName/effectsMethodName 的格式)
  3. 找到方法後就調用方法,把 action 做爲參數放在第一個位置。

使用 dispatch 的好處是顯而易見的:切分業務模塊

組件沒必要再負責具體的業務操做(本身動手涮肉),只須要 dispatch action (大喊一聲) 到對應的 model 裏(給那個負責上菜的人)。

須要用戶列表數據的組件未必只有 route/user.js,其餘須要數據的組件能夠在本身裏面 dispatch action。

同時 model/user.js 的 getData 方法是獨一份,你 dispatch 多少 type 爲
user/getData (若是在 model 內 dispatch 能夠省略前綴)的 action 都得歸到我這來處理。

高內聚(業務處理集中),低耦合( 隨時隨地隨便哪一個組件隨意姿式 dispatch)。

官方教程中給出的作法是在 model 裏的訂閱部分 subscriptions寫一個監聽,根據監聽到具體的事件(進入 /user 頁面)進行特定操做(dispatch action)。

subscriptions: {
    setup({ dispatch, history }) {  // eslint-disable-line
     return history.listen( ({pathname, query}) => {
        if(pathname === '/user') {
          dispatch({
            type: 'getData',
            payload: {
              txt: 'hello, dva'
            }
          })
        }
      })
    },
  },複製代碼

這麼作一樣也是進一步切離業務,沒必要把 dispatch 寫在具體組件的生命週期中,減小組件的複雜程度(其實關鍵仍是 dispatch ,訂閱說到底也是爲 dispatch 服務的)。

如今應該能夠看到輸出後的數據了。

渲染數據

雖然如今拿到了數據,可是數據還憋在 model/effects 裏和 route/user.js 沒什麼關係,總的想個辦法把數據和組件關聯起來。

是時候讓 dva 的 state 出場了。

我理解的 dva 中 model 內的 state 屬性,其實是封裝後的 Redux 全局 store 的一部分。經過不重複的 namespace(桌號) 肯定 store(餐館) 中惟一的 model(餐桌),把 model/effects 請求到的原始數據(生食)放進 model/reducer (特定的火鍋)裏進行必要的處理(燙熟),再放進 model/state (蘸料碟)裏,route/user.js 只須要從這裏拿取所須要的數據(吃的)就能夠了。

從 effects 裏往 reducer 裏傳遞數據使用的是 saga 的put 方法,參數一樣也是一個 action 對象,action 中必須包含的 type 屬性的值就是 reducer 屬性裏的方法名:

import * as userService from '../services/user';

export default {
  namespace: 'user',
  state: {},
  reducers: {
    dealData(state, action) {
      // 理論上 reducer 裏的函數應該是純函數,此處只是爲了方便在控制檯裏看參數
      console.log('state==>', state);
      console.log('action==>', action);
      return { ...state }
    }
  },
  effects: {
    *getData(action, { call, put }) {
      const temp = yield call(userService.getUserData, {});
      yield put({
        type: 'dealData',
        payload: {
          temp
        }
      });
    }
  },
  subscriptions: {
    setup({ dispatch, history }) {  // eslint-disable-line
     return history.listen( ({pathname, query}) => {
        if(pathname === '/user') {
          dispatch({
            type: 'getData',
            payload: {
              txt: 'hello, dva'
            }
          })
        }
      })
    },
  },
};複製代碼

剩下的作法就是在 model/user.js 的 state 屬性裏定義一個屬性並賦值了。

state: {
    dataList: []
  },
  reducers: {
    dealData(state,
      { payload: { temp: { data: dataList } } }
      // action
      // { payload: { temp: { data: dataList  } }} 
      // 是 es 6 的解構作法,等同於
      // const {payload} = action;
      // const {temp} = payload;
      // const {data} = temp;
      // const dataList = data;
    ) {
      return { ...state, dataList }; // 必須有返回值(純函數必須有返回值),不然會報錯
      // 經評論提醒 修改 
      // 等同於 
      // let tmp = Object.assign([], this.state)  
      // tmp.dataList = dataList

    }
  },複製代碼

如今須要的數據已經掛在 model/user.js 的 state 屬性裏了,最後一步即是在 route/user.js 裏使用 connectmapStateToProps 讓組件監聽數據源,實現響應型編碼了。

import React from 'react';
import { connect } from 'dva'; // 0.關鍵的 connect 
import styles from './User.css';
import * as userService from '../services/user';
function User({ dataList }) { // 5. 這裏的屬性就是 3 裏的返回值

  return (
    <div className={styles.normal}>
      {
        !!dataList.length && dataList.map((data, index) => {
          return <div key={index}>{data.name}</div>
        })
      }
    </div>
  );
}

function mapStateToProps(store) { // 1關鍵的 mapStateToProps
  const { dataList } = store.user; // 2.從 model/user.js 拿取須要的數據
  return { dataList }; // 3.將數據做爲屬性返回
}

export default connect(mapStateToProps)(User); // 4.鏈接組件複製代碼

碎碎念

其實日後的代碼還有蠻多,分頁、封裝、引入 antd 調整樣式。不過都是一些須要花時間慢慢雕琢、順便發發 dispatch 的細節(其實細節也很重要 >_<),至少理解起來比較容易了。

理解第 5 步思路的順序是基於數據流向的,而實際開發中的編寫順序恰好是倒過來:先肯定頁面須要的數據,再編寫 model 中的業務,最後把網絡接口掛進來。不過如今這麼幹已經內心有譜,知道怎麼回事了。

可喜可賀。

相關文章
相關標籤/搜索