项目地址
在前端开发中向服务端请求数据是非常重要的,特别是在复杂的项目中对后台的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对象中的数据不一样。这样实现以后对于服务调用就非常清晰了。
更多细节可以参考代码,项目地址在文章开头。