DvaJS前端数据流之一小时快速上手

676 阅读10分钟

简介

dva首先是一个基于redux和redux-saga的数据流方案,然后为了简化开发体验,dva还额外内置了react-router和fetch,所以也可以理解为一个轻量级的应用框架。[dva = React-Router + Redux + Redux-saga]

最简结构

dva应用的最简结构:

import dva from 'dva';
const App = () => <div>Hello dva</div>;

// 创建应用
const app = dva();
// 注册视图
app.router(() => <App />);
// 启动应用
app.start('#root');

🥷分层开发

主要分层结构主要有以下几点:

  • Page 负责与用户直接打交道:渲染页面,接受用户的操作输入,侧重于展示型交互性逻辑,这里需要了解无状态组件
  • Model 负责处理业务逻辑,为Page做数据、状态的读写、变换、暂存等,Dva中model就是做了这一层的操作
  • Service 负责与HTTP接口对接,进行纯粹的数据读写

🥷数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过dispatch发起一个action,如果是同步行为会直接通过Reducers改变State,如果是异步行为(副作用)先触发Effects然后流向Reducers最终改变State

核心概念:

  • State:一个对象,保存整个应用状态

  • View:React 组件构成的视图层

  • Action:一个对象,描述事件

  • connect 方法:一个函数,绑定State到View

  • dispatch 方法:一个函数,发送Action到State

🥷基础概念

🍪namespace

  • model的命名空间,同时也是他在全局state上的属性
  • 整个应用的State,由多个小的Model的State以namespace为key合成
  • 只能用字符串,不支持通过“.”的方式创建多层命名空间,相当于这个model的key
  • 在组件里面,通过connect这个key将想要引入的model加入
import { connect } from 'dva';

export default connect(({namespaceValue}) => ({...namespaceValue}))(DvaComponent);

🍪State

type State = any

  • 表示Model的状态数据 (通常表现为一个javascript对象)
  • 数据保存在这里,直接决定了视图层的输出
  • 操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系

🍪State 和 View

State是储存数据的地方,收到Action以后,会更新数据。

View就是React组件构成的UI层,从State取数据后,渲染成HTML代码。只要State有变化,View就会自动更新

🍪Reducer

type Reducer<S, A> = (state: S, action: A) => S

  • 必须是纯函数,有固定输入输出,主要目的是修改自身state(Action处理器,处理同步动作,用来算出最新的State)
  • 可以看做是state的计算器。它的作用是根据Action,从上一个State算出当前State
  • Reducer函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值
  • 在dva中,reducers聚合积累的结果是当前model的state对象。通过actions中传入的值,与当前reducers中的值进行运算获得新的值(也就是新的state)
  • 需要注意的是同样的输入必然得到同样的输出,它们不应该产生任何副作用effect。并且,每一次的计算都应该使用immutable data
// count +1
function add(state) { return state + 1; }

// 往 [] 里添加一个新 todo
function addTodo(state, action) { return [...state, action.payload]; }

// 往 { todos: [], loading: true } 里添加一个新 todo,并标记 loading 为 false
function addTodo(state, action) {
  return {
    ...state,
    todos: state.todos.concat(action.payload),
    loading: false
  };
}
...
reducers: {
	/** 设置RPC标签列表 */
  setRpcLabels(state, { payload: { data } }) {
      const {
          labelList,
      } = data;
      return {
          ...state,
          labelList: [...labelList],
      };
  },
  ...
}
...

🍪Effect

  • 主要用于异步请求,接口调用之类的(Action处理器,处理异步动作)
  • effect被称为副作用,在我们的应用中,最常见的就是异步操作
  • 它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出
  • dva为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数
function *addAfter1Second(action, { put, call }) {
  yield call(delay, 1000);
  yield put({ type: 'add' });
}
...
effects: {
	/** 获取RPC已有标签列表 */
  * getRpcLabels({ payload }, { call, put }) {
      const {
          produceId, // 模块id
      } = payload;
      if (produceId) {
          const rsp = yield call(service.getLabels, { produceId });
          if (rsp) {
              yield put({ type: 'setRpcLabels', payload: { data: rsp.data } });
          }
          return rsp;
      }
  },
  ...
},
...

🍪Subscription

  • subscription语义是订阅,用于订阅一个数据源,然后 根据条件dispatch需要的action
  • 数据源可以是当前的时间、服务器的websocket连接、keyboard输入、geolocation变化、history路由变化等等
  • 内部定义的函数都会被被执行,执行之后作为监听来处理事务
import key from 'keymaster';
...
app.model({
  namespace: 'count',
  subscriptions: {
    keyEvent({dispatch}) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
});

🍪Action

type AsyncAction = any

  • Action是一个普通javascript对象,它是改变State的唯一途径(是用来描述 UI 层事件的一个对象)
  • 无论是从UI事件、网络回调,还是WebSocket等数据源所获得的数据,最终都会通过dispatch函数调用一个action,从而改变对应的数据
  • action必须带有type属性 指明具体的行为,其它字段可以自定义,如果要发起一个action需要使用 dispatch函数;需要注意的是 dispatch是在组件connect Models以后,通过props传入的
dispatch({
  type: 'add',
});

🍪dispatch函数

type dispatch = (a: Action) => Action

  • dispatch函数是 一个用于触发action的函数(用来将Action发送给State) ,action是改变state的唯一途径,但是它只描述了一个行为,而dipatch可以看作是触发这个行为的方式,Reducer则是描述如何改变数据的
  • 在Dva中,connect model的组件通过props可以访问到dispatch, (意思就是:被connect的Component会自动在props中拥有dispatch方法 可以调用Model中的Reducer或者Effects
常见形式:
dispatch({
  type: 'user/add', // 如果在model外调用,需要添加namespace
  payload: {}, // 需要传递的信息
});
import { connect } from 'dva';
const testCom = props => {
  const { dispatch } = props;
  const changeValue = (id, val) => {
    // 调用reducer,一般是同步修改state中的值
    dispatch({
      type: 'dva/save',
      payload: {
        param: val
      },
    });
    // 调用effect,一般是发送后台请求
    dispatch({
      type: 'dva/queryValue',
      payload: {
        id: id
      },
    });
  };
  return(
    <div>'hello world'</div>
  )
}
export default connect(({ dva }) => ({ ...dva }))(testCom);

🍪Router

  • 这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作
  • dva 实例提供了router方法来控制路由,使用的是react-router
import { Router, Route } from 'dva/router';
app.router(({history}) =>
  <Router history={history}>
    <Route path="/" component={HomePage} />
  </Router>
);

🍪Route Components

  • 在dva中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而 /components/目录下则是纯组件

🍪connect方法

  • connect是一个函数,绑定State到View
  • connect 方法返回的也是一个React组件,通常称为容器组件。因为它是原始UI组件的容器,即在外面包了一层State。
  • connect 方法传入的第一个参数是mapStateToProps函数,mapStateToProps函数会返回一个对象,用于建立State到Props的映射关系。
import { connect } from 'dva';

function mapStateToProps(state) {
  return { todos: state.todos };
}
connect(mapStateToProps)(App);
export default connect(({ rpc, global, login }) => ({ rpc, global, login }))(UpdateCaseForm);

最简结构(带model)

dva提供 app.model 这个对象,所有的应用逻辑都定义在它上面:

const app = dva();

// 新增这一行
app.model({ /**/ });

app.router(() => <App />);
app.start('#root');

Model对象的例子:

{
  namespace: 'count',
  state: 0,
  reducers: {
    add(state) { return state + 1 },
  },
  effects: {
    *addAfter1Second(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'add' });
    },
  },
}

dva应用的最简结构(带model):

// 创建应用
const app = dva();

// 注册 Model
app.model({
  namespace: 'count',
  state: 0,
  reducers: {
    add(state) { return state + 1 },
  },
  effects: {
    *addAfter1Second(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'add' });
    },
  },
});

// 注册视图
app.router(() => <ConnectedApp />);

// 启动应用
app.start('#root');

🥷Model中的Effects函数解析

需要注意的是:Effects里面的函数都是Generator函数

🍪Generator 函数

  • Effect是一个Generator函数,内部使用yield关键字,标识每一步的操作(不管是异步或同步)
* getRpcPageType({ payload }, { call, put }) {
  const {
      dictType,
  } = payload;
  if (dictType) {
      const rsp = yield call(service.getOptions, { dictType });
      if (rsp) {
          yield put({ type: 'setRpcPageTypes', payload: { data: rsp.data } });
      }
      return rsp;
  }
},
...

🍪yield

  • 固定关键词,Generator函数自带的关键词,和*搭配使用,有点像async和await,使用*则表明它是Generator函数
  • 然后每使用一个yield就是告诉程序这里是异步,需要等待这个后面的代码执行完成,同步代码可不使用该关键词

🍪payload

  • 页面上通过dispatch传过来的payload同名参数

🍪select

  • Dva中Effects函数的固定传参
  • 用于拿到model中state的数据,需要注意的是,state后面跟命名空间namespace的值
const data = yield select((state) => state.namespaceName.valueName);

🍪call

  • Dva中Effects函数的固定传参
  • 第一个参数是一个异步函数,payload是参数,可以 通过call来执行一个完整的异步请求,又因为yield的存在,就实现了异步转同步的方案
const { data } = yield call(queryInterface, payload);

🍪put

  • Dva中Effects函数的固定传参
  • 发出一个Action,类似于dispatch
  • 可以使用同model中的Reducers或者Effects通过Reducers来实现数据到页面的更新,也可以通过put实现Effects的嵌套使用
yield put({
	type: 'save',
	payload: {
	  ...payload
	},
});

🥷开发目录

由于常见的umi框架已经对Dva做了深度继承,会默认将src/models下的model定义自动挂载,只需要在model文件夹中新建文件即可新增一个model用来管理组件状态,对于某个page文件夹下面的model也会默认挂载

├─assets `静态资源`
├─components `公共组件`
├─config `路由和环境配置`
├─constants `全局静态常量`
├─locale `国际化`
│  ├─en_US `英文配置`
│  └─zh_CN `中文配置`
├─models `全局数据状态` *Dva涉及的目录*
├─pages `页面目录,用我参与开发的其中一个目录来作为示例` *Dva涉及的目录*
│  ├─NodeConfig  `NodeConfig示例目录`
│  │  ├─components
│  │  │  ├─Select `Select组件页面文件` *Dva涉及的目录*
│  │  │  │  └─components
│  │  │  │    ├─AudienceInfo
│  │  │  │		│	├─index.js
│  │  │  │		│	└─index.less
│  │  │  │    ├─BlackList
│  │  │  │		│	├─index.js
│  │  │  │		│	└─index.less
│  │  │  │    ├─ControlGroup
│  │  │  │		│	├─index.js
│  │  │  │		│	└─index.less
│  │  │  │    ├─GroupSelect
│  │  │  │		│	├─index.js
│  │  │  │		│	└─index.less
│  │  │  │		├─index.js
│  │  │  │		└─index.less
│  │  ├─models
│  │  │  ├─select.js `Select组件数据状态管理` *Dva涉及的目录*
│  │  └─services
├─services `全局接口配置`
├─themes `全局样式主题`
└─utils `js通用工具`

PS: 该树形图通过 `windows shell` 自带的 `tree` 命令生成

Dva示例

步骤1:首先定义一个简易的model示例

export default {
  namespace: 'dva',
  state: {
	id: '',
    value: {},
  },
  effects: {
		// 所有effect前必须要加 *
    * queryValue({ payload }, { select, call, put }) {
  	  const params = {
  		  id: payload.id ? payload.id : yield select(state => state.select.id)
  	  }
      // queryInterface是定义好的后台请求接口,一般用axios或fetch来完成
      const { data } = yield call(queryInterface, params);
      yield put({ type: 'save', payload: data });
    },
  },
  reducers: {
    save(state, { payload }) {
      return {
        ...state,
        ...payload,
      };
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
	  	key('⌘+up, ctrl+up', () => { dispatch( {type:'save'}) });
    },
  },
};

步骤2:然后把model和组件绑定在一起(React的Connect函数是一种柯里化写法)

import { connect } from 'dva';

const testCom = props => {
  const { helloWorld = 'hello world'} = props;
  return(
    <div>{ helloWorld }</div>
  )
}

// 绑定之后就可以在testCom组件中使用命名为dva的model了
export default connect(({ dva }) => ({ ...dva }))(testCom);

备注知识点:

1)柯里化

是把接受多个参数的函数转换成接受一个单一参数的函数

// 柯里化
var foo = function(x) {
	return function(y) {
		return x + y
	}
}
foo(3)(4)


// 普通方法
var add = function(x, y) {
	return x + y;
}
add(3, 4) 

2)无状态组件

创建无状态组件是为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到要改变state状态的操作,在实际项目中页面组件被写成无状态的组件,通过简单组合可以构建成页面或复杂组件,通过多个简单组件来合并成一个复杂的大应用。

优点是:由于是无状态组件,所以无状态组件就不会在有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升;代码整洁、可读性高,对于大型项目的开发维护非常有好处

const NoStateComponent = props => {
  const { helloWorld = 'hello world'} = props;
  return(
    <div>{ helloWorld }</div>
  )
}
export default NoStateComponent;

参考内容

Dva官网:dvajs.com/

DvaJS github地址:github.com/dvajs/dva

Dva.js快速上手指南:blog.csdn.net/PirateRacco…

图解DVA:www.yuque.com/flying.ni/t…

Generator函数详解:blog.csdn.net/weixin_4617…