大家好,我是入错行的bug猫。(http://blog.csdn.net/qq_41399429,谢绝转载)
本文主要解决大型项目中,海量ajax的统一管理方法
公司所做的产品系统,均是严格意义上的前后端分离,数据交换全部通过ajax+json完成。
页面上大量地使用到ajax,在后续产品迭代过程中,如果需要修改一些关于ajax底层东西:比如修改超时配置、调整加载过程中的图片、url地址调整、后台系统换地址、统一风格的异常提示等等。假设没有一个集中管理方案,这些改动简直是灾难!
一般解决方案是,在一些框架(jquery、angular、vue)的ajax基础之上,做一次封装,创建一个全局的ajax对象等。总之,在做前端规划的时候,肯定会意识到,必须要有一个高可复用、或者统一的ajax请求方法。
作为一个专业的前端,bug猫其实不想关注ajax调用的地址是多少、如果发生了http异常*(400、404、500、505)*之后要怎么做、如果发生了业务异常怎么处理、用户未登录异常、token超时异常等等这很low的东西!
如果每发起一个ajax请求,bug猫都要做这一系列的判断,那么bug猫肯定陷入了无限加班凄惨的命运QwQ
bug猫只想关注页面做得怎么绚丽、ajax的入参是什么、正常返回的报文是什么!
还有开发环境的后台地址是一个,测试环境、正式环境的地址是另外一个,有时甚至还要连后台开发人员的地址。
总之,bug猫也不想总是ajax的ip地址换来换去!还有隔壁同事偶尔把ip地址修改之后,忘记改回来提交了…
bug猫在此决定开源自己写的一个框架,集中式ajax配置管理。
所以跟着bug猫的思路来,了解集中式ajax配置管理是怎么解决上述问题的!
集中管理设计:
每一个ajax请求,看似都是各自独立、没有任何联系,但是仔细分析可以得出一些共性:
- 都需要请求地址
- 都有请求参数(没有请求参数按空字符串处理)
- 都有请求方式(get|post|jsonp)
- 都有请求成功后执行方法(没有按空方法处理)
- 请求失败后执行方法(没有按制定的默认行为处理)
如果仅以请求地址为维度*(不包括get请求url上的参数、和通过url传递参数的情况)*,那么系统中所有的ajax请求都能穷举出来。
因此可以将每一个ajax请求,预置为一个函数:里面封装好url、请求方式、同步异步、是否需要加载动画,统一调用公共的异常处理方案,等等。
执行这个函数,传入入参、业务处理成功后的函数、业务处理失败后的函数,等价于发起这个ajax请求。
对于前端开发而言,只需要执行这些预置函数,传入指定参数即可。
eg:传统ajax方案:
$http({url: host + "/1.0.0/login", method: "post", data:{xxx}})
.then(function(resp){
//需要判断ajax是否请求成功、业务是否处理成功还是失败
});
//或者:
$http.post(host + "/1.0.0/login", {xxx})
.success(function(resp){
//成功之后执行的方法。
//其中,业务处理失败,是算成ajax请求成功、还是属于ajax请求失败,需要根据后台框架决定,判断逻辑是放在success、还是error方法里面
}).error(function(err) {
//http异常后的方法
});
注:ajax请求成功,业务处理失败:比如发起订单取消,后台系统处理之后,发现订单因某种原因不可以取消。那么处理结果成功返回到前端,就属于ajax请求成功,业务处理失败。
集中式管理方案:
//预置ajax函数
//用户登录
//param:入参
//success:业务处理成功之后执行的方法,
//error:业务处理失败之后执行的方法,可以省略
function userLogin(param, success, error){
//预置函数体
//处理:具体访问地址、请求方式、http异常(400,404,500,304)处理、同步异步、是否需要加载动画、默认的业务处理失败之后执行方案
}
//使用时,不需要关注具体访问地址、请求方式、http异常(400,404,500,304)处理、同步异步、是否需要加载动画。
//这些配置全部在预置ajax函数中,已经配置好了
userLogin({xxxx}, function(resp){
//业务处理成功之后执行的方法
}, function(resp){
//业务处理失败之后执行的方法,可以省略
});
在所有需要使用用户登录的地方,直接执行userLogin即可,保证了代码的复用性,和简洁!
代码结构示意图:
应用层 ──── 应用层关注入参、业务处理,调用预置函数
│
│
预置函数 ──── 预置ajax函数,根据配置,调用$get、或者$post、或者$jsonp
│
┌──────┼──────┐
│ │ │
$get $post $jsonp ──── 根据配置,在函数中追加配置:同步异步、是否需要加载动画、请求超时,调用基层_ajaxSend
│ │ │
└──────┼──────┘
│
_ajaxSend ──── 公共的处理:http异常(400,404,500,304)处理、加载动画、默认的业务处理失败之后执行方案
│
│
$http ──── 这一层负责发起ajax请求,具体实现代码无需关注,甚至不是angular的$http,是juqery的$.ajax都无所谓
看似很美好,但是!!!!!!!!
一个稍微大型的管理系统,ajax的数量可能会达到上千!
即便我们能不辞辛劳地为每一个ajax设置一个预置函数,
但是,这样会导致在页面加载之后,会出现上千个函数!
会不会影响性能、会不会造成浏览器露出底裤,bug猫没有测试过,
但是要写上千个函数,bug猫从来都没想要干过!
要是有一个方法,可以在需要某个预置函数的时候,能动态生成它就好了!
嗯,简单来说,就是根据配置,动态生成一个函数!
上千个预置函数,要想指定动态生成某一个,至少要给每个预置函数起个名字吧!
但是又会出现操蛋的命名冲突!方案有隐患,pass!
上述说到的,每个ajax预置函数,至少有请求地址、请求方式两项配置,我们可以把配置声明成为一个对象,存放到一个"仓库"里面,
给函数取名会有命名冲突,我们给配置取名称,总行了吧?
给配置加上不同的命名空间:将"仓库"按照功能模块,分成不同的"容器",容器里面可以再放"容器",也可以直接放配置。
//ajax配置
var configs = {
//登录注册模块
login : {
//登录ajax配置
userLogin:{url:"xxx", method:"post"},
//退出ajax配置
logout : {url: "xxx", method: "get", loading:"0"},
},
//个人中心模块
home : {
},
//商品模块
goods : {
},
}
现在ajax配置有自己的名字了,那预置函数的名字……?
还有存在的必要吗?因为预置函数,是实时通过ajax配置生成的,在同一时刻只可能存在一个。
我们假设有一个"工厂",可以根据configs.xxx.xxx生产不同的预置函数,并且将其返回到应用层,
ajax配置 返回
应用层 ─────────────────> 工厂 ────────────> 预置函数
configs是全局的配置,如果在表现层能直接获取到configs的引用,是十分危险的!
危险的东西必须隐藏起来!
//通过ajax配置命名空间,获取ajax配置
//operId:ajax配置的命名空间,示例:"login.userLogin","login.logout",全部基于configs对象
//configs:configs对象
var getValues = function(operId, configs) {
var operIds = operId.split("."), obj = configs;
for(var i = 0, l = operIds.length; i < l; i++) {
var key = operIds[i];
obj = obj[key];
}
return obj;
}
现在只有单个的ajax配置对象,返回给了应用层。
不行,还是危险,需要再次封装!
而且,当有发起多个ajax请求,要求在这些ajax执行完毕之后,在执行某些操作的应用场景,
angular、jquery、VUE等框架,都有配套的方法,
因此bug猫的集中式ajax管理框架中也不能少!
上代码,以angular框架为例:
page1
/**
* 注册三个对象,分别为开发、测试、线上环境相关的配置
* */
app.constant("devconfig", {apiurl :"http://192.168.3.90:8070"})
.constant("testconfig", {apiurl : "http://测试环境地址"})
.constant("prodconfig", {apiurl : "http://线上环境地址"});
page2
/**
* 注册一个名为$conn的服务,依赖$http,和环境配置("devconfig")
* 在打包的时候,将"devconfig"字符串,替换成page1中对应的环境名称
* $http:angular的$http服务
* devconfig:依赖注入的环境配置
* */
app.factory("$conn",["$http","devconfig",function($http,conf){
//从环境配置中获取后台服务地址
//如果在本地需要修改后台地址,在page1中修改,禁止开发人员修改 "devconfig",和apiurl的值
//通过svn、或者git禁止page1提交,这样可以防止开发人员误将配置文件提交
var apiurl = conf.apiurl;
//所有的ajax都在configs对象中集中配置
var configs = {
//登录注册模块
login : {
//此处为ajax的配置, url:请求的地址,method:请求方式 get(键值对)|post(键值对)|jsonp(json字符串)
//还可以有其他的额外配置,比如超时、是否需要加载动画、同步异步等等
login : {url: apiurl + "/1.0.0/login", method: "jsonp"}, //登录
logout : {url: apiurl + "/1.0.0/logout", method: "get"}, //退出
register:{url: apiurl + "/1.0.0/userRegister",method: "jsonp"}, //注册
},
//个人中心模块
home : {
editPwd: {url: apiurl + "/1.0.0/vipEmpEditPwd",method: "jsonp"},//修改密码
},
//商品模块
goods : {
},
//此处模块下,可以有多级子模块
};
//factory返回对象
var conn = {};
//公共的默认参数
var defaults = {
//请求头文件
headers: {},
timeoutError: function(msg) {
msg = msg || "服务器失去响应,请刷新后重新尝试!";
swal.error(["似乎网络开小差了", msg]); //公司的ui,不用在意
conn.overlayHide(); //隐藏加载动画
},
httpError: function(msg) {
msg = msg || "您当前网络不畅,请刷新后重新尝试!";
swal.error(["似乎网络开小差了", msg]);
conn.overlayHide();
},
businessError: function(tips) {
tips = tips || "";
swal.error(["系统出错了,请刷新后重新尝试!", tips]);
conn.overlayHide();
}
};
//加载动画
var overlay = {};
//正在执行的ajax次数
//如果同时发起了多个ajax请求,那么应该在最后一个ajax结束之后,才隐藏加载动画
overlay.overlayCount = 0;
//显示遮罩层,每发起一个ajax请求,都要执行该方法
//ajaxConf:为ajax配置对象
overlay.layerOverlayShow = function(ajaxConf) {
//如果在ajax配置中,设置的loading=1、或者为undefined,表示在执行ajax过程中,需要加载动画,
ajaxConf.loading = !ajaxConf.loading ? "1" : "0";
//计算器加一,并且显示加载动画
if(ajaxConf.loading == "1"){
overlay.overlayCount ++ ;
$("#layerOverlay:first").show();
}
};
//隐藏遮罩层,每个ajax执行完毕之后,都要执行该方法
//ajaxConf:为ajax配置
overlay.layerOverlayHide = function(ajaxConf) {
//如果在ajax配置中,设置的loading=1,表示在执行ajax过程中,需要加载动画
//在ajax结果返回之后,计算器减一
if(ajaxConf.loading == "1"){
overlay.overlayCount -- ;
}
//当计算器为0时,表示所有的ajax都返回结果了,隐藏加载动画
if( overlay.overlayCount <= 0 ){
overlay.overlayCount = 0;
$("#layerOverlay:first").hide();
}
};
/**
* 使用get方式,发送键值对
* $http:angular的$http服务
* url:请求地址
* param:请求参数,支持"&key=value&k=v"字符串,也支持对象
* success:ajax执行成功,并且业务执行成功之后,执行的方法
* error:ajax执行成功,但是业务处理失败之后,执行的方法;如果不传入,默认执行 defaults.businessError
* ajaxConf:传入的ajax配置对象
* */
var $get = function($http, url, param, success, error, ajaxConf) {
//设置ajax的请求头
var headers = angular.extend({}, defaults.headers);
headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8";
var _ajax = {
url: url,
method: "get", //"get|post|jsonp"
headers: headers,
};
var param = param || "";
if(typeof(param) == "string") {
url = url + param;
} else {
_ajax.params = param;
}
_ajax.url = url;
_ajaxSend($http, _ajax, success, error, ajaxConf);
};
/**
* 使用post方式,发送键值对
* $http:angular的$http服务
* url:请求地址
* param:请求参数对象
* success:ajax执行成功,并且业务执行成功之后,执行的方法
* error:ajax执行成功,但是业务处理失败之后,执行的方法;如果不传入,默认执行 defaults.businessError
* ajaxConf:传入的ajax配置对象
* */
var $post = function($http, url, param, success, ajaxConf) {
//设置ajax的请求头
var headers = angular.extend({}, defaults.headers);
headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8";
param = param || {};
var _ajax = {
url: url,
method: "post", //"get|post|jsonp"
headers: headers,
params: param
};
_ajaxSend($http, _ajax, success, error, ajaxConf);
};
/**
* 使用post方式,发送json字符串
* $http:angular的$http服务
* url:请求地址
* param:请求参数对象,支持json字符串
* success:ajax执行成功,并且业务执行成功之后,执行的方法
* error:ajax执行成功,但是业务处理失败之后,执行的方法;如果不传入,默认执行 defaults.businessError
* ajaxConf:传入的ajax配置对象
* */
var $jsonp = function($http, url, json, success, error, ajaxConf) {
//设置ajax的请求头
var headers = angular.extend({}, defaults.headers);
headers["Content-Type"] = "application/json;charset=utf-8";
json = json || "";
if(!!json && typeof(json) !== "string") {
json = JSON.stringify(json);
}
var _ajax = {
url: url,
method: "post", //"get|post|jsonp"
headers: headers,
data: json
};
_ajaxSend($http, _ajax, success, error, ajaxConf);
}
/**
* 发送ajax
* */
var _ajaxSend = function($http, _ajax, success, error, ajaxConf) {
//ajaxConf.beforeFun:在执行ajax之前执行的方法,如果该方法返回false,则阻止ajax
if(typeof(ajaxConf.beforeFun) == "function" && ajaxConf.beforeFun(_ajax) === false) {
return false;
}
//在ajax请求头中,追加token
_ajax.headers.token = window.sessionStorage.getItem("token") || "";
//默认超时时间 60s
_ajax.timeout = ajaxConf.timeout || 60000;
//显示遮罩层
overlay.layerOverlayShow(ajaxConf);
//使用angular的$http发起请求
$http(_ajax).then(function(resp) {
//关闭遮罩层
overlay.layerOverlayHide(ajaxConf);
//ajaxConf.thenBefore:在ajax返回结果之后,立即执行(优先success、error执行)。如果该方法返回false,则立即结束
if(typeof(ajaxConf.thenBefore) == "function" && ajaxConf.thenBefore.call(this, resp, _ajax) === false) {
return false;
}
//http请求成功,正常返回
if(!!resp && resp.status == 200) {
if(resp.data.errCode == 1) {//公司定义,errCode=1表示业务处理成功,其他表示失败
success = success || noop;
success(resp.data);
} else if(resp.data.errCode == 3901 || resp.data.errCode == 4001) {
swal.error("登录会话已失效,请重新登录!", function() {
window.location.href = "/#/access/signin";
});
} else if(resp.data.errCode == 3902) {
swal.warning("无权限访问!");
} else if(resp.data.errCode == 3003){
swal.error(["参数不合法", resp.data.errMsg]);
} else { //业务处理异常
if(!!error && typeof(error) == "function") {//如果没有传入error方法,则执行默认的defaults.businessError
error(resp.data);
} else {
defaults.businessError(resp.data.errMsg);
}
}
} else { //http 异常
defaults.httpError();
};
//ajaxConf.thenAfter:在执行完success、或error方法之后执行
if(typeof(ajaxConf.thenAfter) == "function" && ajaxConf.thenAfter.call(this, resp, _ajax) === false) {
return false;
}
}, function(resp) {//超时异常
//关闭遮罩层
overlay.layerOverlayHide(ajaxConf);
if(typeof(ajaxConf.httpError) == "function" && ajaxConf.httpError.call(this, resp, _ajax) === false) {
return false;
}
defaults.timeoutError();
});
};
/**
* 通过ajax配置命名空间,获取ajax预置函数
* operId:ajax配置命名空间,示例:"login.userLogin","login.logout",全部基于configs对象
* uerConf:自定义配置,虽然有预置配置,但是不保证没有fuck的个性化配置,key省略不传
* */
conn.getConn = function(operId, uerConf) {
var operIds = operId.split(".");
return creart(getValues(operIds, configs), uerConf);
}
// 应用场景:当有发起多个ajax请求,要求在这些ajax执行完毕之后,在执行某些操作
// 参数:ajaxfun.lay, ajaxfun.lay, ajaxfun.lay, ..., lastdo
// 每个 ajaxfun.lay 都是独立的,有自己独立的回调函数
// lastdo是这些ajax都执行完毕之后,才执行的函数,入参是按ajaxfun.lay的排列顺序,返回的结果集组成的数组
conn.when = function() {
var args = [].slice.call(arguments);
var argsCount = args.length - 1,
lastdo = args[argsCount],
resps = new Array(argsCount);
for(var i = 0, l = argsCount; i < l; i++) {
var lay = args[i];
var thenAfter = lay.thenAfter || $.noop;
lay.thenAfter = (function(resps, i) {
return function(){
thenAfter.apply(this, arguments);
resps[i] = arguments[0];
argsCount--;
if(argsCount <= 0) {
lastdo.apply(this, resps);
}
}
})(resps, i);
lay.send();
}
}
/**
* 通过ajax配置命名空间,获取ajax配置
* */
var getValues = function(operIds, configs){
var obj = configs;
for(var i=0,l=operIds.length;i<l;i++){
var key = operIds[i];
obj = obj[key];
}
return obj;
}
//仅创建ajax预置对象
function onlyLay(ajaxfun, param, success, error, beforeFun, thenBefore, thenAfter) {
return {
param: param,
success: success,
error: error,
beforeFun: beforeFun,
thenBefore: thenBefore,
thenAfter: thenAfter,
send: function() {
ajaxfun(this.param, this.success, this.error, this.beforeFun, this.thenBefore, this.thenAfter);
}
}
}
/**
* 根据配置,创建ajax预置函数
* option:ajax配置
* uerConf:自定义配置
* */
var creart = function(option, uerConf) {
var method = option.method.toUpperCase();
//ajax预置函数
var fun = null;
//get请求
if(method == "GET") {
//闭包,立即执行返回一个ajax预置函数
fun = (function(o) {
/**
* ajax预置函数
* param:入参
* success:业务处理成功
* error:业务处理失败
* beforeFun:在执行ajax之前执行的方法,如果该方法返回false,则阻止ajax,可忽略不传
* thenBefore:在ajax返回结果之后,立即执行(优先success、error执行)。如果该方法返回false,则立即结束,可忽略不传
* thenAfter:在执行完success、或error方法之后执行,可忽略不传
* */
var ajaxfun = function(param, success, error, beforeFun, thenBefore, thenAfter) {
uerConf = $.extend(uerConf, {
"beforeFun": beforeFun,
"thenBefore": thenBefore,
"thenAfter": thenAfter,
"loading": o.loading,
"timeout": o.timeout
});
$get($http, o.url, param, success, error, uerConf);
};
//仅创建ajax预置函数对象,配合conn.when方法
ajaxfun.lay = function(param, success, error, beforeFun, thenBefore, thenAfter) {
return onlyLay(ajaxfun, param, success, error, beforeFun, thenBefore, thenAfter);
}
/**
* 最终返回到应用层的预置函数,应该是这样:ajaxfun(xxx)和ajaxfun.lay(xxx)包含两种方法,
* 第一种方法执行之后,生成ajax预置函数,可以直接执行,然后立即发起ajax请求;
* 第二种仅生成ajax配置对象,不可以执行。需要手动执行对象的send方法才会发起请求,一般配合conn.when方法使用
* */
return ajaxfun;
})(option);
} else if(method == "JSONP") { //post方式,发送json对象/json字符串。在Controller中,使用@RequestBody封装成对象
fun = (function(o) {
var ajaxfun = function(json, success, error, beforeFun, thenBefore, thenAfter) {
uerConf = $.extend(uerConf, {
"beforeFun": beforeFun,
"thenBefore": thenBefore,
"thenAfter": thenAfter,
"loading": o.loading,
"timeout": o.timeout
});
$jsonp($http, o.url, json, success, error, uerConf);
};
//仅创建ajax对象
ajaxfun.lay = function(json, success, error, beforeFun, thenBefore, thenAfter) {
return onlyLay(ajaxfun, json, success, error, beforeFun, thenBefore, thenAfter);
}
return ajaxfun;
})(option);
} else if(method == "POST") { //post方式,发送json对象,在Controller中,直接使用对象/键 接收
fun = (function(o) {
var ajaxfun = function(param, success, error, beforeFun, thenBefore, thenAfter) {
uerConf = $.extend(uerConf, {
"beforeFun": beforeFun,
"thenBefore": thenBefore,
"thenAfter": thenAfter,
"loading": o.loading,
"timeout": o.timeout
});
$post($http, o.url, param, success, error, uerConf);
};
//仅创建ajax对象
ajaxfun.lay = function(param, success, error, beforeFun, thenBefore, thenAfter) {
return onlyLay(ajaxfun, param, success, error, beforeFun, thenBefore, thenAfter);
}
return ajaxfun;
})(option);
}
return fun;
};
return conn;
}]);
page3
//将page2依赖注入到应用层
app.controller('bugCatCtrl', ["$scope", "$http", "$conn", function($scope, $http, $conn) {
var userLogin = $conn.getConn("login.login");
userLogin({userName:"bugCat","paswd":"bugCat123"},function(resp){
//登录成功
});
var logout = $conn.getConn("login.logout");
userLogin("",function(resp){
//退出成功
},function(resp){//业务处理失败方法,可以省略
//退出失败
});
var demo1 = $conn.getConn("xxx.xxx", {
"beforeFun":function(ajax){
console.log(ajax.param);
console.log("在发起ajax之前执行,如果返回false,ajax不会执行");
return false;
},
"thenBefore":function(){
console.log("在success、error之前执行,如果返回false,success、error都不会执行");
return false;
},
"thenAfter":function(){
console.log("在success、error之后执行");
},
loading:0 //不需要加载动画
});
demo1({},function(resp){
console.log("我是业务处理成功后执行方法");
},function(resp){
console.log("我是业务处理失败后执行方法");
});
demo1({},function(resp){
console.log("我是业务处理成功后执行方法");
},function(resp){
console.log("我是业务处理失败后执行方法");
},function(ajax){
console.log(ajax.param);
console.log("在发起ajax之前执行,如果返回false,ajax不会执行。会覆盖beforeFun");
return false;
}function(resp){
console.log("在success、error之前执行,如果返回false,success、error都不会执行。会覆盖thenBefore");
}function(resp){
console.log("在success、error之后执行。会覆盖thenAfter");
});
var demo2 = $conn.getConn("xxx.xxx");
var demo3 = $conn.getConn("xxx.xxx");
var demo4 = $conn.getConn("xxx.xxx");
$conn.when(
demo2.lay({}),
demo3.lay({"p":"1"},function(resp){
console.log("我是业务处理成功后执行方法");
}),
demo3.lay({"p":"2"},function(resp){
console.log("我是业务处理成功后执行方法");
}),
demo4.lay("",function(resp){
console.log("我是业务处理成功后执行方法");
}),
function(respArr){
console.log("我是上述ajax都执行之后,再执行的方法");
console.log(respArr);
}
);
}]);
应用层变得简洁多了,基本上在configs中配置完毕后,满足90%的地方使用!
个别ajax,可以通过conn.getConn("", {});
的第二个参数做适配。
换到jquery框架、或者vue下,同样需要将page1和page2分开。
在page2通过模块加载、或者通过全局对象,引入page1的环境配置。
只需要根据框架,调整$get
、$post
、$jsonp
、_ajaxSend
方法,表现层完全不需要做任何修改!
如果后台框架返回的报文格式变了,只需要修改_ajaxSend
方法中,判断业务成功、业务失败的代码!
结束语
项目地址:https://gitee.com/qq283365011/WebAP5
这是bug猫和同事做的一个开源项目,里面就用到了ajax集中管理模式,有兴趣的可以试试。
~THE END~