文章目录
前言
本文章主要对 @connect 做了一个简单介绍分析,和使用注意点,以及其他一些 涉及到 dva 相关的知识点,对于更详细的知识点了解,附上了文档参考链接
引入
首先我们来通过一个例子,引出我们这里要描述的内容,假设现在我们需要对书店中书籍产品页面通过点击按钮更新获取书籍产品列表信息,文件涉及目录结构
routes文件下的 ProductPage.js 产品页面
import React, { Component } from 'react';
import Product from '../../components/Product.js';
import { connect } from 'dva';
@connect(({ product: { productList, innerText }, loading }) => ({
productList,
innerText,
}))
export default class ProductPage extends Component {
render () {
console.log()
let { productList, dispatch, innerText } = this.props;
return (
<div>
<Product dispatch={dispatch} title='hahah' productList={productList} />
{innerText}
</div>
)
}
}
子页面 Product.jsx
import React from 'react';
import { withRouter } from 'dva/router'
import { connect } from 'dva';
class Product extends React.Component {
constructor(props) {
super(props);
}
addProduct = () => {
this.props.dispatch({
// 空间命名
type: 'product/updateList',
payload: {
name: '白夜行',
}
})
}
render () {
let { productList } = this.props
return (
<div>
product商品:
<ul>
{productList.map((ele, index) => {
return <li key={index}>{ele.name}</li>
})}
</ul>
<!--- 点击添加商品 --->
<button onClick={this.addProduct}>添加商品</button>
</div>
)
}
}
export default withRouter(Product)
models 文件夹下的 product.js
import * as api from '../services/product'
export default {
namespace: 'product',
state: {
productList: [
{
name: '秘密'
},
{
name: '变身'
}
],
innerText: 'hello'
},
subscriptions: {},
// 异步操作
effects: {},
reducers: {
// 更新数据
updateList (state, action) {
// action 参数
let currentProductList = deepClone(state);
currentProductList.productList.push(action.payload)
return currentProductList;
},
}
}
// 深度拷贝
function deepClone (arr) {
let _obj = JSON.stringify(arr),
objClone = JSON.parse(_obj)
return objClone
}
-
在还没有点击按钮更新时,页面展示
我们发现在组件页面中我们并没有给到初始值,父子组件都是无状态的组件,那么,初始默认值(秘密,变身,hello)是哪里来???
父组件 ProductPage 中的 props 中的 productList 是怎么来的继而传给子组件 Product 的????这里我们不难发现,只有 models 文件夹下的 product.js 中我们有初始化值 (秘密,变身,hello),可是我们发现组件页面中并没有对该文件有相关的引入使用,那么是怎么进行内容链接的呢???
最终我们发现 ProductPage 父组件中 有以下这段代码涉及到了 productList 和 innerText 的传入,发现是通过了 connect函数,,啊啊啊啊,,,那么来了,来了,来了,@connect 是什么呢???
@connect(({ product: { productList, innerText }}) => ({ productList, innerText, }))
-
点击添加商品后,发现这里我们新添加了一个名叫白夜行的商品
通过点击添加商品,我们 发现我们是通过调用 addProduct 这个方法,来更新数据,但是方法中 dispatch 函数又是怎么来的呢??? 是如何使用,达到更新数据的呢???
希望你看完之后能找到答案!!!
@connect是什么呢???
@connect介绍
-
这里的@connect 是 connect 的语法糖:负责将 model 和 component(组件/视图)串联起来,用于将 model 中的 state 绑定到当前组件的 props(属性) 中,实现不同页面(或组件)的的数据共享,避免组件间一层层的嵌套传值
model
是dva
中最重要的概念,Model
非MVC
中的M
,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发
- 通过我们上面例子层面来说,它的作用就是用来连接我们的 model 和 组件页面的,就是将我们 models 下的 product.js 文件下的 state 绑定到我们 ProductPage.js 页面的 props 中的,这个时候我们就能拿到 productList,innerText 等东西了
话说 @connect 是 connect 的语法糖,那 connect 又是怎么个回事呢??往下看
connect介绍
-
connect
是一个高阶函数 -
其中
connect
函数会接收两个参数:mapStateToProps
和mapDispatchToProps
,他们都是函数。
-
第一个函数
mapStateToProps
,会将State
作为参数注入,返回的是对象,并将对象中 color 映射当前组件的属性。const mapStateToProps=state=> // 这里的函数名字可任意,与入参保持一致即可 ({ color:state.color })
-
第二个函数
mapDispatchToProps
,会将Store
的dispatch
函数作为参数注入,以便后续组件触发回调函数属性时调用。const mapDispatchToProps=dispatch=> // 这里的函数名字可任意,与入参保持一致即可 ({ onRemove(id){ dispatch(removerColor(id)) } })
- 它返回的函数将是一个组件,通常称为
容器组件
。因为它是原始 UI 组件的容器,在外面包了一层 State。 可以通过属性发送数据,达到创建组件访问Store
目的。
class ProductList extends Component{
...
}
const mapStateToProps=state=>
({
color:state.color
})
const mapDispatchToProps=dispatch=>
({
onRemove(id){
dispatch(removerColor(id))
}
})
export default connect(mapStateToProps,mapDispatchToProps)(ProductList)
实际开发中,常常需要写成下面这样
class ProductList extends Component{}
export default connect(mapStateToProps,mapDispatchToProps)(ProductList)
但是当我们通过 装饰器,可以改写以上的代码
@connect(mapStateToProps,mapDispatchToProps)
export default class ProductList extends Componect{}
这里为什么两者可以 相互改写,和装饰器分不开关系,下面会简单介绍下类的装饰相关知识!!!
类的装饰
代码中@ + 函数名是JavaScript中的装饰器。这里connect是一个高阶函数,@connect就是一个装饰器。装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。它可以放在类和类方法的定义前面,这里主要介绍放在类定义的前面的这种。
基本语法
@decorator
class A {}
// 等同于
class A {};
A = decorator(A) || A;
修饰器函数的第一个参数,默认就是所要修饰的目标类。下面请看官方的一个例子,作用为给类添加了一个isTestable属性。
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
如果觉得一个参数不够用,就可以在修饰器外面封装一层函数。
function testable(isTestable){
return function(target){
target.isTestable=isTestTable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable //true
通过上面这种方式,我们这里可以自己写一个connect实现,connect高阶函数,它的返回值是一个函数。
let state={
color:'#000'
}
function connect(f){
let obj=f(state)
return function(Com){ // 这里的 Com 就是默认的第一个参数目标组件
return class A extends Component {
render(){
return <Com {...obj}/> // 给目标组件添加传递过来的属性
}
}
}
}
@connect(state=>({color:state.color}))
class ProductList {
constructor(props){
super(props)
}
componentDidMount(){
console.log(this.props.color)
}
}
上面讲了,关于 connet 的相关知识点,那么具体和组件之间是是如何实现注入连接的呢,让我们接着往下看
connect的注入
定义模块区域 state 数据
-
由于
dva
是 基于 Redux,redux
的定义是所有的变量都存放在一个变量池store
中,所有的 state 都以一个对象树的形式储存在一个单一的store
中。dva
是根据namespace
对state
进行区分的(这里为model
下的 js 文件):这里换句话说, 就是你需要给不同的 涉及的需求业务做一个数据区分存储,这里也方面获取不是吗??值得思考。假设这里有两个需求业务那么,我们应该在 models 文件下定义唯一的一个 namespace,你应该看出来了,这个唯一的不一般呀,在后面获取方便我获取指定模块下的数据嘛。
// example.js
export default {
namespace:'example',
state: {
...
}
}
// product.js
export default {
namespace:'product',
state:{
...
}
}
建立连接
- 如下代码:指定了某一个
namespace
命名空间后,dva
会将该 namespace 下的state
注入到连接的组件中。 这里connect
中的 example 就是 namespace(空间名)
@connect((state)=>({example:state.example}))/@connect({example}) // 关于更多见 @connect 分析
@connect 中的箭头函数()=>{}中的 () 里的内容,这里对常见的几种传递方式做个介绍
- 可以是state——这里指全部的state数据,这个时候,你在 props 下能获取到这个 example 对象,即 包含了namespace 为 example 下的所有数据
@connect(( state )=>{ example => state.example }) // 传入全部的 state 数据,返回 example 命名空间下的 state 数据绑定在了对应组件的参数中
//对应组件中想要获取 example 下的数据的时候,通过 this.props.example.XX 即可
- 也可以是指定绑定某命名空间下的 state,这个时候 productlist 会单独绑定在 props ,输入 this.props 我们可能无法知道是来自那个 namespace 下的。
@connect(({ example })=>{ productlist : example.productlist })
@connect(({ example : productlist })=>{ productlist })
// 直接将 example 命名空间下的 productList 数据绑定到对应组件参数中
//这里通过 this.props.productlist 直接获取到 example 下的 productlist数据
可根据实际情况 确定使用哪种效果更佳!!!
获取指定模块区间下的 state 数据
-
连接后就可以在组件页面中获取 example 的 namespace(命名空间) 下的
state
数据 (以下为获取model文件夹下 example.js 中的state
数据)this.props.product.innerText this.props.product.productlist 或 const {innerText,productlist}=this.props.product
这里就体现了 定义不同 namespace 的用处了,你完全可以根据需要获取单独某一个模块下的数据!!!
关于数据更新操作,你需要知道的
- 实际上这里的
connect
传递model
中的数据给了组件router
(路由),组件中 通过 this.props 访问,从而获取到model
中保存的值了。通过打印当前页面的this.props
,会发现同样也会把dispatch
(作用:可以发送请求到model
去,这个过程可以去改变 state 中的数据),history
方法传递过来。
dispatch
使用通过 type 属性指定对应的action
类型(行为),而这个类型名在reducers(effects)
会一一对应,从而知道调用那一个reducers (effects)
(注意:action
类型,如果是在model
文件夹以外)—即组件中调用需要指定添加 namespace 命名空间)
this.props.dispatch({
type:'product/update',
payload: { // 这里的 payload 的名称可以任意,只需要和 effect 和 reducers 入参一致
id:1001
}
})
最后我们简单来分析下@connect()!!!
分析@connect()
- @connect() 的 () 中实际上定义了一个箭头函数 ()=>{},connect负责注入 () 中的参数,{} 中的赋值是从 () 传入的数据赋值并绑定 组件 中的参数。 所以这里最后你需要拿到什么值,你都需要在 {} 中返回,你可以选择给返回的数据重命名,但是一般不会这样做,因为当你的重命名和 namespace 不一致,这对于你找到 props 下的数据很不利。
@connect(({example}) => ({
productList:example.productList
}))
// @connect 是 ProductPage 组件的注解,必须在 class 前进行注解
export default class ProductPage extends Component{
componentDidMount(){
console.log(this.props.productList) // 获取到 example 命名空间下 state 的 productList 数据
}
// ...
}
到这里我们的介绍就差不多告一段落了,最后我会给出几种常见的使用方式!!!
页面中不同使用方式
- 通过 connect 的使用
const mapStateToProps=(state)=>{
return {
// 从整个 state 中获取命名空间为 example 下的 productList 数据
productList:state.example
}
}
export default connect(mapStateToProps)(ProductPage)
// 获取指定命名空间下的 state 数据
export default connect((state) => ({
productList:state.example
}))(ProductPage);
// connect(从 model 的 state 中获取数据)(要将数据绑定到哪个组件)
- @connect的使用
//@connect 是 ProductPage 组件的注解,必须在 class 前进行注解
@connect(({example}) => ({
productList:example.productList
}))
export default class ProductPage extends Component{
// ...
}
引申
这里主要对文章中涉及到的没有详细分析的点做了一个指向参考。
generator函数语法介绍可参考:http://jsrun.net/t/RZKKp
注解:http://cw.hubwiz.com/card/c/5599d367a164dd0d75929c76/1/1/4/
models文件夹下的js文件的初步了解
export default {
namespace: 'example', // 表示对于整个应用不同的命名空间,以便通过 this.props.example 访问,和当前的model文件名相同就好之前的renducer名字相同,是全局state的属性,只能为字符串,不支持 . 的放式建立
state:{
productlist:'小白',
initText:'hello'
} // 表示当前的 example 中的 state 状态,可以给初始值
subscriptions:{
// 这里的函数名随意,可以有多个函数
setup({dispatch,history}){
// 订阅,监听服务器连接,键盘输入,路由,状态等的变化,比如,浏览器窗口大小变化
window.onresize=()={}
}
setupHistory({dispatch,history}){
history.listen((location)=>{
console.log(location)
})
}
...
}
effects:{
// * ES6新语法,generator提供,返回迭代器,通过 yield 关键字实现暂停功能。官网 API, https://dvajs.com/knowledgemap/#generators
* update({payload},{call,put}){
// payload 是从组件传递过来的参数,不一定名为 payload,取决于页面中传递过来的参数名
// call 方法可以使用payload参数传递给后台程序进行处理,这里可以调用 service 层的方法进行后端程序
// put 表示存储在当前命名空间 example 中,通过 save 方法存在当前 state 中
yield put({type:'save'}); // 用于触发 action type不带 namespace 表明是本 model 中的方法
}
* add({payload},{call,put}){
const data=yield call(add,payload) // 异步请求,向 add 接口发出请求,传参为 payload 的值,data 可以接收到后台传来的数据
if(data){ // 通过返回来的数据,更新 state 中的数据
yield put({
type:'save',
paylod:data
})
}
}
}
//用来保存更新 state 值 上面的 put 方法调用这里的方法(通常用来把页面,后台传来的值存入到 state 中),用于处理同步操作,唯一可以修改 state 的地方,通过 action 触发。
reducers:{
// action 行为 返回新的数据对象,在其他 view 组件中通过 props.dispatch('namespace/save',{})) 来进行调用
save(state,action){
return {…state,…action.payload};
},
updateList(state,action){
let currentProductList=deepClone(state);
currentProductList.productList.push(action.payload)
return currentProductList;
}
}
}
function deepClone(){
let _obj=JSON.stringify(arr),
objClone=JSON.parse(_obj)
return objClone
}
dva-loading 的引入使用
对于一般全局的loading
,我们会根据业务的不同来显示响应不同的 loading
图标,就是那种类似正在加载中。。。,
这时候我发现别人写的代码中就有传递一个loading
,但是不知道这里的 loading 是哪里来的???所以这里提出来做一个了解,注意这里的 loading
使用,是需要重新下载相关插件。如下代码是我在项目中看到疑惑的就是这个loading: {effects} :
<html>
<!--在这里插入内容-->
</html>
const mapStateToProps = ({products: {productList, productsById, searchType}, loading: {effects}, login: {loginStatus}}) => ({
productList,
productsById,
searchType,
loading: effects,
loginStatus
});
@connect(mapStateToProps)
安装依赖
npm install dva-loading
dva-loading 引入
便于全局使用,你需要在入口 index.js 文件中引入下载的 dva-loading
插件
import createLoading from 'dva-loading'
app.use(createLoading()) // 装载插件
使用
-
使用了
app.use(createLoading())
之后reducer
的 state 中会增加一个 loading 属性- 之后就可以在需要的组件中获取它
const mapStateToProps=(state)=>{ loading:state.loading } @connect(mapStateToProps)
- 组件中拿到
loading
属性后通过它 可以检测指定的异步请求的状态,来判断对应组件的loading
状态
现在以请求
add
为例import { Spin } from 'antd' export default class ProductList extends React.Component { render() { const loading = this.props.loading.effects['example/add'] return ( <Spin spinning={!!loading}> <div> <h1>example</h1> </div> </Spin> ) } }
这里的
loading.effects['example/add']
就会拿到 namespace 为 example 的model
中的名称为 add 的异步请求的状态(true/false),当指定的异步请求不存在时就会返回 undefined。打印this.props.loading
为:{ effects:{} global: false / true models:{} } //global 代表全局的 loading 状态, effects 为每个异步请求 的 loading 状态,models 为对应 model 的 loading状态
这里请求的状态对应不同的
loading
状态:请求前,请求中,请求完成。-
发送请求前,
loading
中: global 为 false,effects 和 models 为空对象 -
请求中…, global 为 true,effects 和 models 由当前是否触发 action 决定,其中 effects 的 key 为 dispatch 的 type值,value 为 true/false
-
请求完成,
global 为 false,effects 和 models 由当前是否触发 action 决定
专有名词查阅
- Store 具体可以查看文档:Redux-Store
- State (状态)
初始值,dva() 初始化的时候和在 modal 里面的 state 对其两处惊醒定义,其中 modal 中的优先级低于传给 dva() 的 opts.initialState,具体可以查看文档:Dva-Modal