关于 AOP
AOP 即“面向切面编程”(也译作面向方面编程),在我经历过的 Java 项目中, 大量运用于:权限控制、日志记录、数据校验、异常处理、主动通知等领域。AOP 主要思想是把一些与核心业务无关但又在多个模块使用的功能分离出来,然后动态给业务模块添加上 需要的功能。JavaScript 有得天独厚的动态解析执行的优势,用来实现类似 AOP 这样的模式是非常简单的,但目前很多开发者只是把 JavaScript 当作一种美化页面的“玩具语言” ,而极少深入使用一些“旁门左道”,在本文我举的几个例子都是从实际出发,用 AOP 的思想解决问题。开始之前,要确认您已经掌握 jQuery 选择器的基本用法。
Web 开发中常遇到的问题
首先看一下简单的例子:
清单 1. 一个简单的 HTML 表单
<form action="/test.jsp" method="post"> <input type=”text” name=”username” /> <input type="submit" /> </form>
这是一个最简单的 form 表单,看起来没什么问题。但在低速网络环境下,心急的浏览者没耐心等待,很可能会重复提交表单,如图 1。
图 1. 低速网络下的浏览器状态栏
作为一个 Web 开发者,我们是绝对不希望浏览者重复提交表单的,这时您可能会想到以下方案
清单 2. 表单提交时禁用 submit 按钮
<form action="/test.jsp" method="post"> <input type=”text” name=”username” /> <input type="submit" onClick="this.disabled=true;" /> </form>
很好,问题解决了,点击“提交”按钮同时该按钮也被禁用了,这样浏览者就不能重复提交了。
可是,如果您的工程很大,有成百上千个页面,每个页面都有一段类似的代码,难道您还独自编辑吗?如果有一天要在按钮 disabled 之前 alert 一个提示信息,那您还不抓狂?
基于 AOP 思想的解决方案
前面我讲过“AOP 主要思想是把一些与业务无关但又在多个模块使用的功能分离出来”,在这里,每个页面都可以看作是一个模块,而“表单提交时禁用提交按钮”这个功能就可以分离出来,在不改变各模块代码的前提下为模块织入此功能。(注:实际上还是要修改代码的,因为要引入 .js 文件,但这非常简单,完全可以使用工具批量完成,并且是一劳永逸的)。下面谈谈具体的实现思路:
- 把禁用 submit 按钮的代碼封裝成 function,写入 JS 文件(在 AOP 领域,这个 function 的术语叫做 “通知(Advice)” );
- 在各页面引入该 JS 文件;
- JS 引入时,把原 submit 事件处理函数备份起来;
- 把 submit 事件处理函数替换成我们的通知 function;
- 其中,通知 function 要做两件事情:
- 禁用 submit 按钮;
- 调用备份好的原 submit 事件处理函数;
没看明白?请阅读代码:
清单 3.AOP 的基本实现
// 这就是我们要实现的通知 function function disableFormSubmitButton(){ // 首先获取要操作的 DOM 对象 ( 织入点 ),我们需要拦截所有 form,代码如下 $("form") // 因为一个页面可能会有多个 <form>,所以这里需要遍历 $("form").each(function(i, v){ // 原 submit 事件函數,备份成 _ invokesubmit, //_invokesubmit 是随便起的名字,充分利用了 javascript 的动态特性。 v._invokesubmit = v.onsubmit ; // 替换原 submit 事件函數 v.onsubmit = function(){ // 禁用提交表单按钮 $(this).find(“input[type= 'submit']”).prop(“disabled”, true); v._invokeprocess();// 调用备份的 submit 事件函数 }; }); }
把这段代码写入一个 .js 文件中,在各页面引入。至此,我们已用 AOP 成功解决了文章开头提出的问题。示意图如下:
图 2:应用 AOP 前后的示意图
我们把以上三个步骤整理一下,并封装成一个 jQuery 插件,以支持多种事件,完整的代码:
$.fn.exeprocess = function(){ if(this[0]._exeprocess != null) return this[0]._exeprocess(); }; $.fn.jqsetaop = function(eventname, newevent){ $(this).each(function(i, v){ v._exeprocess = eval("v.on"+eventname); // 原有的事件 eval("v.on"+eventname+" = function(){return newevent($(v))}"); // 回调,替换事件 }); }; /* 已经测试过的支持的事件 onblur onchange onclick ondblclick onerror onfocus onkeydown onkeypress onkeyup onload,onmousedown onmousemove onmouseout onmouseover onmouseup onresize onselect onsubmit,onunload */
学习例子 : 函数执行时间统计
在这个例子中,我们将织入一个 JS 事件,并算出执行该事件函数所消耗的时间
清单 4. 函数执行时间统计
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> number of cycles:<br /> <input id="txttimes" type="text" value="1000000" /> <button id="btnTest" onClick="doCircle($('#txttimes').attr('value'));" >Test</button> <span id="spanMilliSeconds"></span> </body> <script type="text/javascript" src="./jquery-1.7.2.min.js"></script> <script type="text/javascript" src="./jqaop4e-0.1.js"></script> <script type="text/javascript"> function doCircle(times){// 事件函数,指定次数的循环 var total = 0; for(var i=1;i<=times;i++){ total += times; } } $(document).ready(function(){ $("#btnTest").jqsetaop("click", function(jthis){// 织入 click 事件 var beforetime = new Date().getTime();// 记录当前时间 var result = jthis.exeprocess();// 执行事件函数 $("#spanMilliSeconds").text((new Date().getTime() - beforetime) + " ms");// 得到执行时间 return result; }); }); </script> </html>
图 3.Firefox 运行例子效果
扩展:jQuery 基于对象方法的 AOP 实现
以上我们提到的都是以“JS 事件”为切入点的 AOP 实现,其实借助一些 jQuery 增强插件,我们也能很容易实现以“JS 方法”为切入点的 AOP, jquery-aop 就是这样一个插件,它可以:
- 提供 Beforer、After、After Throw、After Finally、Around、Introduction 等多种类型的通知
- 可使用正则表达式定义要织入的方法名
- 可随时移除通知
清单 5:jquery-aop 前置织入 alert 方法
$.aop.before( {target: window, method: 'alert'}, function(alertarguments/* 这里是 alert 方法的参数 */) { document.write("before alert,argument: " + alertarguments[0] + "<br />"); } );
以上代码会在 alert 方法调用前打印方法的参数。
更多有关 jquery-aop 的使用,请参考本文附件中的示例代码。