前言:
本文分为2个部分:
1.前半部分为5个前置知识点,简要介绍了React中的props和state、dva中connect、dispatch方法
2.后半部分从一个项目实例入手介绍react+dva前后端传值的过程,涉及models、action、view、service。
Tips:配合官网看例子效果更好:
react官网:https://reactjs.org/
dva-github:dvajs/dva
1.React中的props和state的区别
参考:https://segmentfault.com/q/1010000008340434
在任何应用中,数据都是必不可少的。我们需要直接的改变页面上一块的区域来使得视图的刷新,或者间接地改变其他地方的数据。React的数据是自顶向下单向流动的,即从父组件到子组件中,组件的数据存储在props和state中,这两个属性有啥子区别呢?
props
React的核心思想就是组件化思想,页面会被切分成一些独立的、可复用的组件。
组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是props,所以可以把props理解为从外部传入组件内部的数据。由于React是单向数据流,所以props基本上也就是从服父级组件向子组件传递的数据。
用法
假设我们现在需要实现一个列表,根据React组件化思想,我们可以把列表中的行当做一个组件,也就是有这样两个组件:和。
先看看
import Item from “./item”;
export default class ItemList extends React.Component{
const itemList = data.map(item => <Item item=item />);
render(){
return (
{itemList}
)
}
}
列表的数据我们就暂时先假设是放在一个data变量中,然后通过map函数返回一个每一项都是的数组,也就是说这里其实包含了data.length个组件,数据通过在组件上自定义一个参数传递。当然,这里想传递几个自定义参数都可以。
具体是这样的:
export default class Item extends React.Component{
render(){
return (
<li>{this.props.item}</li>
)
}
}
在render函数中可以看出,组件内部是使用this.props来获取传递到该组件的所有数据,它是一个对象,包含了所有你对这个组件的配置,现在只包含了一个item属性,所以通过this.props.item来获取即可。
只读性
props经常被用作渲染组件和初始化状态,当一个组件被实例化之后,它的props是只读的,不可改变的。如果props在渲染过程中可以被改变,会导致这个组件显示的形态变得不可预测。只有通过父组件重新渲染的方式才可以把新的props传入组件中。
默认参数
在组件中,我们最好为props中的参数设置一个defaultProps,并且制定它的类型。比如,这样:
Item.defaultProps = {
item: ‘Hello Props’,
};
Item.propTypes = {
item: PropTypes.string,
};
关于propTypes,可以声明为以下几种类型:
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
注意,bool和func是简写。
这些知识基础数据类型,还有一些复杂的,附上链接:
https://facebook.github.io/react/docs/typechecking-with-proptypes.html
总结
props是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的props来重新渲染子组件,否则子组件的props以及展现形式不会改变。
state
state是什么呢?
- State is similar to props, but it is private and fully controlled by the component.
一个组件的显示形态可以由数据状态和外部参数所决定,外部参数也就是props,而数据状态就是state。
export default class ItemList extends React.Component{
constructor(){
super();
this.state = {
itemList:‘一些数据’,
}
}
render(){
return (
{this.state.itemList}
)
}
}
首先,在组件初始化的时候,通过this.state给组件设定一个初始的state,在第一次render的时候就会用这个数据来渲染组件。
setState
state不同于props的一点是,state是可以被改变的。不过,不可以直接通过this.state=的方式来修改,而需要通过this.setState()方法来修改state。
比如,我们经常会通过异步操作来获取数据,我们需要在didMount阶段来执行异步操作:
componentDidMount(){
fetch(‘url’)
.then(response => response.json())
.then((data) => {
this.setState({itemList:item});
}
}
当数据获取完成后,通过this.setState来修改数据状态。当我们调用this.setState方法时,React会更新组件的数据状态state,并且重新调用render方法,也就是会对组件进行重新渲染。
注意:
通过this.state=来初始化state,使用this.setState来修改state,constructor是唯一能够初始化的地方。setState接受一个对象或者函数作为第一个参数,只需要传入需要更新的部分即可,不需要传入整个对象,比如:
export default class ItemList extends React.Component{
constructor(){
super();
this.state = {
name:‘axuebin’,
age:25,
}
}
componentDidMount(){
this.setState({age:18})
}
}
在执行完setState之后的state应该是{name:’axuebin’,age:18}。
setState还可以接受第二个参数,它是一个函数,会在setState调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成:
this.setState({
name:‘xb’
},()=>console.log(‘setState finished’))
总结
state的主要作用是用于组件保存、控制以及修改自己的状态,它只能在constructor中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的this.setState来修改,修改state属性会导致组件的重新渲染。
区别
state是组件自己管理数据,控制自己的状态,可变;
props是外部传入的数据参数,不可变;
没有state的叫做无状态组件,有state的叫做有状态组件;
多用props,少用state。也就是多写无状态组件。
2.Object.assign方法
参考:https://github.com/ruanyf/es6tutorial/blob/06fac98bb11b0b1f3fccd396d402e918ae3fc002/docs/object.md
基本用法
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target =``{ a:``1, b:``1``};
const source1 =``{ b:``2, c:``2``};
const source2 =``{ c:``3``};
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果只有一个参数,Object.assign会直接返回该参数。
const obj =``{a:``1};
Object.assign(obj)``=== obj // true
注意点
(1)浅拷贝
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 =``{a:``{b:``1}};
const obj2 =``Object.assign({}, obj1);
obj1.a.b =``2;
obj2.a.b // 2
上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
(2)同名属性的替换
对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
-
const target =
{ a:
{ b:
*‘c’, d:*``*‘e’*``*}*``*}*
-
const source =
*{ a:*``*{ b:*``*‘hello’*``*}*``*}*
Object.assign(target, source)
// { a: { b: ‘hello’ } }
上面代码中,target对象的a属性被source对象的a属性整个替换掉了,而不会得到{ a: { b: ‘hello’, d: ‘e’ } }的结果。这通常不是开发者想要的,需要特别小心。
一些函数库提供Object.assign的定制版本(比如 Lodash 的_.defaultsDeep方法),可以得到深拷贝的合并。
(3)数组的处理
Object.assign可以用来处理数组,但是会把数组视为对象。
-
Object.assign([1,
2,
3],
[4,
5])
// [4, 5, 3]
上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。
(4)取值函数的处理
Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
-
const source =
{
-
get foo()
{
return
1
}
};
-
const target =
{};
Object.assign(target, source)
// { foo: 1 }
上面代码中,source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,只会拿到值以后,将这个值复制过去。
如果该参数不是对象,则会先转成对象,然后返回。
3.ES6拓展运算符
参考:http://es6.ruanyifeng.com/#docs/object#对象的拓展运算符
扩展运算符
对象的扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let z =``{ a:``3, b:``4``};
let n =``{``…z };
n // { a: 3, b: 4 }
这等同于使用Object.assign方法。
let aClone =``{``…a };
// 等同于
let aClone =``Object.assign({}, a);
4.dva connect函数
参考:
https://dvajs.com/guide/introduce-class.html#connect-%E6%96%B9%E6%B3%95
connect 方法
connect 是一个函数,绑定 State 到 View。
import``{ connect } from ‘dva’;
function mapStateToProps(state)``{
return``{ todos: state.todos };
}
connect(mapStateToProps)(App);
connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。
connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系。
5.dispatch方法
dispatch 方法
dispatch 是一个函数方法,用来将 Action 发送给 State。
dispatch({
type:``‘click-submit-button’,
payload:``this.form.data
})
dispatch 方法从哪里来?被 connect 的 Component 会自动在 props 中拥有 dispatch 方法。
前后端传值示例:
先看一下项目目录:
1.Index——index.js
import dva from ‘dva’;
import ‘./index.less’;
import ‘./todolist.css’;
import createLoading from ‘dva-loading’
import ‘moment/locale/zh-cn’
import {message, notification} from ‘antd’
import roleManage from ‘./application/models/roleManager’
import themeConfig from ‘./application/models/themeManager’
// 1\. Initialize
const app = dva({
onError(e) {
// notification.error({
// description: e.message,
// })
},
});
app.use(createLoading());
// 2\. Plugins
// dbmanager.use({});
// 3\. Model
app.model(require(‘./application/models/userManager’))
….
app.model(require(‘./application/models/todoList’))
// 4\. Router
app.router(require(‘./router’))
// 5\. Start
app.start(‘#root’);
在index.js中定义了model,dva 提供 app.model 这个对象,所有的应用逻辑都定义在它上面。
2.Model——userManager.js
说直白点,model就是模型,定义了同步或者异步的操作,通常是前后端传值的操作。在model中引入service,通过调用service的具体方法完成post/get请求等前后端传值。
userManager.js如下:
import as service from “../services/userManager”;
import alert from ‘../../framework/common/alert’
export default {
namespace: ‘userManager’,
state: {
userList:[],
roleList: [],
spinning: true,
},
reducers: {
save(state, action) {
return {…state, …action.payload};
},
},
effects: {
queryUserList({payload}, {call, put}) {
yield put({type: ‘save’, payload: {spinning: true}})
const {data} = yield call(service.queryUserList)
if (data.code == ‘0’) {
alert(‘查询用户’, data)
} else {
yield put({type: ‘save’, payload: {userList: data.payload}})
}
},
addUser({payload}, {call, put}){
const {data} = yield call(service.addUser, payload)
if (data.code == ‘0’) {
alert(‘添加人员’, data)
}
yield put({type: ‘queryUserList’})
},
update({payload}, {call, put}){
const {data} = yield call(service.update, payload)
if (data.code == ‘0’) {
alert(‘修改人员’, data)
}
yield put({type: ‘queryUserList’})
},
deleteUser({payload}, {call, put}){
const {data} = yield call(service.deleteUser, payload)
if (data.code == ‘0’) {
alert(‘修改人员’, data)
}
yield put({type: ‘queryUserList’})
},
queryAllRole({payload}, {call, put}) {
const {data} = yield call(service.queryAllRole)
if (data.code == ‘0’) {
alert(‘查询角色’, data)
}
yield put({type: ‘save’, payload: {roleList: data.payload}})
},
},
subscriptions: {
setup({dispatch, history}) {
},
},
}
Model 对象的属性:
namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
reducers: Action 处理器,处理同步动作,用来算出最新的 State
effects:Action 处理器,处理异步动作
参考:https://dvajs.com/guide/introduce-class.html
3.Service——userManager.js
Service类似Java中的Service,不过这里不用写接口,直接就是实现类了,在Service中直接调用方法和后端进行数据交互。
import axios from ‘axios’
export async function queryUserList() {
return axios.get(‘userManager/queryUserList’)
}
export async function addUser(payload) {
return axios.post(‘userManager/addUser’,payload)
}
export async function update(payload) {
return axios.post(‘userManager/update’,payload)
}
export async function deleteUser(payload) {
return axios.post(‘userManager/deleteUser’,payload)
}
export async function queryAllRole() {
return axios.post(‘/role/queryAllRole’)
}
4.Router——router.js
import React from “react”;
import {Route, Router, IndexRoute} from “dva/router”
import UserManager from ‘./application/views/UserManager/index’
import Todolist from‘./application/views/TodoList’
function RouterConfig({history, app}) {
return (
<Router history={history}>
<Route path=“/“ component={NavLayout}>
<IndexRoute component={UserManager}/>
<Route path=“userManager” component={UserManager}/>
//…
<Route path=“todolist” component={Todolist}/>
</Route>
</Router>
)
}
export default RouterConfig
router是路由,router.js文件定义了路径path和对应的跳转页面,类似Java中的Controller。如:
Route path=”userManager” component={UserManager}/>
component是组件的意思,即相应path需要跳转到的页面。userManager组件在开头已经通过import引入了:
import UserManager from ‘./application/views/UserManager/index’
当访问http://localhost:8080/userManager 时会自动找到UserManager组件,(位于application/views/UserManager文件夹下的index.js文件)然后将其渲染成页面。
PS:到这里其实我们知道接下来要来到这个userManager组件所在的页面了。那之前1.2.3.步中的index和model和service有什么用呢?暂时没有用,因为这些都是为了后面前后台数据交互(传值)用的。
5.View——index.js
View顾名思义是视图层,视图层就是页面,在react中即组件,通常一个页面组件是顶层组件,也就是一个view,一个页面组件中通常会包裹其他各种小组件。截图如下:
里面主要包含各种react周期函数和自定义的各种方法,最后通过render()来渲染出页面的html。
以上就是页面的流通过程,下面结合具体页面操作来过一遍前后端传参过程。
1.进入http://localhost:8080/userManager 页面:
其中import头部如下:
点击新增按钮,对应View——index.js中render()方法中如下元素:
<Button onClick={()``=>{this.setState({modalDisplay:``“add”})}}
type=“primary” style={{borderRadius:``15, width:``100}}>``新增``</Button>
react中一切皆组件,这个button也是一个组件,里面包含onClick、type、style属性。这里的button并不是自定义的组件,而是是引用自antd的开源组件
PS: antd是蚂蚁金服开源的基于react的前端UI组件库具体可以参考官网:https://ant.design/components/icon-cn/)
button常用属性如下:
2.onClick触发箭头函数→this.setState({modalDisplay: “add”})
this.setState是react中的语法,作用是改变当前组件的state。这时,看一下整个页面初始化构造器中其实是在state中定义了modalDisplay 的:
constructor(props) {
super(props)
this.state = {
userId: window.sessionStorage.getItem(“userId”),
modalDisplay: “none”,
selectUser: null,
modalVisible: false
}
}
通过给modalDisplay赋值,modalDisplay从null变为了”add”,组件的state改变会触发相应的元素重新渲染,那么对应的就是页面组件中的Modal组件:
<Modal title={selectUser ? “编辑用户” : “添加用户”} visible={modalDisplay === “none” ? false : true}
onCancel={() => this.handleCancel() }
cancelText=“关闭”
okText=“确定”
onOk={() => this.editUser()}>
Modal和button组件一样,都是antd组件,当此组件捕获到整个state改变后,(modalDisplay从none变为”add”),Modal的visible属性变为true,故此Modal被渲染了出来,效果是如下弹框:
3.前面只是铺垫,只是前端页面的点击和渲染,还没涉及到前后端数据交互,从Modal组件上点击【确定】时,正式开始前后端传参过程。Modal组件上点击【确定】调用组件的editUser()方法。
/编辑用户/
editUser() {
const {selectUser} = this.state
const {form, dispatch} = this.props
const {validateFields, resetFields} = form
validateFields((errors, values) => {
if (errors) {
return
}
if (!selectUser) {
values.userId = this.state.userId
dispatch({type: ‘userManager/addUser’, payload: values})
resetFields()
this.setState({modalDisplay: “none”})
} else {
values.userId = this.state.userId
dispatch({type: ‘userManager/update’, payload: values})
resetFields()
this.setState({modalDisplay: “none”, selectUser: null})
}
})
}
selectUser是页面组件初始化constructor中定义的state属性,初始为null。在editUser()中先判断selectUser,如果为空则执行:
dispatch({type: ‘userManager/update’, payload: values})
不为空则执行:
dispatch({type: ‘userManager/addUser’, payload: values})
这里,dispatch是dva中定义的函数,用来将 Action 发送给 State。Action 是一个普通 javascript对象,它是改变 State 的唯一途径。一切触发state改变的行为都可以叫Action,此处Action即为:{type: ‘userManager/update’, payload: values}
PS:被 connect 的 Component 会自动在 props 中拥有 dispatch 方法。这里页面组件在开始已经做了connect:
@connect(state =>``Object.assign({}, state.userManager,``{loading: state.loading.models.userManager}))
这个connect看起来挺复杂的,用到了箭头函数、Object.assign()方法。但是其原理很简单,就是绑定state到view。此处即绑定当前组件的state到models下的userManager.js(view)
4.此处以dispatch({type: ‘userManager/addUser’, payload: values})
为例看一下添加用户的前后端交互流程。
state改变后触发contact绑定过的models中userManager.js中addUser方法:
effects: {
addUser({payload}, {call, put}){
const {data} = yield call(service.addUser, payload)
if (data.code == ‘0’) {
alert(‘添加人员’, data)
}
yield put({type: ‘queryUserList’})
},
…
}
在这里call和put方法都是effect内部常用的处理函数。
call:执行异步函数,这里通过call方法调用service来实现向后台传值,
put:发出一个 Action,类似于 dispatch,这里通过put一个Action:{type: ‘queryUserList’}来改变前台state状态。
5.service.addUse
这里就是前台页面向后端传值的最后一个步骤.