学习react前端框架dva

 dva 是由阿里架构师 sorrycc 带领 team 完成的一套前端框架,在作者的 github 里是这么描述它的:“dva 是 react 和 redux 的最佳实践”。


一.介绍

1.What's dva ?

dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。dva 是 react 和 redux 的最佳实践。最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起。官网    dva = React-Router + Redux + Redux-saga

2.安装

1.安装 dva-cli
npm install dva-cli -g
2.扎到安装项目的目录
cd ylz_project/my_reactdemo
3.创建项目:Dva-test项目名
dva new Dva-test
4.进入项目
cd Dva-test
5.启动项目
npm strat

 成功。

3.项目结构

二.概念

下面都是官网的知识点

(1).数据流向

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

(2).Model

model 是 dva 中最重要的概念,Model 非 MVC 中的 M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发

1.State

State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。

// dva()初始化
const app = dva({
  initialState: { count: 1 },
});

// modal()定义事件
app.model({
  namespace: 'count',
  state: 0,
});

//初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中  modal 中的优先级低于传给  dva() 的  opts.initialState

2.Action   :type dispatch = (a: Action) => Action

Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。表示操作事件,可以是同步,也可以是异步
action 的格式如下,它需要有一个 type ,表示这个 action 要触发什么操作;payload 则表示这个 action 将要传递的数据

我们通过 dispatch 方法来发送一个 action

/*Action
Action 表示操作事件,可以是同步,也可以是异步
 {
  type: String,
  payload: data
}
*/
dispatch(Action);
dispatch({ type: 'todos/add', payload: 'Learn Dva' });

其实我们可以构建一个Action 创建函数,如下 

const  USER-LIST  = 'USER-LIST'//用户列表
//action 函数
function  user(data){
	return {type:MSG_READ ,payload:data}
}

dispatch(user(data))

3.dispatch 函数: type dispatch = (a: Action) => Action

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

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

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

Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。

//state的值
const initState = {
	users:{},//用户信息
}

//1 reducer 函数
export function chat(state=initState, action){
	switch(action.type){
		case USER_LIST:
			return {...state,users:action.payload.users}
		default:
		 	return state
	}
}

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,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。 

5.Effect

用于处理异步操作和业务逻辑,不直接修改 state,简单的来说,就是获取从服务端获取数据,并且发起一个 action 交给 reducer 的地方。基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写。

常见的操作:

1.put
用于触发 action 。
yield put({ type: 'todos/add', payload: 'Learn Dva' });
 
2.call
用于调用异步逻辑,支持 promise 。
const result = yield call(fetch, '/todos');
 
3.select
用于从 state 里获取数据。
const todos = yield select(state => state.todos);

简单的理解Redux-Saga

6.subscription

subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start() 时被执行,数据源可以是当前的时间、当前页面的url、服务器的 websocket 连接、history 路由变化等等。

一般格式是:

subscriptions: {
    setup({ dispatch, history }) { 
		
    },
},

异步数据初始化
比如:当用户进入 /users 页面时,触发 action users/fetch 加载用户数据。 

app.model({
  subscriptions: {
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        if (pathname === '/users') {
          dispatch({
            type: 'users/fetch',
          });
        }
      });
    },
  },
});

 键盘事件

import key from 'keymaster';
...
app.model({
  namespace: 'count',
  subscriptions: {
    keyEvent({dispatch}) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
});

(3).Router   表示路由配置信息

项目中的 router.js。

这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。

Route Component 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据

import { connect } from 'dva';

function App() {
  return <div>App</div>;
}

function mapStateToProps(state) {
  return { todos: state.todos };
}

export default connect(mapStateToProps)(App);

三.Dva API

官网.

1.app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)​

opts 包含:
    history:指定给路由用的 history,默认是 hashHistory      关于react-router中的hashHistorybrowserHistory的区别大家可以看:react-router
    initialState:指定初始数据,优先级高于 model 中的 state,默认是 {},但是基本上都在modal里面设置相应的state。
如果要配置 history 为 browserHistory,可以这样:

import createHistory from 'history/createBrowserHistory';
const app = dva({
  history: createHistory(),
});

另外,出于易用性的考虑,opts 里也可以配所有的 hooks ,下面包含全部的可配属性:

const app = dva({
  history,
  initialState,
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
});

 2.app.use(Hooks):配置 hooks 或者注册插件。  

这里最常见的就是dva-loading插件的配置,就是引入第三方插件的时候使用

import createLoading from 'dva-loading';
...
app.use(createLoading(opts));

3.app.model()

model 是 dva 中最重要的概念。 这个是你数据逻辑处理,数据流动的地方。

(1).5个属性

A.namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 . 的方式创建多层命名空间。
B.state
初始值,优先级低于传给 dva() 的 opts.initialState。

const app = dva({
  initialState: { count: 1 },
});
app.model({
  namespace: 'count',
  state: 0,
});
此时,在 app.start() 后 state.count 为 1 。

同上面
C.reducer
D.Effect
E.subscription

总结:

Model 对象的属性

  1. namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
  2. state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
  3. reducers: Action 处理器,处理同步动作,用来算出最新的 State
  4. effects:Action 处理器,处理异步动作

4.app.unmodel(namespace)   取消 model 注册

5.app.router(({ history, app }) => RouterConfig)
注册路由表,我们做路由跳转的地方。一般是这样写:

import { Router, Route } from 'dva/router';
app.router(({ history }) => {
  return (
    <Router history={history}>
      <Route path="/" component={App} />
    <Router>
  );
});

推荐把路由信息抽成一个单独的文件,这样结合 babel-plugin-dva-hmr 可实现路由和组件的热加载。

比如:app.router(require('./router'));

但是如果你的项目特别的庞大,我们就要考虑到相应的性能的问题,就要router按需加载的写法。 

6.app.start(selector)

启动应用,项目跑起来
app.start('#root');

四.安装插件

1.使用 antd

A.antd 和 babel-plugin-import 。babel-plugin-import 是用来按需加载 antd 的脚本和样式的,

npm install antd babel-plugin-import --save

B.编辑 .webpackrc,使 babel-plugin-import 插件生效。

{
  "extraBabelPlugins": [
    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
  ]
}

2.安装antd-mobile

npm install antd-mobile --save 

直接引用css就好:import 'antd-mobile/dist/antd-mobile.css'

按需加载:

安装babel-plugin-import:   cnpm install babel-plugin-import --save

找到根目录下的.webpackrc文件,并在该文件中添加以下代码

{
	"extraBabelPlugins": [
	    ["import", { "libraryName": "antd-mobile", "style": "css" }]
	]
}

五.操作

官网:操作         上手快速

一.路由定义:router.js
	import React from 'react';
	import { Router, Route, Switch } from 'dva/router';
	import User from './routes/user';

	function RouterConfig({ history }) {
		return (
			<Router history={history}>
			  	<Switch>
			    	<Route path="/user" exact component={User} />
			  	</Switch>
			</Router>
		);
	}

	export default RouterConfig;
	浏览器访问:http://localhost:8000/#/user


二.定义 model/user:设置state的值

   export default {
	  	namespace: 'user',
	  	state: {
		    list: [],
		    total: null,
		    loading: false, // 控制加载状态
			current: null, // 当前分页信息
			currentItem: {}, // 当前操作的用户对象
		},
	}

三.页面组件定义:
1. routes/user
	import React from 'react';
	import { connect } from 'dva';
	import styles from './user.css';
	import UserList from '../components/users/userList';
	import UserSearch from '../components/users/userSearch';
	import UserModal from '../components/users/userModal';

	function User() {
		const userSearchProps = {};
		const userListProps = {};
		const userModalProps = {};

		return (
			<div className={styles.normal}>
			   {/* 用户筛选搜索框 */}
				<UserSearch {...userSearchProps} />
				{/* 用户信息展示列表 */ }
				<UserList {...userListProps} />
				{/* 添加用户 & 修改用户弹出的浮层 */ }
				<UserModal {...userModalProps} />
			</div>
		);
	}


	export default connect()(User);

2.完成component页面:userList  userSearch userModal
	import React, { PropTypes } from 'react';
	import { Table, message, Popconfirm } from 'antd';

	const UserList = ({total,current,loading,dataSource}) => {
		const columns=[{}];
		const pagination = {};
		return (
			<div>
		      	<Table  columns={columns}
			        dataSource={dataSource}
			        loading={loading}
			        rowKey={record => record.id}
			        pagination={pagination} />
		    </div>
		);
	}

	export default userList;


注意:routes可以简单的理解是页面组件   components可以理解是模块组件, 
 routes里面引用多个components的组件


四.更新state:—》model/user里面完成 reducers  计算新的state

	export default {
	  	namespace: 'user',
		reducers: {
		     // 使用静态数据返回
		    querySuccess(state){
		      	const mock = {
			        total: 3,
			        current: 1,
			        loading: false,
			        list: [
						{
							id: 1,
							name: '张三',
							age: 23,
							address: '成都',
						},
			        ]
			    };
		      	return {...state, ...mock, loading: false};
		    },
		    createSuccess(){},
	    	deleteSuccess(){},
	    	updateSuccess(){},
		}
	}

五.关联Model数据:把model中计算的新state的值  关联到组件 
    =》返回routes/user 

	import { connect } from 'dva';
	function User({ location, dispatch, user }) {
		//Model中state 赋值到 { loading, list, total, current,currentItem, modalVisible, modalType}
		const {
		    loading, list, total, current,
		    currentItem, modalVisible, modalType
		} = user;

		const userListProps = {
			dataSource: list,
			total,
			loading,
			current,
		};		
		const userSearchProps = {};
		const userModalProps = {};

		return (
			<div className={styles.normal}>
			   {/* 用户筛选搜索框 */}
				<UserSearch {...userSearchProps} />
				{/* 用户信息展示列表 */ }
				<UserList {...userListProps} />
				{/* 添加用户 & 修改用户弹出的浮层 */ }
				<UserModal {...userModalProps} />
			</div>
		);
	}
    //指定订阅数据,这里关联了model中 user
	function mapStateToProps({ user }) {
	  return {user};
	}
	//建立数据关联关系
	const User = connect(mapStateToProps)(User);
	export default  User;

注意:1.记得 index.js 入口文件处要要关联model :app.model(require('./models/user')); 否则获取不到数据

2.数据关联以后,就可以通过 props 访问到 model 的数据了,
而 UserList 展示组件的数据,也是 routes/user 组件 通过 props 传递的过来的。

六.数据关联后,就要更新reducer的数据 :model/user
调用 reducers呢,就是需要发起一个 action。  这边可以使用Subscription 订阅 监听路由的改变
subscriptions: {
	    setup({ dispatch, history }) {
		    history.listen(location => {
		        if (location.pathname === '/user') {
		          	dispatch({
		            	type: 'querySuccess',
		            	payload: {}
		          	});
		        }
		    });
	    },
  	},


七.异步处理:返回model  effects: {...}的操作

    	import { hashHistory } from 'dva/router';
	import { query } from '../services/user';
	export default {
		namespace: 'user',
		reducers: {
			showLoading(state, action){
				return { ...state, loading: true };
			},// 控制加载状态的 reducer
			showModal(){}, // 控制 Modal 显示状态的 reducer
			hideModal(){},
			// 使用服务器数据返回
			querySuccess(state, action){
			  return {...state, ...action.payload, loading: false};
			},
		},
		effects: {
			*query({ payload }, { select, call, put }) {
				yield put({ type: 'showLoading' });
				const { data } = yield call(query);
				if (data) {
					yield put({
						type: 'querySuccess',
						payload: {
							list: data.data,
							total: data.page.total,
							current: data.page.current
						}
					});
				}
			},
		}
	}

八.后端数据请求
1.代理 
 在.webpackrc 文件里面加
"proxy": {
	  	"/api": {
	    	"target": "http://localhost:8080",
	    	"changeOrigin": true,
	    	"pathRewrite": { 
	    		"^/api" : "" 
	    	}
	  	}
	},

或是  安装  roadhorg    在.roadhogrc.js里面做代理

2.在services/user 做交互   

	/*与后台系统的交互)
	request 是我们封装的一个网络请求库
	qs是一个npm仓库所管理的包  对请求返回的数据进行处理
*/
	import request from '../utils/request';
	import qs from 'qs';

	export async function query(params) {
	  return request(`/api/user?${qs.stringify(params)}`);
	}

执行上面8步骤就差不多

六.补充

(一).dva 2.0中如何使用代码进行路由跳转

[email protected] 让路由变得更简单,最大特点就是可以路由嵌套。由于 dva 将react-router-domreact-router-redux都封装到了 dva/router 中,在使用 [email protected] 和 redux 里面的东西时只需引入 dva/router 这个包即可。[email protected] 文档 API

dva 中使用 router4.0

router.js:

import React from 'react';
import { Router, Route, Switch,routerRedux } from 'dva/router';
import BasicLayout from '../layouts/BasicLayout'


const { ConnectedRouter } = routerRedux;
function RouterConfig({ history }) {
	return (
		<ConnectedRouter history={history}>
        	<Route path="/" component={BasicLayout} />
      	</ConnectedRouter>
	);
}

export default RouterConfig;

Route 为 react-router-dom 内的标签
ConnectedRouter 为 react-router-redux 内的对象 routerRedux 的标签,作用相当于 react-router-dom 中的 BrowserRouter 标签,作用为连接 redux 使用
 

1.路由跳转

引入 dva/router,使用 routerReux 对象的 push 方法控制,值为要跳转的路由地址,与根目录下 router.js 中配置的路由地址是相同的。routerReux 就是上面 dva/router 第二个导出的 react-router-redux 包对象。

此处示例为跳转到 /user 路由。

// models > app.js
import { routerRedux } from 'dva/router';

export default {
  // ...
  effects: {
      // 路由跳转
      * redirect ({ payload }, { put }) {
        yield put(routerRedux.push('/user'));
      },
  }
  // ...
}

2.携带参数
有时路由的跳转还需要携带参数。

3.传参:
routerRedux.push 方法的第二个参数填写参数对象。此处示例表示跳转到 /user 路由,并携带参数 {name: 'dkvirus', age: 20}。

// models > app.js
import { routerRedux } from 'dva/router';

export default {
  // ...
  effects: {
      // 路由跳转
      * redirect ({ payload }, { put }) {
        yield put(routerRedux.push('/user', {name: 'dkvirus', age: 20}));
      },
  }
  // ...
}

接收参数: 

// models > user.js
export default {
  subscriptions: {
    /**
     * 监听浏览器地址,当跳转到 /user 时进入该方法
     * @param dispatch 触发器,用于触发 effects 中的 query 方法
     * @param history 浏览器历史记录,主要用到它的 location 属性以获取地址栏地址
     */
    setup ({ dispatch, history }) {
      history.listen((location) => {
        console.log('location is: %o', location);
        console.log('重定向接收参数:%o', location.state)
        // 调用 effects 属性中的 query 方法,并将 location.state 作为参数传递 
        dispatch({
          type: 'query',
          payload: location.state,
        })
      });
    },
  },
  effects: {
    *query ({ payload }, { call, put }) {
       console.log('payload is: %o', payload);
    }
  }
  // ...
}

在 user.js 中 subscriptions 属性会监听路由。当 app.js 中通过代码跳转到 /user 路由,models>user.js>subscriptions 属性中的 setup 方法会被触发,location 记录着相关信息。打印如下:

location is: Object
    hash: ""
    key: "kss7as"
    pathname: "/user"
    search: ""
    state: {name: "bob", age: 21}
重定向接收参数:Object
    age:21
    name:"bob"

 可以看到 location.state 就是传递过来的参数。在 subscriptions 中可以使用 dispatch 触发 effects 中的方法同时传递参数。

(三). 解决组件动态加载问题的 util 方法

import dynamic from 'dva/dynamic';

opts 包含:

  • app: dva 实例,加载 models 时需要
  • models: 返回 Promise 数组的函数,Promise 返回 dva model
  • component:返回 Promise 的函数,Promise 返回 React Component

使用如下:

mport dynamic from 'dva/dynamic'

{
routeArr.map((item, key) => {
	return <Route key={key} exact path={item.path} component={dynamic({ //保证路由的唯一性 exact key
		app,
		model: item.models,
		component: item.component,
	})} />
})

}

(四).React.Component与React.PureComponent的区别


React15.3中新加了一个 PureComponent 类,顾名思义, pure 是纯的意思, PureComponent 也就是纯组件,取代其前身 PureRenderMixin , PureComponent 是优化 React 应用程序最重要的方法之一,易于实施,只要把继承类从 Component 换成 PureComponent 即可,可以减少不必要的 render操作的次数,从而提高性能,而且可以少写 shouldComponentUpdate 函数,节省了点代码。

    React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent 通过prop和state的浅对比来实现 shouldComponentUpate()
如果React组件的 render() 函数在给定相同的props和state下渲染为相同的结果,在某些场景下你可以使用 React.PureComponent 来提升性能

七.参考

  1. Dva-React 应用框架在蚂蚁金服的实践
  2.   dva.js 知识导图     
  3. redux-saga   
  4.  UMI       
  5. dva理论到实践——帮你扫清dva的知识盲点      
  6. History API   
  7. redux docs 中文
  8. roadhog介绍
  9. 基于dva-cli&antd的react项目实战
  10. 10分钟 让你dva从入门到精通
  11. 上手快速
  12. async 函数的含义和用法
  13. 基于 dva 创建 antd-mobile 的项目

八.案例

1.后台管理系统

原文链接:加载失败,请重新获取