DataBinder 数据交互方案
如果项目中使用的是 1.x 版本的基础组件(@alifd/next),请在左侧导航顶部切换组件版本。
安装方法
- 在命令行中执行以下命令
npm install @icedesign/data-binder@0.1.11 -S
ICE 前后端数据绑定、交互方案。
介绍和使用方法
目标
灵感来源于 Webx,基于一定的约定帮你在组件上绑定一些数据和用来更新数据的 API,让你专注于 render 方法中界面显示逻辑,从而屏蔽掉 AJAX、state 管理等开发成本。
如果你希望你的接口更自由,或者你希望自行把数据维护在组件 state 内,则都不推荐使用本方案。
15 分钟快速上手教程
15 分钟视频演示快速对比 Data-Binder 与 AJAX 方案对比,详细文档在下面。
使用方法
1. 在某个 Class 上面配置当前需要的 DataSource
DataBinder 采用 decorator(类似 Java 注解)的方式使用,即在 class 上面调用并配置相关信息即可生效。
DataSource 是 ICE DataBinder 解决方案中最重要的概念,组件、页面中某一块依赖数据的功能会被作为一个 DataSource 模块,用一个模块 Key 来区分,每一个数据模块可以配置多种数据获取方式以及默认数据等,然后注入到组件中被使用(详细的说明在下面)。比如我们最常见的通过 AJAX 获取、操作数据:
@DataBinder({
'模块名 key': {
url: 'xxxx.json',
method: 'post',
// 请求附带的 request 参数,method post 下是 data 参数
data: {
page: 1
},
// AJAX 部分的参数完全继承自 axios ,参数请详见:https://github.com/axios/axios
// 下面是请求会返回的默认数据
defaultBindingData: {
// ...字段需要与 xxxx.json 接口返回的字段一一对应
}
}
})
class ListView extends Component {
...
}
例如:
@DataBinder({
account: {
url: '/getAccountInfo.json',
type: 'post',
data: {
uid: '123123'
},
defaultBindingData: {
// 配置接口返回数据的字段的初次默认值
userName: '',
userAge: 0
}
}
})
class ListView extends Component {
...
}
详细的解释下:
模块名 key 必填 用来将数据和接口约束在某一个范围下,通常按照接口数据划分或者按照功能区块。
url, type, data, etc.
选填 配置当前模块接口相关信息,基于 axios 支持其文档所有参数。该参数可选,部分模块可能无需 AJAX 交互,或者无法写死配置需要通过其他接口来获取。defaultBindingData
选填 该字段配置当前模块数据初始化默认值,如果当前模块有异步接口配置,则模块的字段需要与接口返回的数据字段一一对应。该参数可选,因为有些接口只需要提交成功即可,无需 UI 变化。
2. 通过 this.props.bindingData.模块key
来获取绑定的数据
对某个 React class 添加 DataBinder 绑定配置之后,DataBinder 会在组件上添加一个 props bindingData
用来存放配置的所有数据,模块 key 为你对应的 DataSource key 的前部分,比如:配置 account
可以通过 this.props.bindingData.account
获取到被绑定的数据,第一次为 defaultBindingData
里面配置的数据。
因此你可以在你 render 部分的代码编写如下代码调用:
@DataBinder({...})
class ListView extends Component {
render() {
const { account } = this.props.bindingData;
return (
<div>
<p>用户名:{account.userName}</p>
<p>年龄:{account.userAge}</p>
</div>
);
}
}
3. 通过 this.props.updateBindingData
来更新模块数据
DataBinder 除了为组件添加一个 props 之外,还向组件内部注入一个 API 用来更新模块数据:
this.props.updateBindingData(key, params, callback);
第一个参数是模块 key ,字符串类型,用来标记更新哪一个 DataSource,需要保留全名(例如:account
)。第二个参数是 DataSource 的配置,对象类型,调用时它会和默认定义的数据进行一个 merge 操作,并发起 AJAX 请求。通常你只需要传递 {data: {…}}
数据即可,data
用来描述对接口发请求时附加的参数。第三个参数是 callback
,是一个函数,当请求结束之后调用,方便你处理额外逻辑。
注意:updateBindingData 里面传递的参数,跟顶部配置的初始化参数是一个 deepmerge 的合并操作。
比如一个翻页组件,当页码变化时获取新数据可以这样做:
@DataBinder({
accountTable: {
url: '/getAccountTableList.json',
type: 'post',
data: {
page: 1,
pageSize: 10,
},
defaultBindingData: {
page: 1,
pageSize: 10,
total: 0,
lists: [],
},
},
})
class ListView extends Component {
changePage = (pageNo) => {
this.props.updateBindingData('accountTable', {
data: {
page: pageNo,
},
});
};
render() {
const { accountTable } = this.props.bindingData;
return (
<div>
当前 Table 数据:{accountTable.lists}
<Pagination
current={accountTable.page}
pageSize={accountTable.pageSize}
total={accountTable.total}
onChange={this.changePage}
/>
</div>
);
}
}
DataBinder 不会在组件初始化的时候帮你自动请求一次,因为有些 DataSource 不需要默认就请求一次。如果你需要在初始化的异步请求数据,就需要在合适的生命周期中主动调用该方法,比如组件即将渲染的时候拉取数据:
@DataBinder({...})
class ListView extends Component {
componentDidMount() {
// 拉取第一页的数据
this.props.updateBindingData('accountTable', {
data: {
page: 1
}
});
}
}
4. 处理 Loading 逻辑和效果
AJAX 是异步的,为了更好的用户体验,推荐添加一个 Loading 效果组件来给用户请求中的反馈。
每一个 DataSource 模块的数据附带了一个私有属性 __loading
来标记当前模块是否正在请求过程中,这样你可以在组件 render 中读取这个数据来判断是否正在加载数据。比如 Table 组件内部封装了一个 Loading 的效果,需要使用 isLoading
props 进行配置,那么就可以写:
const { accountTable } = this.props.bindingData;
<Table dataSource={accountTable.lists} isLoading={accountTable.__loading}>
...
</Table>;
你也可以使用 Loading
组件进行 loading 效果的模拟,参照文档可以写出如下代码:
import DataBinder from '@icedesign/data-binder';
import { Loading } from '@icedesign/base';
@DataBinder({
account: {
// 接口返回数据:{status: 'SUCCESS', data: {foo: 'bar'}}
url: '/getdefaultBindingData.json',
defaultBindingData: {
foo: null,
},
},
})
class ListView extends Component {
componentDidMount() {
this.props.updateBindingData('account');
}
refresh = () => {
this.props.updateBindingData('account');
};
render() {
const { account } = this.props.bindingData;
return (
<div>
<Loading
state={account.__loading ? 'on' : 'off'}
shape="fusion-reactor"
>
<div>当前 foo 的值为:{account.foo}</div>
</Loading>
<div style={{ marginTop: 20 }}>
<Button onClick={this.refresh}>点击重新请求</Button>
</div>
</div>
);
}
}
效果如图示:
此外,根模块也有一个 loading
属性(即:this.props.bindingData.
loading
),用来标记当前注册的所有模块中是否有某个模块在发送 AJAX 请求。这样可以便于进行全局的提示,比如一个 AJAX 请求全局提示标记等。
参数配置
DataBinder decorator 用法
调用方法:
@DataBinder({
模块key: {
url: 'xxx.json',
//... AJAX axios 配置
responseFormatter: (responseHandler, res, originResponse) => {
// 做一些数据转换
const newRes = {
status: res.code !== 0 ? 'SUCCESS' : 'ERROR',
message: res.successMsg,
};
// 回传给处理函数
// 不做回传处理会导致数据更新逻辑中断
responseHandler(newRes, originResponse);
},
defaultBindingData: {
foo: 'bar',
},
},
})
class ListView extends React.Component {
render() {
const key = this.props.bindingData.key;
return <div>{key.foo}</div>;
}
}
dataSouce
内容为符合 axios 的请求参数。responseFormatter
用来做老接口数据转换用,老接口如果不按照现有模式需要进行一层数据转换处理。defaultBindingData
内容为接口对应字段的默认数据,在 render 中使用this.props.bindingData
获取。
接口 API
以下 API 会注入到 Class 中,通过 this.props.xxxx
的方式调用。
API 名 | 说明 | 是否有参数 | 参数类型 | 参数值 | 备注 |
---|---|---|---|---|---|
this.props.updateBindingData | 获取更新 DataSource 的数据 | true | key: string, params: object, callback: function | this.props.updateBindingData('account', {data: {page: 5}}) | |
this.props.getDataSource | 获取某个 DataSource 的默认配置 | true | key: string |
后端接口协议
配置的 AJAX 接口需要按照一定的协议规则实现:
request:
业务接口自定。
response:
{
"status": "SUCCESS",
"message":
"接口请求完成的提示,可有可无,status 为非 SUCCESS 时会显示报错的 UI",
"data": {
"foo":
"data 是具体的数据,需要与该接口定义的 defaultBindingData 字段结构保持一致"
}
}
自定义 requestClient
如果你的项目 ajax 模块进行了统一配置和通用处理的封装,或者使用 ws 或者其它的 RPC 手段进行网络通信,DataBinder 允许对请求客户端进行自定义。在 DataBinder 传递第二个参数对象,并指定 requestClient 为一个返回 promise 的请求函数。该 Promise resolve 的值为 response 对象,该 response 对象必须包含一个 data 字段,值为返回的数据。
DEMO
import jsonp from 'jsonp';
/**
* 自定义的 json request client
*/
function request(opts) {
return new Promise((resolve, reject) => {
jsonp(opts.url, { name: 'callback' }, (err, data) => {
if (err) {
reject(err);
} else {
resolve({ data });
}
})
});
}
@DataBinder({
account: {
// 这里的所有字段会作为参数传递给 requestClient
url: 'https://ice.alicdn.com/assets/mock/53141.jsonp.js',
}
}, { requestClient: request })
export default class extends React.Component {
// ...
}
常见需求
发送数组类型数据,key 自动加了
当你传输的 data 中有个 key 数据(例如:items)为数组格式时,提交给后端 key 会自动添加 []
(例如:items[]=xxx、items[]=yyy)。如果你不需要这种功能,希望使用原本的 key 进行提交,可以添加下面配置解决:
{
serializeArray: false;
}
接口是老版本接口,不符合 DataBinder 接口协议如何处理?
配置 DataSource 时,添加 responseFormatter
配置进行数据处理,然后返回符合规范的数据。
自定义请求成功、失败的提示和逻辑
在 DataSource 配置部分自定义 success、error callback 实现,以 success 为例:
@DataBinder({
key: {
url: 'xxx.json',
success: (res, defaultCallback, originResponse) => {
console.log('请求成功了,返回的数据为', res)
// 执行默认的逻辑请求成功逻辑
// 通常为弹出反馈 Toast
defaultCallback();
// originResponse 内容请参见:https://github.com/axios/axios#response-schema
},
defaultBindingData: {
foo: 'bar'
}
}
})
class ...
error callback 的参数和逻辑同 success。
代码示例
本 Demo 演示最基础的用法。
查看源码在线预览
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import {
Button
} from '@icedesign/base';
@DataBinder({
fooData: {
url: 'https://ice.alicdn.com/assets/mock/53138.json',
// ajax 参数参见:https://github.com/axios/axios
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
refreshFoo = () => {
this.props.updateBindingData('fooData', {
// ajax 参数参见:https://github.com/axios/axios
// 当前接口不需要参数,在这里只是演示,可以打开 Devtool 的 network 面板查看做了什么
params: {
bar: 'foo'
}
});
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<div>
foo 的值: {fooData.foo}
</div>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>请求获取新数据</Button>
</div>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
演示一个实际场景。
查看源码在线预览
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import { Pagination, Table } from '@icedesign/base';
@DataBinder({
account: {
url: 'https://ice.alicdn.com/assets/mock/53141.json',
// ajax 参数参见:https://github.com/axios/axios
defaultBindingData: {
pagination: {
page: 1,
total: 0,
pageSize: 10
},
table: []
}
}
})
class App extends Component {
componentDidMount() {
const {account} = this.props.bindingData;
// 第一次渲染,初始化第一页的数据
this.props.updateBindingData('account', {
params: {
...account.pagination,
page: 1
}
});
}
changePage = (pageNo) => {
// 有些参数可能需要从数据中获取
const {account} = this.props.bindingData;
this.props.updateBindingData('account', {
params: {
...account.pagination,
page: pageNo
},
// 通过设置这个数据,可以快速将页码切换,避免等接口返回才会切换页面
// 这里的变更是同步生效的
// 需要注意多层级数据更新的处理,避免丢掉某些数据
defaultBindingData: {
...account,
pagination: {
...account.pagination,
page: pageNo
}
}
});
};
render() {
const {account} = this.props.bindingData;
return (
<div>
<Table dataSource={account.table} isLoading={account.__loading}>
<Table.Column dataIndex="name" title="name" />
<Table.Column dataIndex="age" title="age" />
</Table>
<Pagination
current={account.pagination.page}
pageSize={account.pagination.pageSize}
total={account.pagination.total}
onChange={this.changePage}
style={{marginTop: 20}}
/>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
格式化接口返回的 Demo 适配老接口。
由于 DataBinder 方案对于接口有一定的规范,因此在接入老接口或者其他人的接口时,可能会有一些数据不兼容的问题,此时可以使用 resposeFormatter
配置来做数据格式兼容。
打开 Network 面板可以看到当前 Mock 接口的数据格式不符合 DataBinder 的数据要求。
查看源码在线预览
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import { Button } from '@icedesign/base';
@DataBinder({
fooData: {
url: 'https://ice.alicdn.com/assets/mock/53146.json',
// ajax 参数参见:https://github.com/axios/axios
responseFormatter: (responseHandler, res, originResponse) => {
// 拿到接口返回的 res 数据,做一些格式转换处理,使其符合 DataBinder 的要求
// 最后再按照顺序丢到 responseHandler 方法里继续执行
res = {
success: res.code === "0" ? false : true,
message: res.msg,
params: {
...res.content
}
};
responseHandler(res, originResponse);
},
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
refreshFoo = () => {
this.props.updateBindingData('fooData', {
// ajax 参数参见:https://github.com/axios/axios
// 当前接口不需要参数,在这里只是演示,可以打开 Devtool 的 network 面板查看做了什么
params: {
bar: 'foo'
}
});
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<div>
foo 的值: {fooData.foo}
</div>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>请求获取新数据</Button>
</div>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
自定义加载效果的 Demo。
在 AJAX 加载过程中,给用户一个加载反馈是非常重要的,DataBinder 针对每个 DataSource 附加了一个私有属性 __loading
用来标记当前模块是否在请求中。因此你可以判断这个属性来看是否显示加载效果。
为了方便,我们还提供了一个全局的 __loading
属性在 this.props.bindingData
上面,当前 Class 注册的任意 DataSource 在请求中,全局都会随之改变。
查看源码在线预览
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import {
Button,
Loading
} from '@icedesign/base';
@DataBinder({
foo1Data: {
url: 'https://ice.alicdn.com/assets/mock/53138.json',
// ajax 参数参见:https://github.com/axios/axios
defaultBindingData: {
foo: 'bar'
}
},
foo2Data: {
url: 'http://dip.alibaba-inc.com/api/v2/services/schema/mock/53138',
// ajax 参数参见:https://github.com/axios/axios
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
refreshFoo1 = () => {
this.props.updateBindingData('foo1Data', {
params: {
bar: 'foo'
}
});
};
refreshFoo2 = () => {
this.props.updateBindingData('foo2Data', {
params: {
bar: 'foo'
}
});
};
render() {
const {foo1Data, foo2Data} = this.props.bindingData;
return (
<div>
<div>
<Loading visible={foo1Data.__loading} shape="fusion-reactor">
<div>
foo1 的值: {foo1Data.foo}
</div>
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo1}>请求获取 foo1 新数据</Button>
</div>
</div>
<div style={{marginTop: 30}}>
<Loading visible={foo2Data.__loading} shape="fusion-reactor">
<div>
foo2 的值: {foo2Data.foo}
</div>
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo2}>请求获取 foo2 新数据</Button>
</div>
</div>
<h3>当前页面是否有模块正在加载:{this.props.bindingData.__loading ? '是' : '否'}</h3>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
本 Demo 演示自定义 requestClient,使用 jsonp 的方法发送请求
查看源码在线预览
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import jsonp from 'jsonp';
import { Pagination, Table } from '@icedesign/base';
/**
* 自定义的 json request client
*/
function request(opts) {
return new Promise((resolve, reject) => {
jsonp(opts.url, { name: 'callback' }, (err, data) => {
if (err) {
reject(err);
} else {
resolve({ data });
}
})
});
}
@DataBinder({
account: {
url: 'https://ice.alicdn.com/assets/mock/53141.jsonp.js',
defaultBindingData: {
pagination: {
page: 1,
total: 0,
pageSize: 10
},
table: []
}
}
}, { requestClient: request })
class App extends Component {
componentDidMount() {
const {account} = this.props.bindingData;
// 第一次渲染,初始化第一页的数据
this.props.updateBindingData('account', {
params: {
...account.pagination,
page: 1
}
});
}
changePage = (pageNo) => {
// 有些参数可能需要从数据中获取
const {account} = this.props.bindingData;
this.props.updateBindingData('account', {
params: {
...account.pagination,
page: pageNo
},
// 通过设置这个数据,可以快速将页码切换,避免等接口返回才会切换页面
// 这里的变更是同步生效的
// 需要注意多层级数据更新的处理,避免丢掉某些数据
defaultBindingData: {
...account,
pagination: {
...account.pagination,
page: pageNo
}
}
});
};
render() {
const {account} = this.props.bindingData;
return (
<div>
<Table dataSource={account.table} isLoading={account.__loading}>
<Table.Column dataIndex="name" title="name" />
<Table.Column dataIndex="age" title="age" />
</Table>
<Pagination
current={account.pagination.page}
pageSize={account.pagination.pageSize}
total={account.pagination.total}
onChange={this.changePage}
style={{marginTop: 20}}
/>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
自定义请求成功或者失败的处理逻辑。
DataBinder 默认的请求成功和失败的行为是弹一个 Toast 将接口的 message 字段信息展示一下。如果你需要自定义一些成功失败的行为可以在 DataSource 中配置 success 和 error 来接入。其中第二个参数是 DataBinder 默认的回调处理逻辑(弹 Toast)如果不需要可以不调用。
error 有两类错位,一类是网络中断,请求都没有到后端服务器;另一类是服务器接口报错(即接口返回 success 字段为 false)。两者应该给用户不同的提示和处理逻辑,可以通过其他参数来判断。
此外为了方便在 render 中处理,与 loading
类似,我们针对每个 DataSource 附加了一个 error
属性用来标记当前模块是否有报错,默认值为 null,出错时会变为 {message: '报错信息'}
数据。
查看源码在线预览
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import { Button, Loading, Feedback } from '@icedesign/base';
@DataBinder({
foo1Data: {
url: 'https://ice.alicdn.com/assets/mock/53138.json',
// ajax 参数参见:https://github.com/axios/axios
success: (res, defaultCallback, originResponse) => {
console.log('res', res, originResponse);
defaultCallback();
},
error: (res, defaultCallback, originResponse) => {
console.log('res', res, originResponse);
defaultCallback();
},
defaultBindingData: {
foo: 'bar'
}
},
foo2Data: {
url: 'http://dip.alibab2a-inc.com/api/v2/services/schema/mock/53138',
// ajax 参数参见:https://github.com/axios/axios
error: (res, defaultCallback, originResponse) => {
console.log('res', res, originResponse);
console.log('详细网络问题可以通过 originResponse 参数拿到:', originResponse.status);
defaultCallback();
},
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
refreshFoo1 = () => {
this.props.updateBindingData('foo1Data', {
params: {
bar: 'foo'
}
});
};
refreshFoo2 = () => {
this.props.updateBindingData('foo2Data', {
params: {
bar: 'foo'
}
});
};
render() {
const {foo1Data, foo2Data} = this.props.bindingData;
return (
<div>
<div>
<h3>foo1 演示自定义回调</h3>
<Loading visible={foo1Data.__loading} shape="fusion-reactor">
<div>
foo1 的值: {foo1Data.foo}
</div>
</Loading>
{foo1Data.__error
?
<Feedback type="error">
foo1 数据获取失败,失败 message: {foo1Data.__error.message}
</Feedback>
:
<Feedback>
foo1 数据获取成功
</Feedback>
}
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo1}>请求获取 foo1 新数据</Button>
</div>
</div>
<div style={{marginTop: 30}}>
<h3>foo2 接口有问题,会模拟网络请求 error</h3>
<Loading visible={foo2Data.__loading} shape="fusion-reactor">
<div>
foo3 的值: {foo2Data.foo}
</div>
</Loading>
{foo2Data.__error &&
<Feedback type="error">
foo1 数据获取失败,失败 message: {foo2Data.__error.message}
</Feedback>
}
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo2}>请求获取 foo2 新数据</Button>
</div>
</div>
<h3>当前页面是否有模块请求报错:{this.props.bindingData.__error ? '是' : '否'},报错信息:{this.props.bindingData.__error && this.props.bindingData.__error.message}</h3>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);