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 。

个人其余文章列表:传送门

相关文章
相关标签/搜索