DataBinder 数据交互方案

如果项目中使用的是 0.x 版本的基础组件(@icedesign/base, @ali/ice, @alife/next),请在左侧导航顶部切换组件版本。

安装方法

  1. 在命令行中执行以下命令npm install @icedesign/data-binder@1.0.5 -S

ICE 前后端数据绑定、交互方案。基于一定的约定帮你在组件上绑定一些数据和用来更新数据的 API,让开发者专注于 render 逻辑,从而屏蔽掉 AJAX、state 管理等开发成本。

API

以下 API 会注入到 Class 中,通过 this.props.xxxx 的方式调用。

API说明类型默认值备注
updateBindingData更新数据源func
bindingData返回数据object

使用

1. 配置数据源

DataBinder 基于 HOC 的思路,采用 decorator 的方式使用,即在 class 上面调用并配置相关信息即可生效。

  1. @DataBinder({
  2. '模块名 key': {
  3. // AJAX 部分的参数完全继承自 axios ,参数请详见:https://github.com/axios/axios
  4. url: 'http://xxxx.json',
  5. method: 'GET',
  6. params: {
  7. page: 1
  8. },
  9. // 接口默认数据
  10. defaultBindingData: {
  11. }
  12. }
  13. })
  14. class ListView extends Component {
  15. ...
  16. }

详细的解释下:

  • 模块名 key:必填,每个 key 代表一个数据源(接口)

  • defaultBindingData: 选填 该字段配置当前模块数据初始化默认值,如果当前模块有异步接口配置,则模块的字段需要与接口返回的数据字段一一对应。该参数可选,因为有些接口只需要提交成功即可,无需 UI 变化。

  • 其他:选填,配置请求相关参数,完成前后端通信,默认基于 axios,因此这里可以使用 axios 的任意参数。同时业务可以自定义其他 request client。

2. 请求并使用数据源

对某个 React class 添加 DataBinder 绑定配置之后,DataBinder 会通过 HOC 在组件上添加一个 props bindingData,用来存放配置的所有数据,模块 key 为你对应的 DataSource key 的前部分,比如:配置 account 可以通过 this.props.bindingData.account 获取到被绑定的数据,第一次为 defaultBindingData 里面配置的数据。

因此你可以在你 render 部分的代码编写如下代码调用:

  1. @DataBinder({
  2. listData: {
  3. url: '/getAccountTableList.json',
  4. },
  5. })
  6. class ListView extends Component {
  7. componentDidMount() {
  8. // 组件加载时获取数据源,数据获取完成会触发组件 render
  9. this.props.updateBindingData('dataSource');
  10. }
  11. render() {
  12. const { listData } = this.props.bindingData;
  13. return (
  14. <div>
  15. <Table loading={listData.__loading} dataSource={listData.list} />
  16. </div>
  17. );
  18. }
  19. }

常见问题

接口协议规范

DataBinder 对数据接口的 response 做了一层规范,不符合该规范的接口将无法正常获取到数据,response 规范:

  1. {
  2. // 必选:标记接口是否成功,非 SUCCESS 都视为失败
  3. "status": "SUCCESS",
  4. // 可选:status 为非 SUCCESS 时显示报错 UI 会使用该字段
  5. "message": "成功",
  6. // 必选:实际数据,会将 data 下的所有字段挂载在对应的 bindingData 上
  7. "data": {
  8. "dataSource": [],
  9. "page": 1
  10. }
  11. }

如果业务里的接口跟该规范不符,可以通过 responseFormatter 做一次转换。具体请参见组件 demo。

自定义前后端通信方式 requestClient

DataBinder 默认使用 axios 完成前后端通信,实际场景里业务可能使用 jsonp,RPC 或者其他协议通信,此时需要通过自定义 requestClient 的方式实现,具体请参见组件 demo。

自定义请求 callback

DataBinder 默认的请求成功和失败的行为是弹一个 Toast 将接口的 message 字段信息展示出来。如果你需要自定义全局的成功失败行为,可以通过自定义 callback 的方式实现,具体请参见组件 demo。

业务自定义 DataBinder

如果业务里出现类似上述所说的场景,比如:接口规范不一致、需要全局统一处理请求失败成功逻辑、使用非 axios 的方式请求数据(比如 jsonp),推荐基于 DataBinder 封装一个自定义的 DataBinder,然后代码里使用自定义 DataBinder,具体请参见组件 demo。

代码示例

简单的用法

通过 GET 方式请求数据,基于 __loading 属性可以区分请求的不同状态,基于 __error 属性可以区分接口是否报错。

DataBinder 数据交互方案 - 图1

查看源码在线预览

  1. import React, {Component} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import DataBinder from '@icedesign/data-binder';
  4. import {
  5. Button, Loading
  6. } from '@alifd/next';
  7. @DataBinder({
  8. fooData: {
  9. url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/success',
  10. defaultBindingData: {
  11. foo: 'bar'
  12. }
  13. }
  14. })
  15. class App extends Component {
  16. componentDidMount() {
  17. this.props.updateBindingData('fooData', {
  18. params: {
  19. key: 'init'
  20. }
  21. }, (response) => {
  22. // 请求回调,可按需使用
  23. console.log('数据加载完成啦', response);
  24. });
  25. }
  26. refreshFoo = () => {
  27. this.props.updateBindingData('fooData', {
  28. params: {
  29. bar: 'foo'
  30. }
  31. });
  32. };
  33. render() {
  34. const {fooData} = this.props.bindingData;
  35. return (
  36. <div>
  37. <Loading visible={fooData.__loading}>
  38. foo 的值: {fooData.foo}
  39. </Loading>
  40. <div style={{marginTop: 10}}>
  41. <Button onClick={this.refreshFoo}>主动获取新数据</Button>
  42. </div>
  43. <h3>数据加载中:{fooData.__loading ? '是' : '否'}</h3>
  44. <h3>接口是否报错:{fooData.__error ? fooData.__error.message : '无'}</h3>
  45. </div>
  46. );
  47. }
  48. }
  49. ReactDOM.render((
  50. <App />
  51. ), mountNode);

POST 请求获取数据

通过 POST 请求获取数据,POST 请求数据时,请求参数可以放在 url query 或者 body 上,具体由接口实现决定:- url query: 通过 params 参数指定- body: 通过 data 参数指定具体可参考 axios 的文档说明。

DataBinder 数据交互方案 - 图2

查看源码在线预览

  1. import React, {Component} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import DataBinder from '@icedesign/data-binder';
  4. import {
  5. Button, Loading
  6. } from '@alifd/next';
  7. @DataBinder({
  8. fooData: {
  9. url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/post',
  10. method: 'POST',
  11. defaultBindingData: {
  12. foo: 'bar'
  13. }
  14. }
  15. })
  16. class App extends Component {
  17. componentDidMount() {
  18. this.props.updateBindingData('fooData', {
  19. // 参数放在 query 上
  20. params: {
  21. key: 'init'
  22. }
  23. });
  24. }
  25. refreshFoo = () => {
  26. this.props.updateBindingData('fooData', {
  27. // 参数放在 body 上
  28. data: {
  29. bar: 'foo'
  30. }
  31. });
  32. };
  33. render() {
  34. const {fooData} = this.props.bindingData;
  35. return (
  36. <div>
  37. <Loading visible={fooData.__loading}>
  38. foo 的值: {fooData.foo}
  39. </Loading>
  40. <div style={{marginTop: 10}}>
  41. <Button onClick={this.refreshFoo}>主动获取新数据</Button>
  42. </div>
  43. </div>
  44. );
  45. }
  46. }
  47. ReactDOM.render((
  48. <App />
  49. ), mountNode);

一个组件多个数据源

某些场景下,一个组件会用到多个数据源,通过 key 来区分即可。

DataBinder 数据交互方案 - 图3

查看源码在线预览

  1. import React, {Component} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import DataBinder from '@icedesign/data-binder';
  4. import {
  5. Button,
  6. Loading
  7. } from '@alifd/next';
  8. @DataBinder({
  9. foo1Data: {
  10. url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/success',
  11. defaultBindingData: {
  12. foo: 'bar'
  13. }
  14. },
  15. foo2Data: {
  16. url: 'https://www.easy-mock.com/mock/5c7c9334869f506acc184ff7/ice/foo2',
  17. defaultBindingData: {
  18. foo: 'bar'
  19. }
  20. }
  21. })
  22. class App extends Component {
  23. refreshFoo1 = () => {
  24. this.props.updateBindingData('foo1Data', {
  25. params: {
  26. bar: 'foo'
  27. }
  28. });
  29. };
  30. refreshFoo2 = () => {
  31. this.props.updateBindingData('foo2Data', {
  32. params: {
  33. bar: 'foo'
  34. }
  35. });
  36. };
  37. render() {
  38. const {foo1Data, foo2Data} = this.props.bindingData;
  39. return (
  40. <div>
  41. <div>
  42. <Loading visible={foo1Data.__loading}>
  43. <div>
  44. foo1 的值: {foo1Data.foo}
  45. </div>
  46. </Loading>
  47. <div style={{marginTop: 10}}>
  48. <Button onClick={this.refreshFoo1}>请求获取 foo1 新数据</Button>
  49. </div>
  50. </div>
  51. <div style={{marginTop: 30}}>
  52. <Loading visible={foo2Data.__loading}>
  53. <div>
  54. foo2 的值: {foo2Data.foo}
  55. </div>
  56. </Loading>
  57. <div style={{marginTop: 10}}>
  58. <Button onClick={this.refreshFoo2}>请求获取 foo2 新数据</Button>
  59. </div>
  60. </div>
  61. <h3>当前页面是否有模块正在加载:{this.props.bindingData.__loading ? '是' : '否'}</h3>
  62. </div>
  63. );
  64. }
  65. }
  66. ReactDOM.render((
  67. <App />
  68. ), mountNode);

接口不符合规范

通过 responseFormatter 格式化接口返回数据,适配跟 DataBinder 接口规范不一致的情况。假设业务实际接口格式如下:json{ &#34;code&#34;: 0, &#34;msg&#34;: &#34;OK&#34;, &#34;content&#34;: { &#34;foo&#34;: &#34;&#34; }}

DataBinder 数据交互方案 - 图4

查看源码在线预览

  1. import React, {Component} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import DataBinder from '@icedesign/data-binder';
  4. import { Button, Loading } from '@alifd/next';
  5. @DataBinder({
  6. fooData: {
  7. url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/custom',
  8. responseFormatter: (responseHandler, body, response) => {
  9. // 拿到接口返回的 res 数据,做一些格式转换处理,使其符合 DataBinder 的要求
  10. const newBody = {
  11. status: body.code === '1' ? 'SUCCESS' : 'ERROR',
  12. message: body.msg,
  13. data: body.content
  14. };
  15. responseHandler(newBody, response);
  16. },
  17. defaultBindingData: {
  18. foo: 'bar'
  19. }
  20. }
  21. })
  22. class App extends Component {
  23. refreshFoo = () => {
  24. this.props.updateBindingData('fooData');
  25. };
  26. render() {
  27. const {fooData} = this.props.bindingData;
  28. return (
  29. <div>
  30. <Loading visible={fooData.__loading}>
  31. foo 的值: {fooData.foo}
  32. </Loading>
  33. <div style={{marginTop: 10}}>
  34. <Button onClick={this.refreshFoo}>请求获取新数据</Button>
  35. </div>
  36. </div>
  37. );
  38. }
  39. }
  40. ReactDOM.render((
  41. <App />
  42. ), mountNode);

自定义请求 callback

自定义请求成功或者失败的处理逻辑:比如接口成功不弹出 toast

DataBinder 数据交互方案 - 图5

查看源码在线预览

  1. import React, {Component} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import DataBinder from '@icedesign/data-binder';
  4. import { Button, Loading, Message } from '@alifd/next';
  5. @DataBinder({
  6. fooData: {
  7. url: 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/success',
  8. success: (body, defaultCallback, originResponse) => {
  9. if (body.status !== 'SUCCESS') {
  10. // 后端返回的状态码错误
  11. Message.error(body.message);
  12. } else {
  13. // // 成功不弹 toast,可以什么都不走
  14. console.log('success');
  15. }
  16. },
  17. // error 有两类错误,一类是网络中断,请求没有发送成功;另一类是服务器接口报错
  18. error: (originResponse, defaultCallback, err) => {
  19. // 失败弹 toast
  20. Message.error(err.message);
  21. },
  22. defaultBindingData: {
  23. foo: '默认值'
  24. }
  25. }
  26. })
  27. class App extends Component {
  28. refreshFoo = () => {
  29. this.props.updateBindingData('fooData', {
  30. params: {
  31. bar: 'foo'
  32. }
  33. });
  34. };
  35. render() {
  36. const {fooData} = this.props.bindingData;
  37. return (
  38. <div>
  39. <Loading visible={fooData.__loading} shape="fusion-reactor">
  40. <div>
  41. foo1 的值: {fooData.foo}
  42. </div>
  43. </Loading>
  44. <div style={{marginTop: 10}}>
  45. <Button onClick={this.refreshFoo}>请求获取新数据</Button>
  46. </div>
  47. </div>
  48. );
  49. }
  50. }
  51. ReactDOM.render((
  52. <App />
  53. ), mountNode);

自定义请求客户端

本 Demo 演示自定义 requestClient:使用 jsonp 的方法发送请求,自定义的 requestClien 必须返回一个 Promise

DataBinder 数据交互方案 - 图6

查看源码在线预览

  1. import React, {Component} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import querystring from 'querystring';
  4. import DataBinder from '@icedesign/data-binder';
  5. import jsonp from 'jsonp';
  6. import { Button, Loading } from '@alifd/next';
  7. /**
  8. * 自定义的 json request client
  9. */
  10. function request(opts) {
  11. return new Promise((resolve, reject) => {
  12. jsonp(opts.url + '?' + querystring.encode(opts.params), {
  13. name: 'callback'
  14. }, (err, data) => {
  15. if (err) {
  16. reject(err);
  17. } else {
  18. resolve({ data });
  19. }
  20. })
  21. });
  22. }
  23. @DataBinder({
  24. fooData: {
  25. url: 'https://sug.so.360.cn/suggest',
  26. defaultBindingData: {
  27. q: '默认值'
  28. },
  29. responseFormatter: (responseHandler, body, response) => {
  30. const newBody = {
  31. success: 'SUCCESS',
  32. message: 'ok',
  33. data: body
  34. };
  35. responseHandler(newBody, response);
  36. },
  37. }
  38. }, { requestClient: request })
  39. class App extends Component {
  40. refreshFoo = () => {
  41. this.props.updateBindingData('fooData', {
  42. params: {
  43. word: 'test'
  44. }
  45. });
  46. };
  47. render() {
  48. const {fooData} = this.props.bindingData;
  49. return (
  50. <div>
  51. <Loading visible={fooData.__loading}>
  52. foo 的值: {fooData.q}
  53. </Loading>
  54. <div style={{marginTop: 10}}>
  55. <Button onClick={this.refreshFoo}>请求获取新数据</Button>
  56. </div>
  57. </div>
  58. );
  59. }
  60. }
  61. ReactDOM.render((
  62. <App />
  63. ), mountNode);

自定义 DataBinder

很多场景下,我们会遇到诸如以下 case:- 业务接口规范跟 DataBinder 默认规范不一致- 需要全局处理接口通用规范,比如:未登录、无权限、出错等- 业务上使用非 AJAX 的方式做前后端通信,比如:jsonp这些场景我们建议业务上对 DataBinder 包装一层,产出业务自身的一个 DataBinder。

DataBinder 数据交互方案 - 图7

查看源码在线预览

  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import DataBinder from '@icedesign/data-binder';
  4. import { Button, Loading, Message } from '@alifd/next';
  5. /**
  6. * 自定义一个 DataBinder,建议放在 src/components/DataBinder 下。支持以下特性:
  7. * - 通过 showSuccessToast/showErrorToast 配置是否要弹 toast
  8. * - 通过 responseFormatter 格式化后端接口
  9. * - 基于 responseFormatter 实现未登录、无权限等逻辑
  10. */
  11. const CustomDataBinder = (options) => {
  12. // 重组 options
  13. let newOptions = {};
  14. Object.keys(options).forEach(dataSourceKey => {
  15. const config = options[dataSourceKey];
  16. const { showErrorToast = true, showSuccessToast = false } = config;
  17. newOptions[dataSourceKey] = {
  18. ...config,
  19. responseFormatter: (responseHandler, body, response) => {
  20. if (body.code === '-1') {
  21. // 未登录
  22. Message.error('未登录,即将跳转到登录页面');
  23. // location.reload();
  24. return;
  25. }
  26. if (body.code === '-2') {
  27. // 无权限
  28. Message.error('无权限,即将跳转无权限页面');
  29. // location.reload();
  30. return;
  31. }
  32. const newBody = {
  33. status: body.code === '0' ? 'SUCCESS' : 'ERROR',
  34. message: body.msg,
  35. data: body.content || {}
  36. };
  37. responseHandler(newBody, response);
  38. },
  39. success: (body, defaultCallback, originResponse) => {
  40. const {config} = originResponse;
  41. if (body.status !== 'SUCCESS') {
  42. // 后端返回的状态码错误
  43. if (config.showErrorToast) {
  44. Message.error(body.message);
  45. }
  46. } else {
  47. if (config.showSuccessToast) {
  48. Message.success(body.message);
  49. }
  50. }
  51. },
  52. error: (originResponse, defaultCallback, err) => {
  53. // 网络异常:404,302 等
  54. const {config} = originResponse;
  55. if (config.showErrorToast) {
  56. Message.error(err.message);
  57. }
  58. }
  59. };
  60. });
  61. return DataBinder.call(this, newOptions);
  62. };
  63. @CustomDataBinder({
  64. successData: {
  65. url:
  66. 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/ok',
  67. showSuccessToast: false
  68. },
  69. errorData: {
  70. url:
  71. 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/error',
  72. showErrorToast: true
  73. },
  74. networkErrorData: {
  75. url:
  76. 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/errorssss',
  77. showErrorToast: false
  78. },
  79. notLogin: {
  80. url:
  81. 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/not-login'
  82. },
  83. noAuth: {
  84. url:
  85. 'https://www.easy-mock.com/mock/5cc669767a9a541c744c9be7/databinder/no-auth'
  86. }
  87. })
  88. class App extends Component {
  89. updateData = key => {
  90. this.props.updateBindingData(key);
  91. };
  92. render() {
  93. const { successData, errorData, notLogin, noAuth, networkErrorData } = this.props.bindingData;
  94. const itemStyle = {
  95. margin: '10px 0',
  96. boderBottom: '1px solid #999',
  97. };
  98. return (
  99. <div>
  100. <div style={itemStyle}>
  101. <div>请求成功不弹 toast</div>
  102. <Loading visible={successData.__loading}>
  103. foo 的值: {successData.foo}
  104. </Loading>
  105. <div style={{ marginTop: 10 }}>
  106. <Button onClick={this.updateData.bind(this, 'successData')}>
  107. 获取新数据
  108. </Button>
  109. </div>
  110. </div>
  111. <div style={itemStyle}>
  112. <div>请求失败弹 toast</div>
  113. <Loading visible={errorData.__loading}>
  114. Error {errorData.__error && errorData.__error.message}
  115. </Loading>
  116. <div style={{ marginTop: 10 }}>
  117. <Button onClick={this.updateData.bind(this, 'errorData')}>
  118. 获取新数据
  119. </Button>
  120. </div>
  121. </div>
  122. <div style={itemStyle}>
  123. <div>请求失败不弹 toast</div>
  124. <Loading visible={networkErrorData.__loading}>
  125. Error {networkErrorData.__error && networkErrorData.__error.message}
  126. </Loading>
  127. <div style={{ marginTop: 10 }}>
  128. <Button onClick={this.updateData.bind(this, 'networkErrorData')}>
  129. 获取新数据
  130. </Button>
  131. </div>
  132. </div>
  133. <div style={itemStyle}>
  134. <div>请求未登录</div>
  135. <Loading visible={notLogin.__loading}>
  136. foo 的值: {notLogin.foo}
  137. </Loading>
  138. <div style={{ marginTop: 10 }}>
  139. <Button onClick={this.updateData.bind(this, 'notLogin')}>
  140. 获取新数据
  141. </Button>
  142. </div>
  143. </div>
  144. <div style={itemStyle}>
  145. <div>请求无权限</div>
  146. <Loading visible={noAuth.__loading}>foo 的值: {noAuth.foo}</Loading>
  147. <div style={{ marginTop: 10 }}>
  148. <Button onClick={this.updateData.bind(this, 'noAuth')}>
  149. 获取新数据
  150. </Button>
  151. </div>
  152. </div>
  153. </div>
  154. );
  155. }
  156. }
  157. ReactDOM.render(<App />, mountNode);

相关区块

DataBinder 数据交互方案 - 图8

暂无相关区块