DataBinder 数据交互方案

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

API

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

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

使用

1. 配置数据源

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 {
  ...
}

详细的解释下:

2. 请求并使用数据源

对某个 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。

自定义前后端通信方式 requestClient

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

自定义请求 callback

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

业务自定义 DataBinder

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

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 请求获取数据,POST 请求数据时,请求参数可以放在 url query 或者 body 上,具体由接口实现决定:

  • url query: 通过 params 参数指定
  • body: 通过 data 参数指定

具体可参考 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);

自定义请求 callback

自定义请求成功或者失败的处理逻辑:比如接口成功不弹出 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);

自定义 DataBinder

很多场景下,我们会遇到诸如以下 case:

  • 业务接口规范跟 DataBinder 默认规范不一致
  • 需要全局处理接口通用规范,比如:未登录、无权限、出错等
  • 业务上使用非 AJAX 的方式做前后端通信,比如:jsonp

这些场景我们建议业务上对 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);