Springboot+react+dva入门实战—前后端传值

8.jpg

前言:

本文分为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方法的第一个参数是目标对象,后面的参数都是源对象。

注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

  1. const target =``{ a:``1, b:``1``};
  2. const source1 =``{ b:``2, c:``2``};
  3. const source2 =``{ c:``3``};
  4. Object.assign(target, source1, source2);
  5. target // {a:1, b:2, c:3}

如果只有一个参数,Object.assign会直接返回该参数。

  1. const obj =``{a:``1};
  2. Object.assign(obj)``=== obj // true

注意点

(1)浅拷贝

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

  1. const obj1 =``{a:``{b:``1}};
  2. const obj2 =``Object.assign({}, obj1);
  3. obj1.a.b =``2;
  4. obj2.a.b // 2

上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

(2)同名属性的替换

对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

  1. const target ={ a:{ b:*‘c’, d:*``*‘e’*``*}*``*}*
  2. const source =*{ a:*``*{ b:*``*‘hello’*``*}*``*}*
  3. Object.assign(target, source)
  4. // { a: { b: ‘hello’ } }

上面代码中,target对象的a属性被source对象的a属性整个替换掉了,而不会得到{ a: { b: ‘hello’, d: ‘e’ } }的结果。这通常不是开发者想要的,需要特别小心。

一些函数库提供Object.assign的定制版本(比如 Lodash 的_.defaultsDeep方法),可以得到深拷贝的合并。

(3)数组的处理

  1. Object.assign可以用来处理数组,但是会把数组视为对象。
  2. Object.assign([1,2,3],[4,5])
  3. // [4, 5, 3]

上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。

(4)取值函数的处理

Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

  1. const source ={
  2. get foo(){return1}
  3. };
  4. const target ={};
  5. Object.assign(target, source)
  6. // { foo: 1 }

上面代码中,source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,只会拿到值以后,将这个值复制过去。
如果该参数不是对象,则会先转成对象,然后返回。

3.ES6拓展运算符

参考:http://es6.ruanyifeng.com/#docs/object#对象的拓展运算符
扩展运算符
对象的扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

  1. let z =``{ a:``3, b:``4``};
  2. let n =``{``…z };
  3. n // { a: 3, b: 4 }

这等同于使用Object.assign方法。

  1. let aClone =``{``…a };
  2. // 等同于
  3. 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。

  1. import``{ connect } from ‘dva’;
  2. function mapStateToProps(state)``{
  3. return``{ todos: state.todos };
  4. }
  5. connect(mapStateToProps)(App);

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

5.dispatch方法

dispatch 方法

dispatch 是一个函数方法,用来将 Action 发送给 State。

  1. dispatch({
  2. type:``‘click-submit-button’,
  3. payload:``this.form.data
  4. })

dispatch 方法从哪里来?被 connect 的 Component 会自动在 props 中拥有 dispatch 方法。


前后端传值示例:

先看一下项目目录:

image

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,一个页面组件中通常会包裹其他各种小组件。截图如下:

image

里面主要包含各种react周期函数和自定义的各种方法,最后通过render()来渲染出页面的html。

以上就是页面的流通过程,下面结合具体页面操作来过一遍前后端传参过程。
1.进入http://localhost:8080/userManager 页面:

image

其中import头部如下:

image

点击新增按钮,对应View——index.js中render()方法中如下元素:

  1. <Button onClick={()``=>{this.setState({modalDisplay:``“add”})}}
  2. 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常用属性如下:

image

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被渲染了出来,效果是如下弹框:

image

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:

  1. @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
这里就是前台页面向后端传值的最后一个步骤.

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容

  • 个人笔记, 转载请注明转载自 szhshp的第三边境研究所 Refs and the DOM In the t...
    szhielelp阅读 1,467评论 0 1
  • 3. JSX JSX是对JavaScript语言的一个扩展语法, 用于生产React“元素”,建议在描述UI的时候...
    pixels阅读 2,814评论 0 24
  • 前言 组件中的state具体是什么?怎么更改state的数据? setState函数分别接收对象以及函数有什么区别...
    itclanCoder阅读 875评论 0 0
  • 今天来看一下react组件之间是怎么进行通讯的。react推崇的是单向数据流,自上而下进行数据的传递,但是由下而上...
    亲亲qin阅读 6,003评论 2 12
  • HTML模版 之后出现的React代码嵌套入模版中。 1. Hello world 这段代码将一个一级标题插入到指...
    ryanho84阅读 6,227评论 0 9