在浏览器端的拦截和跟踪主要是利用 JavaScript 的脚本环境完成,根据笔者的经验,主要寻找并总结了如下的方法。这些方法的使用效果和支持平台可以互相弥补。
名称 | 特点 | 优点 | 缺点 |
利用浏览器的 Event | 通过对 [ 鼠标事件 ],[ 键盘事件 ],[HTML 事件 ],[Mutation 事件 ] 的监听,可以对用户的交互,页面的变化,特别是标签节点的变化做出响应 | 浏览器自身支持,代码量小,几乎可以用来控制所有的 HTML 内容 | 此方法中的 Mutation Event,Firefox2.0 平台已支持,IE6.0 尚未支持 |
通过 AOP 技术拦截 | 可以拦截大部分对象的方法调用。 | 很多 JS 代码库和框架已经支持 AOP 技术,代码简单 | ActiveX 对象无法有效拦截。无法拦截普通的函数。另外单独使用此项技术会造成插入点复杂。 |
覆盖函数进行拦截 | 通过编写同名方法覆盖系统定义,用户自定义的函数 ( 构造函数 ),达到拦截目的,对普通函数的拦截是对 AOP 的补充。 | 不依赖其他的代码库和 JS 框架,对系统函数的覆盖有很好的效果,可以拦截构造函数用来控制对象的生成。 | 拦截自定义函数会造成插入点复杂 |
通过动态代理进行拦截 | 主要用来解决 ActiveX 对象的拦截问题,通过构造 ActiveX 对象的代理对象,实现拦截和跟踪。 | 典型的例子如 IE 平台下 AJAX 通信的拦截 | 代码复杂,属性更新的同步机制可能导致某些应用异常。 |
通过自代理和 HTML 解析进行拦截 | 此方法主要解决的是拦截时机的问题,配合上面的方法,就可以实现很多功能,而不必要等待页面的 onload 事件。 | 实现浏览器端页面加载前拦截的好方法 | 代码复杂 |
利用浏览器的 Event
浏览器的事件也可以很好地用来拦截和跟踪页面。 鼠标和键盘的交互事件这里暂不介绍。比较常用的是 onload,onunload 事件和 Mutation Events 事件。 onload 和 onunload 事件可以作用在 window,frame,img 和 object 等对象上,利用 onload 可以在对象载入前执行一些操作 ,onunload 事件可以跟踪浏览器关闭前执行操作。
在浏览器的 Event 中,Mutation Eventsii 是更加重要的跟踪工具之一。 Mutation Events 是 DOM2.0 标准的一部分,目前 Firefox2.x 已经开始支持 Mutation Events, IE6.0 目前尚不支持。 在 IE6.0 中可以是通过使用 onpropertychange 事件及覆盖节点的方法弥补部分的不足。这里重点介绍一下 Mutation Events 。
Mutation Event 主要包括了七种事件,如下所示。
可以说利用 Onload 事件的拦截,我们基本上解决了静态 HTML 内容的拦截,而对于脚本操作的 HTML 变化,我们就可以通过 Mutation Event 来进行解决。
下面类似的实现框架可以用来跟踪 src、action、href 等属性的变化。
document.addEventListener("DOMAttrModified", AttributeNodeModified, false); function AttributeNodeModified(evt) { if(evt.attrName == "href") { } if(evt.attrName == "src") { } if(evt.attrName == "action") { } } |
通过 DOMAttrModified、DOMNodeInserted 等事件可以很好地跟踪和拦截 HTML 的变化。只可惜在 IE6.0 平台上还不能支持 Mutation Event,导致在 IE6.0 上进行拦截和跟踪的效果大打折扣。针对此平台,通过覆盖 document.write/DomNode.appendChild/DomNode.insertBefore 等方法和利用 onpropertychange 事件可以有限度地支持拦截和跟踪 HTML。
通过 AOP 技术拦截
针对对象方法调用的拦截,比较成熟的方案是使用 JavaScript 平台下的 AOP 技术。
目前,JavaScript 平台上的 AOP 方案主要有 Ajaxpectiii、jQuery AOPiv、Dojo AOPv 等。这些代码库主要功能是给指定对象的指定方法添加 Before, After,Around 等通知,从而达到拦截对象方法调用的目的 , 并且支持正则搜索方法名称。
Ajaxpect 的示例代码如下 :
var thing = { makeGreeting: function(text) { return 'Hello ' + text + '!'; }}function aopizeAdvice(args) { args[0] = 'AOP ' + args[0];return args;}function shoutAdvice(result) { return result.toUpperCase();} Ajaxpect.addBefore(thing, 'makeGreeting', aopizeAdvice); Ajaxpect.addAfter(thing, /make*/, shoutAdvice); |
当然,在不方便使用上述代码库并且需求简单的时候,我们同样可以通过对象的方法覆盖的方式达到同样的效果。但是无论 AOP 还是方法覆盖, 都存在一个问题, 就是拦截代码的插入点不能做到很简捷,因为拦截代码的存在位置直接影响了代码的执行效果,因此在使用上还有一定的不方便。另外,针对 IE 平台的 ActiveX 对象,代码库不能很好的发挥功效,这是一些不足的地方。
覆盖系统类 / 方法进行拦截
覆盖已定义的函数是一种比 AOP 更直接的拦截和跟踪脚本调用的方式。
其原理是在原函数定义后,调用前通过定义同名函数,达到拦截和跟踪的目的。其一般形式多如下面 :
1: var oriFunction = someFunction; 2: someFunction = function () {3: return oriFunction(); //or oriFunction.call(x,);4: } |
第一步是(第一行代码)为了将指向原来函数的指针保存,以便后续使用。
第二步便是定义同名函数,在同名函数里面的适当位置调用原来的功能。这种方法不但可以跟踪原来函数,还可以修改和过滤函数的参数,甚至可以修改返回值。当需要操纵参数的时候,只需在新定义的函数中访问 arguments 对象即可。
例如:针对系统函数 window.open(URL,name,specs,replace) 我们可以通过下面的代码进行拦截:
var oriWindowOpen = window.open; window.open = function(url,names,specs,replace) { url = "http://www.ibm.com"; //or arguments[0]="http://www.ibm.com"; return oriWindowOpen(url,names,specs,replace); } |
上面的拦截会导致所有的 window.open 调用全部打开 http://www.ibm.com 窗口 。
函数覆盖的适用范围较广,不但可以模拟 AOP 的实现,还可以对非对象函数进行操作。函数覆盖可以根据使用的差异分成若干情况 :
覆盖构造函数的一般形式 :
var oriFunction = someFunction;someFunction = function () { temp = oriFunction(); //oriFunction.call(x,); return temp;} |
下面结合动态代理的方法给出 IE/Firefox 平台的 Ajax 通信拦截的一种简单实现。
Ajax 通信的核心是通过 XMLHttpRequest 对象和 HTTP Server 进行通信 ( 同步 / 异步 ),Firefox 和 IE 平台对 XMLHttpRequest 对象的实现不一样,因此两种浏览器的拦截方案也大相径庭。我们通过上面的技术将对 XMLHttpRequest 对象的方法进行跟踪。
拦截方法调用,我们可以使用 AOP,当然也可以直接覆盖函数。
在 Firefox 平台,我们可以通过下面的代码实现拦截 Ajax 对象的通信:
var oriXOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method,url,asncFlag,user,password) { //code to trace or intercept oriXOpen.call(this,method,url,asncFlag,user,password); }; |
但是在 IE 6.0 平台,上面的代码将不会有作用,因为在 IE 6.0 平台,Ajax 通信对象是通过 ActiveX 对象完成的,JS 中的函数覆盖不能起到作用。
通过动态代理进行拦截
当在 IE6.0 平台遭遇 ActiveX 对象的时候,面对直接的函数覆盖不能奏效的时候,我们可以考虑通过另外一种办法,即动态代理 ActiveX 对象的方式实现拦截和跟踪。
首先我们通过覆盖构造函数的方法,将创建 XMLHttpRequest 对象的过程进行改造。
var oriActiveXObject = ActiveXObject; ActiveXObject = function(param) { var obj = new oriActiveXObject(param); if(param == "Microsoft.XMLHTTP" || param=="Msxml2.XMLHTTP" || param == "Msxml2.XMLHTTP.4.0") { return createActiveXAgent(obj); } return obj; }; |
我们将构造过程拦截下来后,进行自己的改造,主要操作是创建对象,对象中设置与 ActiveX 对象相同的属性和方法,并且还需要同步属性方法。
function createActiveXAgent(ao) { var agent = new Object; agent.activeXObject = ao; //被包裹的内核,是真正的通信对象 agent.syncAttribute = function() { //syncAttribute是用来同步属性的 try{ this.readyState = this.activeXObject.readystate; this.responseText = this.activeXObject.responseText; this.responseXML = this.activeXObject.responseXML; this.status = this.activeXObject.status; this.statusText = this.activeXObject.statusText; }catch(e) { } }; agent.trigStateChange = function() { //模拟onreadystatechange agent.syncAttribute(); if(agent.onreadystatechange != null) { agent.onreadystatechange(); } }; agent.activeXObject.onreadystatechange = agent.trigStateChange; agent.abort = function() { //模拟abort this.activeXObject.abort(); this.syncAttribute(); }; agent.getAllResponseHeaders =function() { //模拟内核对应的方法 var result = this.activeXObject.getAllResponseHeaders(); this.syncAttribute(); return result; }; agent.getResponseHeader = function(headerLabel) { //模拟内核对应的方法 var result = this.activeXObject.getResponseHeader(headerLabel); this.syncAttribute(); return result; }; agent.open = function(method,url,asyncFlag,userName,password) { //code to trace and intercept; this.activeXObject.open(method,url,asyncFlag,userName,password); this.syncAttribute(); }; agent.send = function(content) { //模拟内核对应的方法 this.activeXObject.send(content); this.syncAttribute(); }; agent.setRequestHeader = function (label,value) { //模拟内核对应的方法 this.activeXObject.setRequestHeader(label,value); this.syncAttribute(); }; return agent;}; |
从上面的代码可以看出来,代理对象通过自身的方法模拟了原来 ActiveX 对象的方法。而更关键的属性问题,是通过在函数调用前后的属性同步函数实现的。即:在调用代理内核方法之前,将属性从代理对象同步给内核对象;在内核方法调用之后,将属性从内核对象同步给代理对象。
因为 AJAX 对象的属性几乎不被用户写入,故上面的实现只需要单向属性同步,即将内核属性同步给代理属性。对于复杂的应用,可以通过双向属性同步函数来解决属性的代理问题。
这种动态代理的方法将 ActiveX 对象像果核一样包裹起来,通过代理对象自身的同名属性和方法提供给外界进行访问,从而达到跟踪和拦截的目的。
通过自代理和 HTML 解析进行拦截
当代码拦截点需要简单可靠的时候,上面的方法无法很好的满足需求。于是我们需要新的思路来解决代码的拦截点问题。
自代理和 HTML 解析是通过拦截原有的 HTTP 通信,通过重新通信获得内容后,在浏览器解析前通过我们自己的代码进行简单解析过滤的方式进行代码处理的方案。
首先是拦截原有的解析,重新加载新的内容:
var s = document.location.href;var comm = new ActiveXObject("Microsoft.XMLHTTP"); comm.open('get',s,false); comm.onreadystatechange = function() { if(comm.readyState == 4) { document.execCommand("stop"); var retText = removeMe(comm.responseText); retText = processContent(retText); } } comm.send(null); |
如果将上面的代码写在一个 js 文件里,然后通过