angular代码分析之异常日志设计
错误异常是面向对象开发中的记录提示程序执行问题的一种重要机制,在程序执行发生问题的条件下,异常会在中断程序执行,同时会沿着代码的执行路径一步一步的向上抛出异常,最终会由顶层抛出异常信息。而与异常同时出现的往往是日志,而日志往往需要记录具体发生异常的模块、编码、详细的错误信息、执行堆栈等,方便问题的快速定位分析。angularjs作为前端的框架,其对异常信息的处理是怎样的呢?
一、定义模块和异常类型
首先每个模块的日志模块编码是一样的,只需要本模块负责初始化一次即可。通过以下代码我们可以看到angular通过minErr函数传入module参数,并在返回的闭包函数中引用module参数实现这一点。
//构造模块异常类,module为模块编码,ErrorConstructor异常构造函数 function minErr(module, ErrorConstructor) { //如果未传入异常构造函数则使用Error ErrorConstructor = ErrorConstructor || Error; //返回异常对象构建方法 return function() { }; }
这里需要注意的是传入参数ErrorConstructor,这里可以传入特定类型的异常类,比如ES6内置了RangeError、ReferenceError、SyntaxError、TypeError、URIError异常类型,这样我们就可以实现抛出特定类型的异常。但是我感觉这里不好的一点是无法实现模块内自由灵活的抛出不同的特定类型异常,其实这点可以在闭包函数参数中实现。
二、定义日志编码和日志信息模板
当我们每次记录日志的时候,需要传入日志编码及日志信息模板,方便记录异常发生的代码块及更具可读性的异常信息。通过以下的代码,我们可以看到angular通过闭包函数的可变参数的第一个参数定义日志编码,通过第二个参数定义日志信息模板。
//构造模块异常类,module为模块编码,ErrorConstructor异常构造函数 function minErr(module, ErrorConstructor) { //如果未传入异常构造函数则使用Error ErrorConstructor = ErrorConstructor || Error; //返回异常对象构建方法,传入参数可变 return function() { //日志编码,只能是第一个参数 var code = arguments[0], //构造日志前缀 prefix = '[' + (module ? module + ':' : '') + code + '] ', //日志模板 template = arguments[1], //传入参数 templateArgs = arguments, }; }
三、日志模板信息替换
通过传入日志相关对象来替换日志模板中的信息,从而形成更有意义的异常信息。通过以下代码我们可以看到angular通过replace方法实现日志模板中站位符的替换。
//模板信息替换 message = prefix + template.replace(/\{\d+\}/g, function(match) { //获取站位符的索引,并转化为数字 var index = +match.slice(1, -1), arg; //获取替换站位符的传入参数对象的字符串表示形式 if (index + 2 < templateArgs.length) { //对象转换为字符串 return toDebugString(templateArgs[index + 2]); } return match; });
在这里很巧妙的使用了string的replace方法,通过其正则表达式参数和生成替换字符串的方法实现模板中所有站位符的替换。replace的方法定义如下
stringObject.replace(regexp/substr,replacement)
参数 |
描述 |
---|---|
regexp/substr |
必需。规定子字符串或要替换的模式的RegExp 对象。 请注意,如果该值是一个字符串,则将它作为要检索的直接量文本模式,而不是首先被转换为RegExp 对象。 |
replacement |
必需。一个字符串值。规定了替换文本或生成替换文本的函数。 |
在replacement方法里很巧妙的使用了string的slice方法,实现获取站位符索引。这里使用了两个技巧,其中的+号实现了字符串转化为数字,其中slice中传入的参数1和-1巧妙的提取了站位索引。slice的方法定义如下
stringObject.slice(start,end)
参数 |
描述 |
---|---|
start |
要抽取的片断的起始下标。如果是负数,则该参数规定的是从字符串的尾部开始算起的位置。也就是说,-1 指字符串的最后一个字符,-2 指倒数第二个字符,以此类推。 |
end |
紧接着要抽取的片段的结尾的下标。若未指定此参数,则要提取的子串包括start 到原字符串结尾的字符串。如果该参数是负数,那么它规定的是从字符串的尾部开始算起的位置。 |
angular将传入的对象转化为便于调试的字符串形式,具体代码如下,其中函数对象会清空函数体,undefined变量直接返回undefined字符串,字符串则直接返回,其他类型对象则调用serializeObject进行序列化。这里需要注意对函数对象的处理,对匿名函数的处理并不尽如人意,这里我也没有想到更好的处理方式。
//将传入的对象转化为便于调试的字符串形式 function toDebugString(obj) { //如果是函数则清空函数体 if (typeof obj === 'function') { return obj.toString().replace(/ \{[\s\S]*$/, ''); } else if (typeof obj === 'undefined') { return 'undefined'; } else if (typeof obj !== 'string') {//如果是非字符串的其他对象则进行序列化 return serializeObject(obj); } return obj; }
angular将对象转化为json字符串的代码如下,其通过JSON的stringify方法的第二个方法参数控制键值对的序列化结果,同时也通过seen数组防止引用闭环导致序列化死循环。
//将对象转化为json字符串 function serializeObject(obj) { var seen = []; return JSON.stringify(obj, function(key, val) { //获取val的具体字符串表示形式 val = toJsonReplacer(key, val); //防止引用闭环导致死循环 if (isObject(val)) { if (seen.indexOf(val) >= 0) return '<<already seen>>'; seen.push(val); } return val; }); }
JSON.stringify的方法定义如下
JSON.stringify(value[, replacer[, space]])
参数 |
描述 |
---|---|
value |
待序列化为JSON字符串的对象 |
replacer |
可传入数组定义序列化需要包含的属性,或者传入修改序列化行为的方法,如果不传入参数,则默认序列化所有的非方法属性。 |
space |
分割符,具体参考以下 A String or Number object that's used to insert white space into the output JSON string for readability purposes. If this is a Number, it indicates the number of space characters to use as white space; this number is capped at 10 if it's larger than that. Values less than 1 indicate that no space should be used. If this is a String, the string (or the first 10 characters of the string, if it's longer than that) is used as white space. If this parameter is not provided (or is null), no white space is used. |
angular针对内置一些复杂对象的序列化处理代码如下
//过滤对angular复杂对象的序列化 function toJsonReplacer(key, value) { var val = value; //两个$$开头的字符串返回undefined if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) {//window val = '$WINDOW'; } else if (value && document === value) {//document val = '$DOCUMENT'; } else if (isScope(value)) {//scope val = '$SCOPE'; } return val; }
四、构建在线错误链接
针对特定的异常能够提供在线解决方案是十分人性化的,angular默认提供跳转到angular的在线错误页面,我们可以根据实际需要替换为我们的。这里直接返回异常实例,方便调用者直接调用函数使用。
//构建错误在线链接 message = message + '\nhttp://errors.angularjs.org/1.3.9-local+sha.a3c3bf3/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + encodeURIComponent(toDebugString(arguments[i])); } //构建异常错误实例 return new ErrorConstructor(message);
五、整个处理流程如下图所示
六、实际使用样例
调用代码如下
var codeartistminErr = minErr("CodeRrtist"); throw codeartistminErr("AngularExceptionDesinBlog",'this is my AngularExceptionDesinBlog,{0}',{name:'codeartist'});
调用结果
添加注释的angular.js源代码文件下载地址