ICE 前后端数据绑定、交互方案。基于一定的约定帮你在组件上绑定一些数据和用来更新数据的 API,让开发者专注于 render 逻辑,从而屏蔽掉 AJAX、state 管理等开发成本。
以下 API 会注入到 Class 中,通过 this.props.xxxx
的方式调用。
API | 说明 | 类型 | 默认值 | 备注 |
---|---|---|---|---|
updateBindingData | 更新数据源 | func | ||
bindingData | 返回数据 | object |
DataBinder 基于 HOC 的思路,采用 decorator 的方式使用,即在 class 上面调用并配置相关信息即可生效。
@DataBinder({
'模块名 key': {
// AJAX 部分的参数完全继承自 axios ,参数请详见:https://github.com/axios/axios
url: 'http://xxxx.json',
method: 'GET',
params: {
page: 1
},
// 接口默认数据
defaultBindingData: {
}
}
})
class ListView extends Component {
...
}
详细的解释下:
对某个 React class 添加 DataBinder 绑定配置之后,DataBinder 会通过 HOC 在组件上添加一个 props bindingData
,用来存放配置的所有数据,模块 key 为你对应的 DataSource key 的前部分,比如:配置 account
可以通过 this.props.bindingData.account
获取到被绑定的数据,第一次为 defaultBindingData
里面配置的数据。
因此你可以在你 render 部分的代码编写如下代码调用:
@DataBinder({
listData: {
url: '/getAccountTableList.json',
},
})
class ListView extends Component {
componentDidMount() {
// 组件加载时获取数据源,数据获取完成会触发组件 render
this.props.updateBindingData('dataSource');
}
render() {
const { listData } = this.props.bindingData;
return (
<div>
<Table loading={listData.__loading} dataSource={listData.list} />
</div>
);
}
}
DataBinder 对数据接口的 response 做了一层规范,不符合该规范的接口将无法正常获取到数据,response 规范:
{
// 必选:标记接口是否成功,非 SUCCESS 都视为失败
"status": "SUCCESS",
// 可选:status 为非 SUCCESS 时显示报错 UI 会使用该字段
"message": "成功",
// 必选:实际数据,会将 data 下的所有字段挂载在对应的 bindingData 上
"data": {
"dataSource": [],
"page": 1
}
}
如果业务里的接口跟该规范不符,可以通过 responseFormatter
做一次转换。具体请参见组件 demo。
DataBinder 默认使用 axios 完成前后端通信,实际场景里业务可能使用 jsonp,RPC 或者其他协议通信,此时需要通过自定义 requestClient 的方式实现,具体请参见组件 demo。
DataBinder 默认的请求成功和失败的行为是弹一个 Toast 将接口的 message 字段信息展示出来。如果你需要自定义全局的成功失败行为,可以通过自定义 callback 的方式实现,具体请参见组件 demo。
如果业务里出现类似上述所说的场景,比如:接口规范不一致、需要全局统一处理请求失败成功逻辑、使用非 axios 的方式请求数据(比如 jsonp),推荐基于 DataBinder 封装一个自定义的 DataBinder,然后代码里使用自定义 DataBinder,具体请参见组件 demo。
通过 GET 方式请求数据,基于 __loading
属性可以区分请求的不同状态,基于 __error
属性可以区分接口是否报错。
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import {
Button, Loading
} from '@alifd/next';
@DataBinder({
fooData: {
url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/success',
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
componentDidMount() {
this.props.updateBindingData('fooData', {
params: {
key: 'init'
}
}, (response) => {
// 请求回调,可按需使用
console.log('数据加载完成啦', response);
});
}
refreshFoo = () => {
this.props.updateBindingData('fooData', {
params: {
bar: 'foo'
}
});
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<Loading visible={fooData.__loading}>
foo 的值: {fooData.foo}
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>主动获取新数据</Button>
</div>
<h3>数据加载中:{fooData.__loading ? '是' : '否'}</h3>
<h3>接口是否报错:{fooData.__error ? fooData.__error.message : '无'}</h3>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
通过 POST 请求获取数据,POST 请求数据时,请求参数可以放在 url query 或者 body 上,具体由接口实现决定:
具体可参考 axios 的文档说明。
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import {
Button, Loading
} from '@alifd/next';
@DataBinder({
fooData: {
url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/post',
method: 'POST',
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
componentDidMount() {
this.props.updateBindingData('fooData', {
// 参数放在 query 上
params: {
key: 'init'
}
});
}
refreshFoo = () => {
this.props.updateBindingData('fooData', {
// 参数放在 body 上
data: {
bar: 'foo'
}
});
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<Loading visible={fooData.__loading}>
foo 的值: {fooData.foo}
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>主动获取新数据</Button>
</div>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
某些场景下,一个组件会用到多个数据源,通过 key 来区分即可。
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import {
Button,
Loading
} from '@alifd/next';
@DataBinder({
foo1Data: {
url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/success',
defaultBindingData: {
foo: 'bar'
}
},
foo2Data: {
url: 'https://www.easy-mock.com/mock/5c7c9334869f506acc184ff7/ice/foo2',
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}>
<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}>
<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);
通过 responseFormatter 格式化接口返回数据,适配跟 DataBinder 接口规范不一致的情况。
假设业务实际接口格式如下:
{
"code": 0,
"msg": "OK",
"content": {
"foo": ""
}
}
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import { Button, Loading } from '@alifd/next';
@DataBinder({
fooData: {
url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/custom',
responseFormatter: (responseHandler, body, response) => {
// 拿到接口返回的 res 数据,做一些格式转换处理,使其符合 DataBinder 的要求
const newBody = {
status: body.code === '1' ? 'SUCCESS' : 'ERROR',
message: body.msg,
data: body.content
};
responseHandler(newBody, response);
},
defaultBindingData: {
foo: 'bar'
}
}
})
class App extends Component {
refreshFoo = () => {
this.props.updateBindingData('fooData');
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<Loading visible={fooData.__loading}>
foo 的值: {fooData.foo}
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>请求获取新数据</Button>
</div>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
自定义请求成功或者失败的处理逻辑:比如接口成功不弹出 toast
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import { Button, Loading, Message } from '@alifd/next';
@DataBinder({
fooData: {
url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/success',
success: (body, defaultCallback, originResponse) => {
if (body.status !== 'SUCCESS') {
// 后端返回的状态码错误
Message.error(body.message);
} else {
// // 成功不弹 toast,可以什么都不走
console.log('success');
}
},
// error 有两类错误,一类是网络中断,请求没有发送成功;另一类是服务器接口报错
error: (originResponse, defaultCallback, err) => {
// 失败弹 toast
Message.error(err.message);
},
defaultBindingData: {
foo: '默认值'
}
}
})
class App extends Component {
refreshFoo = () => {
this.props.updateBindingData('fooData', {
params: {
bar: 'foo'
}
});
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<Loading visible={fooData.__loading} shape="fusion-reactor">
<div>
foo1 的值: {fooData.foo}
</div>
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>请求获取新数据</Button>
</div>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
本 Demo 演示自定义 requestClient:使用 jsonp 的方法发送请求,自定义的 requestClien 必须返回一个 Promise
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import querystring from 'querystring';
import DataBinder from '@icedesign/data-binder';
import jsonp from 'jsonp';
import { Button, Loading } from '@alifd/next';
/**
* 自定义的 json request client
*/
function request(opts) {
return new Promise((resolve, reject) => {
jsonp(opts.url + '?' + querystring.encode(opts.params), {
name: 'callback'
}, (err, data) => {
if (err) {
reject(err);
} else {
resolve({ data });
}
})
});
}
@DataBinder({
fooData: {
url: 'https://sug.so.360.cn/suggest',
defaultBindingData: {
q: '默认值'
},
responseFormatter: (responseHandler, body, response) => {
const newBody = {
success: 'SUCCESS',
message: 'ok',
data: body
};
responseHandler(newBody, response);
},
}
}, { requestClient: request })
class App extends Component {
refreshFoo = () => {
this.props.updateBindingData('fooData', {
params: {
word: 'test'
}
});
};
render() {
const {fooData} = this.props.bindingData;
return (
<div>
<Loading visible={fooData.__loading}>
foo 的值: {fooData.q}
</Loading>
<div style={{marginTop: 10}}>
<Button onClick={this.refreshFoo}>请求获取新数据</Button>
</div>
</div>
);
}
}
ReactDOM.render((
<App />
), mountNode);
很多场景下,我们会遇到诸如以下 case:
这些场景我们建议业务上对 DataBinder 包装一层,产出业务自身的一个 DataBinder。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import DataBinder from '@icedesign/data-binder';
import { Button, Loading, Message } from '@alifd/next';
/**
* 自定义一个 DataBinder,建议放在 src/components/DataBinder 下。支持以下特性:
* - 通过 showSuccessToast/showErrorToast 配置是否要弹 toast
* - 通过 responseFormatter 格式化后端接口
* - 基于 responseFormatter 实现未登录、无权限等逻辑
*/
const CustomDataBinder = (options) => {
// 重组 options
let newOptions = {};
Object.keys(options).forEach(dataSourceKey => {
const config = options[dataSourceKey];
const { showErrorToast = true, showSuccessToast = false } = config;
newOptions[dataSourceKey] = {
...config,
responseFormatter: (responseHandler, body, response) => {
if (body.code === '-1') {
// 未登录
Message.error('未登录,即将跳转到登录页面');
// location.reload();
return;
}
if (body.code === '-2') {
// 无权限
Message.error('无权限,即将跳转无权限页面');
// location.reload();
return;
}
const newBody = {
status: body.code === '0' ? 'SUCCESS' : 'ERROR',
message: body.msg,
data: body.content || {}
};
responseHandler(newBody, response);
},
success: (body, defaultCallback, originResponse) => {
const {config} = originResponse;
if (body.status !== 'SUCCESS') {
// 后端返回的状态码错误
if (config.showErrorToast) {
Message.error(body.message);
}
} else {
if (config.showSuccessToast) {
Message.success(body.message);
}
}
},
error: (originResponse, defaultCallback, err) => {
// 网络异常:404,302 等
const {config} = originResponse;
if (config.showErrorToast) {
Message.error(err.message);
}
}
};
});
return DataBinder.call(this, newOptions);
};
@CustomDataBinder({
successData: {
url:
'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/ok',
showSuccessToast: false
},
errorData: {
url:
'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/error',
showErrorToast: true
},
networkErrorData: {
url:
'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/errorssss',
showErrorToast: false
},
notLogin: {
url:
'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/not-login'
},
noAuth: {
url:
'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/no-auth'
}
})
class App extends Component {
updateData = key => {
this.props.updateBindingData(key);
};
render() {
const { successData, errorData, notLogin, noAuth, networkErrorData } = this.props.bindingData;
const itemStyle = {
margin: '10px 0',
boderBottom: '1px solid #999',
};
return (
<div>
<div style={itemStyle}>
<div>请求成功不弹 toast</div>
<Loading visible={successData.__loading}>
foo 的值: {successData.foo}
</Loading>
<div style={{ marginTop: 10 }}>
<Button onClick={this.updateData.bind(this, 'successData')}>
获取新数据
</Button>
</div>
</div>
<div style={itemStyle}>
<div>请求失败弹 toast</div>
<Loading visible={errorData.__loading}>
Error: {errorData.__error && errorData.__error.message}
</Loading>
<div style={{ marginTop: 10 }}>
<Button onClick={this.updateData.bind(this, 'errorData')}>
获取新数据
</Button>
</div>
</div>
<div style={itemStyle}>
<div>请求失败不弹 toast</div>
<Loading visible={networkErrorData.__loading}>
Error: {networkErrorData.__error && networkErrorData.__error.message}
</Loading>
<div style={{ marginTop: 10 }}>
<Button onClick={this.updateData.bind(this, 'networkErrorData')}>
获取新数据
</Button>
</div>
</div>
<div style={itemStyle}>
<div>请求未登录</div>
<Loading visible={notLogin.__loading}>
foo 的值: {notLogin.foo}
</Loading>
<div style={{ marginTop: 10 }}>
<Button onClick={this.updateData.bind(this, 'notLogin')}>
获取新数据
</Button>
</div>
</div>
<div style={itemStyle}>
<div>请求无权限</div>
<Loading visible={noAuth.__loading}>foo 的值: {noAuth.foo}</Loading>
<div style={{ marginTop: 10 }}>
<Button onClick={this.updateData.bind(this, 'noAuth')}>
获取新数据
</Button>
</div>
</div>
</div>
);
}
}
ReactDOM.render(<App />, mountNode);