在前端开发中向服务端请求数据是非常重要的,特别是在复杂的项目中对后台的api接口进行友好的调用是非常重要的(这里不得不说typeScript写起代码的体验是很爽的)。
基本思路,可以想后台一样进行接口封装,比如用户相关的接口一个MemberServices 里面有关于用户所有的api接口 例如getMemberById等,这样尽量语义话的调用。
思路如下:
(1)首先抽象出一个Api请求的对象接口(抽象类)
/**
* Api客户端请求接口
*/
export abstract class ApiClientInterface<T> {
/**
* 请求服务端client工具对象
* 非必填
*/
private _client?: any;
constructor(client: any) {
this._client = client;
}
get client(): any {
return this._client;
}
/**
* post请求
* @param option
*/
post = (option: T): any => {
}
/**
* get请求
* @param option
*/
get = (option: T): any => {
}
/**
* 抓取数据
* @param option
*/
fetch = (option: T): any => {
}
}
其中这个泛型T是一个请求配置对象,可以根据不同的项目或者是实现进行切换,这里提供一个基于阿里weex开源项目中stream请求对象封装的 ApiClient和API配置对象的示例
/**
* 请求服务端的api统一接口失效
*/
class ApiClient extends ApiClientInterface<WeexStreamOption> {
/**
* @param client WEEX 中的steam对象
*/
constructor(client: any) {
super(client);
}
/**
* post请求
*
*/
post = (option: WeexStreamOption) => {
option.method = ReqMethod.POST;
return this.fetch(option);
}
/**
* get请求
*/
get = (option: WeexStreamOption) => {
option.method = ReqMethod.GET;
return this.fetch(option);
}
/**
* 获取数据
* @param option
* 默认 Content-Type 是 ‘application/x-www-form-urlencoded’。
* 如果你需要通过 POST json , 你需要将 Content-Type 设为 ‘application/json’。
*/
fetch = (option: WeexStreamOption): any => {
option.data = isUndefined(option.data) ? {} : option.data;
const sign = this.sign(option.signFields, option.data); //获取签名字符串
if (option.method === ReqMethod.GET) {
//拼接参数
let params = null;
if (option.url.indexOf("?") > 0) {
params = "&";
} else {
params = "?";
}
params += this.joinParamByReq(option.data);
option.url += params;
option.url += "&sign" + sign;
} else if (option.method === ReqMethod.POST) {
option.data['sign'] = sign;
}
console.log("请求url--> " + option.url);
console.log("请求method--> " + option.method);
console.log("请求headers--> " + option.headers);
console.log("请求params--> " + JSON.stringify(option.data));
option.type = isUndefined(option.type) ? DataType.JSON : option.type;
let num = Math.round(Math.random() * 20)
let data = {
isSuccess: num % 2 === 0,
num: num
};
option.progressCallback(data);
setTimeout(function () {
option.callBack(data);
}, 1500)
//WEEX stream对象 https://weex.apache.org/cn/references/modules/stream.html
// this.client.fetch({
// method: ReqMethod[option.method], //请求方法get post
// url: option.url, //请求url
// type: DataType[option.type], //响应类型, json,text 或是 jsonp {在原生实现中其实与 json 相同)
// headers: option.headers, //headers HTTP 请求头
// body: JSON.stringify(option.data) //参数仅支持 string 类型的参数,请勿直接传递 JSON,必须先将其转为字符串。请求不支持 body 方式传递参数,请使用 url 传参。
// }, function (response) {
// console.log(response);
// /**
// * 响应结果回调,回调函数将收到如下的 response 对象:
// * status {number}:返回的状态码
// * ok {boolean}:如果状态码在 200~299 之间就为真
// * statusText {string}:状态描述文本
// * data {Object | string}: 返回的数据,如果请求类型是 json 和 jsonp,则它就是一个 object ,如果不是,则它就是一个 string。
// * headers {Object}:响应头
// */
// if (!response.ok) {
// //请求没有正确响应
// console.log("响应状态码:" + status + " 状态描述:" + response.statusText);
// return;
// }
// option.callBack(response.data, response.headers);
// }, function (resp) {
// console.log(resp);
// /**
// * 关于请求状态的回调。 这个回调函数将在请求完成后就被调用:
// * readyState {number}:当前状态state:’1’: 请求连接中opened:’2’: 返回响应头中received:’3’: 正在加载返回数据
// * status {number}:响应状态码.
// * length {number}:已经接受到的数据长度. 你可以从响应头中获取总长度
// * statusText {string}:状态文本
// * headers {Object}:响应头
// */
//
// if (option.progressCallback === null) {
// return;
// }
// option.progressCallback(resp);
// });
}
/**
* 将一个对象转换成url参数字符串
* @param req
* @return {string}
*/
private joinParamByReq(req: Object): String {
var result = "";
for (let key in req) {
result += key + "=" + req[key] + "&";
}
result = result.substr(0, result.length - 1);
return result;
}
/**
* ap请求时签名
* @param fields
* @param params
* @return {string}
*/
private sign = (fields: Array<String>, params: Object): String => {
if (isUndefined(fields) || isNull(fields)) {
return "";
}
let value = "";
fields.forEach(function (item) {
let param = params[item.toString()];
if (isUndefined(param)) {
// console.warn("参与签名的参数:" + item + " 未传入!");
throw new Error("参与签名的参数:" + item + " 未传入!");
}
value += item + "=" + param + "&";
});
value = "timestamp=" + params['timestamp']; //加入时间戳参与签名
let sign = md5(value);
return sign;
}
}
export default ApiClient;
/**
* weex stream请求参数对象
*/
export interface WeexStreamOption {
/**
* 请求url
*/
url?: String;
/**
* 请求方法
*/
method?: ReqMethod;
/**
* 结果数据类型
*/
type?: DataType;
/**
* 请求头
*/
headers?: Object;
/**
* 请求参数,仅post请求需要
*/
data?: Object;
/**
* 参与签名的参数列表
*/
signFields?:Array<String>;
/**
* 响应回调
*/
callBack?:Function;
/**
* 关于请求状态的回调。 这个回调函数将在请求完成后就被调用:
*/
progressCallback?:Function;
}
(2) 提供一个统一的实现,减少工作量,比起前端不是后端,不可能每一个接口都去实现一次,而且接口的流程非常统一,都是获取数据,只不过是url不同,参数不同,结果处理不同罢了。(这边实现使用Proxy对象)
抽象一个ServiceProxy对象
import ApiClient from "../ApiClient";
import {isUndefined} from "util";
import {DataType} from "./DataType";
import {ReqMethod} from "./ReqMethod";
/**
* 服务代理
*/
export default class ServiceProxy {
/**
*
* @param client 用于请求数据的客户端对象,非必填
* @param handler 接口对象
* @return {ServiceProxy} 返回代理对象
*/
constructor(client: any = null) {
const api = new ApiClient(client);
const handler = this;
console.log(handler);
console.log(api)
return new Proxy({}, {
get: function (target, key, receiver) {
return function () {
let params = arguments[0];
let options = arguments[1];
let methodName = arguments[2].toUpperCase(); //请求方法
let resultDataType = arguments[3].toUpperCase(); //结果数据类型
return new Promise(function (resove, reject) {
let config = handler[key](); //获取配置
if (isUndefined(config)) {
throw new Error("请求的方法: " + key.toString() + " 未定义");
}
options.url = config.url; //请求的url
options.signFields = config.signFields; //参与签名的请求参数
options.method = isUndefined(methodName) ? config.method : ReqMethod[methodName]; //请求方法 post、get
options.type = isUndefined(resultDataType) ? DataType.JSON : DataType[resultDataType]; //结果数类型
options.callBack = function (data) {
console.log("api接口" + config.url + " 返回数据-> " + data);
if (data.isSuccess) {
resove(data);
} else {
reject(data)
}
};
options.data=params;
let method = ReqMethod[options.method];
console.log(options);
api[method.toLowerCase()](options);
});
};
},
set: function (target, key, value, receiver) {
throw new Error("接口不允许设置值!");
}
});
}
}
(3)剩下的就是写具体的实现了,示例如下:
import ServiceProxy from “./api/base/ServiceProxy”;
import ApiConfig from “./api/base/ApiConfig”;
import {WeexStreamOption} from “./api/WeexStreamOption”;
import {TestReq} from “./TestReq”;
/**
* 测试服务接口
*/
export default class TestService extends ServiceProxy {
constructor(client?: any) {
super(client);
}
/**
* 该方法写法固定,可以考虑使用自动生成
* @param params 请求参数
* @param option 请求配置
* @param method 请求类型
* @param dataType 结果数据类型
* @return {ApiConfig}
*/
testApi(params: TestReq, option: WeexStreamOption = {}, method?: String, dataType?: String): any {
return ApiConfig.newInstance("/api/test.htm", ["userName", "phoneCode"]);
};
}
服务中的方法实现非常统一,只是方法名和放回的ApiConfig对象中的数据不一样。这样实现以后对于服务调用就非常清晰了。
更多细节可以参考代码,项目地址在文章开头。