React中使用Dva


在公司使用Antd时接触到了dva,它是由阿里架构师 sorrycc 带领 team 完成的一套前端框架,在作者的 github 里是这么描述它的:“dva 是 react 和 redux 的最佳实践”。dva 是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。简单的说 dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。dva 是 react 和 redux 的最佳实践。最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起。Dva = React-Router + Redux + Redux-sagaDva官网地址

Dva快速上手

dva的安装和启动只需四个步骤,整合antd地址

// 1.安装dva-cli
npm install dva-cli -g

// 2.创建项目:dva-test项目名
dva new dva-test

// 3.进入dva-test目录
cd dva-quickstart

// 4.启动项目
npm start

启动成功后访问 http://localhost:8000/, 会出现如下界面

react项目的推荐目录结构,如果使用dva脚手架创建,则自动生成如下

|── /mock/             # 数据mock的接口文件  
|── /src/              # 项目源码目录(我们开发的主要工作区域)   
|   |── /components/   # 项目组件(用于路由组件内引用的可复用组件)   
|   |── /routes/       # 路由组件(页面维度) 
|   |  |── route1.js  
|   |  |── route2.js   # 根据router.js中的映射,在不同的url下,挂载不同的路由组件
|   |  └── route3.js    
|   |── /models/       # 数据模型(可以理解为store,用于存储数据与方法)  
|   |  |── model1.js  
|   |  |── model2.js   # 选择分离为多个model模型,是根据业务实体进行划分
|   |  └── model3.js  
|   |── /services/     # 数据接口(处理前台页面的ajax请求,转发到后台)   
|   |── /utils/        # 工具函数(工具库,存储通用函数与配置参数)     
|   |── router.js       # 路由配置(定义路由与对应的路由组件)  
|   |── index.js       # 入口文件  
|   |── index.less      
|   └── index.html     
|── package.json       # 项目信息  
└── proxy.config.js    # 数据mock配置  

Dva 概念

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。

从图中可以看出dva是设计理念是把数据层和组件层剥离开来,实现完全解耦。简单的分析一下这个图:

首先我们根据 url 访问相关的 Route-Component,在组件中我们通过 dispatch 发送 actionmodel 里面的 effect 或者直接 Reducer当我们将action发送给Effect,基本上是取服务器上面请求数据的,服务器返回数据之后,effect 会发送相应的 actionreducer,由唯一能改变 statereducer 改变 state ,然后通过connect重新渲染组件。当我们将action发送给reducer,那直接由 reducer 改变 state,然后通过 connect 重新渲染组件。这样我们就能走完一个流程了。

Models

State

State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。在 dva 中你可以通过 dva 的实例属性 _store 看到顶部的 state 数据,但是通常你很少会用到:

const app = dva();
console.log(app._store); // 顶部的 state 数据

Action

action 的格式如下,它需要有一个 type ,表示这个 action 要触发什么操作;payload 则表示这个 action 将要传递的数。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。

dispatch({
  type: 'add',
});

具体可以查看文档:redux——action

Dispatch 函数

dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:

dispatch({
  type: 'user/add', // 如果在 model 外调用,需要添加 namespace
  payload: {}, // 需要传递的信息
});

Reducer

Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。Reducer 的概念来自于是函数式编程,很多语言中都有 reduce API。如在 javascript 中:

[{x:1},{y:2},{z:3}].reduce(function(prev, next){
    return Object.assign(prev, next);
})
//return {x:1, y:2, z:3}

在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。

Effect

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于 纯函数,如果你想了解更多可以阅读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。其中它用到了redux-saga,里面有几个常用的函数。

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

    yield put({ type: 'minus' });
  2. call 用于异步调用逻辑,支持Prommise

    const result = yield call(fetch, '/todos');
  3. select 用于从 state 中获取数据

    const todos = yield call(state, state.todos);

简单使用例子

构建service

新增文件 src/services/users.js,内容如下,这里写了用户的api接口列表,即用户增删改查

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

//查询
export function fetch({ page }) {
  return request(`/api/users?_page=${page}&_limit=$5`);
 }
 //删除
export function remove(id) {
  return request(`/api/users/${id}`, {
    method: 'DELETE',
  });
}
//修改
export function patch(id, values) {
  return request(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(values),
    headers: {
    'Content-Type': 'application/json',
      'Accept': 'application/json',
  },
  });
}
//新增
export function create(values) {
   return request('/api/users', {
       method: 'POST',
        body: JSON.stringify(values),
      });
  }

构建model

可在终端通过命令 dva g model users生成文件,也可手动创建,然后修改 src/models/users.js

import fetch as usersService from '../services/users';

export default {

  namespace: 'example',   //表示对于整个应用不同的命名空间,以便通过this.props.example访问,和当前model文件名相同就好之前的reducer名字相同,是全局state的属性,只能为字符串,不支持.的方式建立多重

  state: {initText:"hello"},     //表示当前的example中的state状态,这里可以给初始值

  subscriptions: {
    setup({ dispatch, history }) {  // 订阅,可以监听服务器连接,键盘输入,路由,状态等的变化
    },
  },

  effects: {
    *test1({ payload }, { call, put }) { 
     //payload是从组件router传递过来的参数,
     //这里的call方法可以使用payload参数传递给后台程序进行处理这里可以调用service层的方法进行调用后端程序,
     //这里的put表示存储在当前命名空间example中,通过save方法存在当前state中
      const response = yield call(fetch , payload);
      yield put({
            type: 'saveTest',
            payload: response
          });
    },
  },

  //用来保存更新state值 上面的put方法调用这里的方法
  reducers: {
    saveTest(state, payload) {
      return { ...state, ...payload };
    },
  },

};

上面的model是需要注册到src/index.js中的app.router(require(‘./router’).default);否则后面和route组件无法用connect传递数据,如果定义了多个model,那么需要使用多个app.model()来传递model数据;

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

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

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

// 3. Model
app.model(require('./models/example').default);

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

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

构建components

前面的model的demo中定义的example.js中定义了state数据,那么在route的路由组件中怎么获取到这个数据呢?通过connect可以传递过来,然后通过this.props就可以访问了,同样也会把dispatch(可以发送请求到model去),history方法传递过来,这样就可以通过组件获取到model保存的值了。这里有三种写法

  1. 方式一

    import React from "react";
    import {Component} from 'react';
    import { connect } from "dva"; //从dva中导入connect
    
    class Counter extends Component {
        constructor(props){
            super(props)
        }  
        render (){
            return (
                <div>
                     <p> this.props.example.initText</p> //如这里就获取到了上面定义的initText数据了
                </div>
            )
        }
    }
    const mapStateToProps = (state) =>{
        return {
            example:state.example, //这里的example表示后面用this.props.example获取state(根节点)中exmpale命名空间(model的example.js中的state所有数据)的数据
        }
    }
    export default connect (mapStateToProps)(Counter) //通过这种方式来把model层的数据传递到当前组件了
  2. 方式二

    import React from "react";
    import {Component} from 'react';
    import { connect } from "dva"; //从dva中导入connect
    
    class Counter extends Component {
        constructor(props){
            super(props)
        }  
        render (){
            return (
                <div>
                     <p> this.props.example.initText</p> //如这里就获取到了上面定义的initText数据了
                </div>
            )
        }
    }
    
    export default connect ({example})(Counter) //通过这种方式来把model层的数据传递到当前组件了,默认这面的也是example属性,通过this.props.example可以获取到model(example.js)中state的数据了
  3. 方式三

    采用ES6注解的形式传递值,效果也是一样的

    import React from "react";
    import {Component} from 'react';
    import { connect } from "dva"; //从dva中导入connect
    
    @connect({example})
    class Counter extends Component {
        constructor(props){
            super(props)
        }  
        render (){
            return (
                <div>
                     <p> this.props.example.initText</p> //如这里就获取到了上面定义的initText数据了
                </div>
            )
        }
    }

Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
Audio实现音频文本同步 Audio实现音频文本同步
最近公司项目接到一个需求,要实现录音播放时和文本同步,类似于音乐播放器播放歌曲时,歌词和声音同步的效果。要实现这个功能需要监听audio标签的实时播放时间,然后在监听事件中处理文本,通过当前时间定位到对应的文本数据,并展示在页面。 原生实现
2020-06-17
Next 
ReactNative对比Cordova ReactNative对比Cordova
事实上对于移动端,支持HTML,JS和Css的框架并不只有ReactNative,还有诸如Cordova,Fultter等。Cordova 是 Apache 旗下的。 React-Native 是 Facebook 旗下的在2013年发布的
2020-06-13
  TOC