从早期从事基于java的服务器端开发,再到之后从事基于web和js的ui开发,总体感觉基于web页面的ui开发远不如服务器端健壮。主要是早期ie浏览器功能太弱小,很多业务被迫放到服务器端去实现,浏览器端技术设计的比较简单。其次,js这门语言对于异常里也不够完善,没有一套足够完善的异常处理思路,尽管js存在throw、try、catch这种异常的语法,但是js本身是一门函数式编程的语言,try、catch在很多回调场景中是那么无力。
所以一直以来我就想构建出一个完善的js异常处理机制,而且还要基于具体ui实际需求,做出具体的处理逻辑。简单来说,当boss问我们做这种统一的异常处理有什么好处时,作为前端工程师,最理想的回答就是增加用户体验,作为前段工程师,用户体验是我们最大的价值,因为只有增加用户体验,公司才能留住用户,保持盈利,我们才能发挥出价值。那么如何提升用户体验呢?当用户遇到了不同的错误情况,使用最适合的提示方式提供给用户,不就是增加了用户的使用体验吗?
所以统一的异常处理的目的就是,能够使用简单、严谨的思路让程序员开发出更容易维护的代码,增加前端js代码的健壮性,同时将异常处理的提示具体实现和业务逻辑解耦,由统一的提示接口去将处理异常信息。
那么这我们提出设计方案之前,先来分析一下传统的异常处理方法,以及js中使用时候的问题,也来想想为什么我们平时在js中使用异常处理的情况是那么少。
1.责任链模式的异常处理理论
传统的异常处理理论,就是一个责任链模式,当方法A调用方法B,方法B调用方法C,其中每一个方法其实都可能抛出异常,可能处理异常。
function f(){ try{ } catch(e){ //什么也不做 throw e; } } //等效于 function f(){}
本身方法调用就是一个链,而异常处理则处于这个链上。当一个方法抛出一个异常时,他本也是异常的第一个接收人(catch到异常),如果是他职责身份能处理的异常,他就应该立刻处理此异常,不再向上抛出;否则就应该向上抛出,抛到上一层方法。依次类推,直到抛到最顶层。通常这个最顶层是一个用户或者系统调用接口,这个接口作为方法调用的发起者,同时也应该是异常处理的最终响应者,在这一层我们将真正地去处理底层方法无法处理的异常。
如何判断一个异常是一个方法能否处理的呢?依据“最小知道原则”和“职责单一原则”,一个异常如果属于其业务范围内的一部分,就应该处理这个异常,如果需要外界知晓这个异常存在,就应该将这个异常加工后抛到上一层。
function fa(){ try{ fb(); }catch(e){ if(e == "方法a能处理的异常"){ console.log("方法a处理了异常") } else { console.log("方法a无法处理此异常,继续向上抛出") throw e; } } } function fb(){ try{ fc(); }catch(e){ if(e == "方法b能处理的异常"){ console.log("方法b处理了异常") } else { console.log("方法b无法处理此异常,继续向上抛出") throw e; } } } function fc(){ try{ throw "方法acb都不能处理此异常"; //throw "方法a能处理的异常"; //throw "方法b能处理的异常"; //throw "方法c能处理的异常"; }catch(e){ if(e == "方法c能处理的异常"){ console.log("方法c处理了异常") } else { console.log("方法c无法处理此异常,继续向上抛出") throw e; } } } (function(){ try{ fa() }catch(e){ console.log("最顶层处理了此异常"); } })()
其中a、b、c中如果没有可以处理的异常,try、catch语句是可以省略掉的,这样代码就会简写为
function fa(){ fb(); } function fb(){ fc(); } function fc(){ throw "方法acb都不能处理此异常"; } (function(){ try{ fa() }catch(e){ console.log("最顶层处理了此异常"); } })()
是不是简单了不少,我们实际开发中更多的是这种例子,因为一个函数出现错误,后续执行都将无法正常进行,所以是需要向上层抛出的。因此js语法中的异常处理策略简化了整个过程。
所以说,传统的异常处理就是一个责任链模式。然而js中真的就可以使用这种责任链模式的异常处理吗?为什么我们在开发js中,很少采用这种责任链模式的异常处理呢?接下来我们继续介绍js的异步调用。
2.js异步调用的异常处理
js是个很神奇的语言,用这个语言你可以像用c语言那样在全局变量里,面向过程地编写你的代码,也可以像使用java那种,面向对象地编写代码,他还可以使用现状渐渐开始火起来在函数式的方法编写代码。
在js里面,函数可以向变量一样被声明,可以做方法的入参,可以做方法的返回值,这些都是js不可或缺的语法特性。
因为js还有没有语法级的阻塞方案,这样你无法同步两个不同的线程能够同步彼此,例如一个异步请求,或者调用。当调用一个异步方法,因为没有语法级的阻塞方式,所以整个调用过程中,你无法顺序地编写调用过程,唯一能做的事情只有回调。
这些语法特性使得我们开发的时候,只能做到基于函数回调情况去处理不同的状况,而责任链模式的异常处理也变得不再适用。
function f(){ throw "error"; } try{ setTimeout(f) } catch(e){ console.log(e) //接收不到这个error的 }
这种情况在实际开发中会经常遇到。最常见的就是一个ajax的异步请求,除此之外,异步io的api、地理定位的api、摄像机的api,这些都是异步的。再比如我们模拟一个alert方法,不使用系统的alert而是我们自己的alert(系统的alert函数调用后会弹出真正的模式对话框,此时线程挂起,运行阻塞,当用户点掉alert对话框后线程才会唤起)。与系统alert不一样,我们自己做的alert只能在回调里加alert后续的业务方法。
//使用Window对象的alert alert("我被阻塞了"); console.log("执行完毕"); var myAlert = { show: function(fn) { //绘制alert对话框略 //监听用户点击事件,点击后回调fn函数 var body = document.querySelector("body"); var _fn = function() {
//去除对话框略 body.removeEventListener("click", _fn) fn && fn(); } body.addEventListener("click", _fn, false); } } //调用自定义的alert myAlert.show(function() { console.log("执行完毕"); })
这种回调在js程序设计中常常被用到,因为这种方式是不支持责任链模式的,所以try、catch这种异常处理的语法在这种调用中很少会被使用到。那么这种基于回调的调用过程,一般使用什么方法做异常处理呢?通常函数本身会有一个错误的回调入参,参数要求是一个我们还是使用a、b、c这三个方法彼此调用来说明。
function fa(error){ var errorFn = function(e){ if(e == "方法a能处理的异常"){ console.log("方法a处理了异常") } else { console.log("方法a无法处理此异常,继续向上抛出") error && error(e); } } setTimeout(function(){ fb(errorFn); }) } function fb(error){ var errorFn = function(e){ if(e == "方法b能处理的异常"){ console.log("方法b处理了异常") } else { console.log("方法b无法处理此异常,继续向上抛出") error && error(e); } } setTimeout(function(){ fc(errorFn); }) } function fc(error){ var errorFn = function(e){ if(e == "方法c能处理的异常"){ console.log("方法c处理了异常") } else { console.log("方法c无法处理此异常,继续向上抛出") error && error(e); } } setTimeout(function(){ try{ throw "方法acb都不能处理此异常"; //throw "方法a能处理的异常"; //throw "方法b能处理的异常"; //throw "方法c能处理的异常"; }catch(e){ errorFn(e); } }) }
fa(function(){
console.log("最顶层处理了此异常");
})
注意这里的errorFn是不可以省略的,因为这个责任链模式是我们手动书写出来的。所以要想实现异步过程的责任链模式,是必须通过自己手动完成的,js并没有提供什么语法糖帮我们简化这个过程(其实也是有的)。
而且,只有最里层的fc当中使用try、catch语句,fb、fa都无法再使用try、catch语句了,因为一旦使用了这种回调方案,就再也无法回归传统的try、catch处理了,这也是try、catch语法无法在js里面流行的一大原因。
这里基本分析出了传统js代码在异常处理方面的方案和出现的问题,如何更好地解决异常处理问题,提供我们程序的健壮性我们值得进一步思考,待续。。。。