dva 上手筆記 ( 一 )

dva 有着阿里巴巴的金字招牌,使用者不乏。下份工做必須上手 dva 了,因而乎做爲一個以前主要使用 redux + thunk + promise-middleware 的用戶,開始了探究之旅。css

吐槽

  • 我的認爲 dva 想作的事情太多了。dva 涵蓋了 redux, react-redux, redux-saga, react-router, react-router-redux, isomorphic-fetch。巴不得一個 import dva from 'dva' 就解決全部事。其中最奇怪的是涵蓋 react-router 這個決定,由於以我目前看來,dva 並無任何對 react-router 的「改進」,只是原本來本使用了它,既然如此,何須要包含它呢?html

  • 不過優勢是,雖然涵蓋了不少庫,dva 只是很薄的一層,因此哪怕文檔沒有寫,redux,saga 或者是 router 的用法都是能夠照搬,學習曲線很平。node

本文適合

已經瞭解 redux 的使用,但還未深刻接觸 dva 的各位。dva 的文檔說實在只能打個 80 分,提供的實例除了最簡單單文件 counter 實例外,就是一堆直接整合 umi 的大項目。我的認爲上手從 Account System 這個實例開始看比較好。不過這裏仍是有個gap,因此本篇嘗試填補一下。react

以實戰的角度討論如何從 redux 快速轉型 dva,同時比較二者使用感觸上的不一樣。git

第一天的課題

將本身以前寫的 todo-list redux 最佳實踐 (多文件)改寫成 dva 項目。github

todo list

dva 是什麼?

dva 是一個試圖簡化 React 開發流程,特別是 redux 狀態管理流程的輕框架。npm

從 Counter 瞭解 api

文檔的第一個例子, 熟悉的counter: json

counter

import React from "react";
import dva, { connect } from "dva";
// 1. 生成app實例
const app = dva();

// 2. Model 模型
const counter = {
  namespace: "count",
  state: 0,
  reducers: {
    add(state, action) {
      return state + 1;
    },
    minus(state, action) {
      return state - 1;
    }
  }
}

app.model(counter);

// 3. UI. 注意 model根據 namespace 和 reducer 自動生成的 type
const App = props => {
  console.log(props);
  return (
    <div> <h2>{props.count}</h2> <button onClick={() => { props.dispatch({ type: "count/add" }); }} > + </button> <button onClick={() => { props.dispatch({ type: "count/minus" }); }} > - </button> </div>
  );
};

// 4. react-redux 的 connect
const EnhancedApp = connect(({ count }) => ({ count }))(App);

// 5. Router. 提供 history props 給 Router 組件,這裏不須要因此照常寫
app.router(({ history }) => <EnhancedApp />);

// 6. 運行app, 並掛到 id 爲 root 的 div 上(相似於 reactDOM.render)
app.start("#root");
複製代碼

概述

  • 1 和 6 是必寫模板
  • 2 就是寫 reducer,同時自動生成了 action 的 type
  • 3,4 是 UI,connect 使用方法徹底相同
  • 5 是根組件,提供 history props, 通常在這裏寫路由佈局,沒有特別規範,只要返回的是組件就行

亮點

  1. 自動生成了 action 的 type
  2. 簡化了 reducer 的寫法

帶 payload 的 action 怎麼寫?

要寫一個點 「+」、「-」 增減任意指定數目的 counter 該如何改?redux

// 首先是 reducer
reducers: {
    add(state, action ) {
      return state + action.payload;
    },
    // 也能夠再改進,當payload不被指定時,默認1
    minus(state, { payload = 1 }) {
      return state - payload;
    }
  }
// 其次是 action
<button
    onClick={() => {
      props.dispatch({ type: "count/add", payload: 2 }); 
    }}
  >
複製代碼

dva-cli 學習寫項目的基本結構

瞭解了基本用法,下面探索寫項目時如何合理地佈局項目結構。
dva 有相似 create-react-app 的腳手架 dva-cliapi

npm i -g dva-cli

## 創建名爲 my-first-dva 的項目
dva new my-first-dva 
複製代碼
  1. 項目結構以下
my-first-dva
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── mock
├── public
└── src
    ├── assets ## 資源
    ├── components ## 純組件
    ├── models ## 模型
    ├── routes ## 頁面組件
    ├── index.js ## 起始點
    └── router.js ## 路由
複製代碼
  1. 從入口文件index.js 看起
import dva from 'dva'
import './index.css'

// 1. 初始
const app = dva()

// 2. 插件,若是有的話使用
// app.use({});

// 3. 模型
app.model(require('./models/example').default)

// 4. 路由
app.router(require('./router').default)

// 5. 啓動
app.start('#root')
複製代碼

大體上把model和router部分拆分出去是基本作法。
爲啥 index.js 裏莫名其妙地使用 require 語法,是個迷。嘗試了下,使用正常的 import 語法是沒問題的:

import dva from 'dva'
import './index.css'
import routes from './router'
import example from './models/example'

// 1. Initialize
const app = dva()

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

// 3. Model
app.model(example)

// 4. Router
app.router(routes)

// 5. Start
app.start('#root')
複製代碼

那麼,model不止一個咋辦?很簡單,屢次使用 app.model() 便可。 例如,推薦demo Account Systemindex.js 就以下

import './index.html';
import './index.less';
import dva from 'dva';
import {browserHistory} from 'dva/router';
import router from './router';
import home from './models/home';
import orders from './models/orders';
import storage from './models/storage';
import manage from './models/manage';
import systemUser from './models/systemUser';
import customers from './models/customers';
import products from './models/products';
import suppliers from './models/suppliers';
import settlement from './models/settlement';
import resource from './models/resource';
import customerBills from './models/customerBills';
import supplierBills from './models/supplierBills';

// 1. Initialize
const app = dva({
	history: browserHistory
});

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

// 3. Model
app.model(home);
app.model(orders);
app.model(storage);
app.model(manage);
app.model(systemUser);
app.model(customers);
app.model(products);
app.model(suppliers);
app.model(settlement);
app.model(resource);
app.model(customerBills);
app.model(supplierBills);

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

// 5. Start
app.start('#root');
複製代碼
  1. UI組件結構
  • 一個總路由文件 router.js
  • 每一個頁面一個組件,放置在 routes 文件夾下
  • 複用的UI組件放置在 components 文件夾下
router.js ---> routes 組件 ----> components組件
複製代碼

UI 的大體結構如上。

// router.js
// react-router 怎麼寫,這兒就咋寫
import React from 'react'
import { Router, Route, Switch } from 'dva/router'
import IndexPage from './routes/IndexPage'

const RouterConfig = ({ history }) => (
  <Router history={history}> <Switch> <Route path="/" exact component={IndexPage} /> </Switch> </Router> ) export default RouterConfig 複製代碼

至此,一個正常 dva 項目如何擴展你們應該有個概念。

試寫 todo-list

接着用 todo-list redux 最佳實踐 練個手。看看如何將一個純 redux 項目快速改形成 dva 項目。並嘗試分析一下其中產生的好處(和壞處?)。你們能夠先試試手。我本身寫下來,開始感受最大的思考點是「選擇器」,不事後來發現這徹底不是問題。

我寫的dva實現:todo-list demo

大致思路:

  • 改寫入口文件 index.js
  • 將reducers 改寫成 models
  • 改寫UI組件。因爲並非多頁面路由,因此全部的組件都放在了本來的components文件夾下
  • 處理其餘細節,好比選擇器等

1. 如何將一個reducer改爲model?

直接上代碼了

// redux 添加和toggle一個todo
let nextId = 4;
const todos = (state = [], action) => {
  switch (action.type) {
    case "ADD_TODO":
      return [
        ...state,
        {
          id: nextId++,
          detail: action.payload.detail,
          completed: false
        }
      ];
    case "TOGGLE_TODO":
      return state.map(t => {
        if (t.id === action.payload.id) {
          return { ...t, completed: !t.completed };
        }
        return t;
      });
    default:
      return state;
  }
};

export default todos;
複製代碼

改寫成

let nextId = 4;

export default {
  namespace: "todos",
// redux例子裏本來createStore的initialState也直接放進來了。
  state: [
    { id: 1, detail: "學習graphQL", completed: false },
    { id: 2, detail: "寫博客", completed: false },
    { id: 3, detail: "本週的西部世界", completed: true }
  ],
// 由於namespace,reducer命名能夠更加簡潔
  reducers: {
    add(state, action) {
      return [
        ...state,
        {
          id: nextId++,
          detail: action.payload.detail,
          completed: false
        }
      ];
    },

    toggle(state, action) {
      return state.map(todo => {
        if (todo.id === action.payload.id) {
          return { ...todo, completed: !todo.completed };
        }
        return todo;
      });
    }
  }
};
複製代碼

2. 如何在UI組件裏使用actions?

方案一:上面的 todos model 對應的 UI 是<List />組件, 用於展現todo的列表。因爲 dva 中 action type 已在書寫 model 時自動定義,這裏只須要直接使用:

//List.js

import React from "react";
import { connect } from "dva";
import { getFilteredTodos } from "../models";

const List = ({ filteredTodos, dispatch }) => {
// 直接 dispatch action ////////////////
  const handleClick = id => {
    dispatch({
      type: "todos/toggle",
      payload: { id }
    });
  };
////////////////////////////////////// 
  return (
    <ul className="list pl0 pv5"> {filteredTodos.map((t, index) => ( <li key={t.id} onClick={() => handleClick(t.id)} > {t.completed && <span>✔️ </span>} {t.detail} </li> ))} </ul>
  );
};

const mapStateToProps = state => ({ filteredTodos: getFilteredTodos(state) });

const ConnectedList = connect(mapStateToProps)(List);

export default ConnectedList;
複製代碼

方案二:事實上,dva 的 connect 與 react-redux的相同,還能夠接收第二個參數 mapDispatchToProps, 因此另外一種使用方式是將handleClick內dispatch action的部分轉移至 connect 內,並利用到 react-redux 的語法糖簡寫:

import React from "react";
import { connect } from "dva";
import { getFilteredTodos } from "../models";

const List = ({ filteredTodos, toggle }) => (
    <ul className="list pl0 pv5"> {filteredTodos.map((t, index) => ( <li key={t.id} onClick={() => toggle(t.id)} > {t.completed && <span>✔️ </span>} {t.detail} </li> ))} </ul>
);

const ConnectedList = connect(
  state => ({ filteredTodos: getFilteredTodos(state) }),
  {
    toggle: id => ({
      type: "todos/toggle",
      payload: { id }
    })
  }
)(List);

export default ConnectedList;
複製代碼

方案三:固然若是將全部的 actionCreator 寫在一個文件中,在我看來也不錯。

3. 選擇器的書寫

List.js裏,用到了一個選擇器 getFilteredTodos(state), 功能是經過 todos 和 filter 來計算此時頁面所應該顯示的是哪些 todo (例如點擊「未完成」,就該只顯示未完成的todos)。
寫這個demo時,忽然意識到選擇器只是一個普通的 js 函數,因此在 dva 裏照舊正常使用,不需任何修改。個人作法如你們所見,將全部選擇器放在 model/index.js 裏。

如何拆分很是複雜的reducers

從開始使用時,這就是我最大的關注點,不過看完全部的 demo,彷佛並無獲得解答( 很驚訝,你們彷佛都以爲兩層夠用了 )。簡單的說,dva 的模型是兩層結構,一個總的 model 由不少第二層的小 model 經過 app.model() 的方式聚合組成。但若是須要第三層呢?這方面 redux 使用 combineReducers() 是不受限制的,reducer 套 reducer 能夠無限套下去。但目前我沒想到啥簡單的 dva 處理方式。用代碼敘述一下這個問題:

// redux 中, 如上例的todos reducer,若是還須要添加一個 isFetching 狀態,那麼
import { combineReducers } from 'redux'

const todos = (state = [], action) => { ... }
// 添加新的reducer
const isFetching = (state = false, action) => {
    switch(action.type){
        case "FETCH":
        return !state
        default:
        return state
    }
}
// 合併
export default combineReducers({ todos, isFetching })
複製代碼

在 redux 裏很簡單的實現,但在 dva 裏:

const todos = {
  namespace: "todos",

  state: [ ... ],

  reducers: { ... }
};
// 添加新的 model
const isFetching = {
  namespace: "todos",

  state: false,

  reducers: {
    fetch (state, action) {
      return !state
    }
  }
};

// 如何合併兩個model成爲一個呢?本身寫一個 combineModels() 嗎 ? 
複製代碼

這個問題我沒想到怎麼辦,但願各位大神幫忙解答!

結語

dva 的上手篇,還沒涉及到異步以及redux-saga 的部分。目前看來

優勢

  1. 以前寫大項目最頭痛的 action namespace 的問題在此漂亮的解決了
  2. 同時 action type 的生成也是自動的
  3. reducer 的書寫簡潔了很多

(雖然分了三點說,但差很少是一個事兒)

缺點

彷佛沒法很好的分解超過兩層的複雜狀態?(求解)

下一篇,探究 dva 異步的 api 。

個人其餘文章列表:傳送門

相關文章
相關標籤/搜索