Date: 2017-09-20 21:43

最近在公司做的一个需求:在某个页面发送特定请求之后,拦截它的后续操作,以便自定义某些操作。听起来好像有点奇葩,但这类需求应该还是存在的,解决方案比较独特,值得记录一下。

分析页面请求情况,发现是个jsonp请求,就引出了问题所在,如何拦截jsonp的回调函数?

要解决这个问题,就要先回顾jsonp的原理,其实也很简单,就是利用<script>元素不受跨域限制的特性,来发起请求,拿到构造好的响应之后(用callback函数包裹),就自然会调用callback函数:

举个例子:

<script>
    function test(data){
        //do something
    }
</script>
<script src="/api?callback=test"></script>

假设在文档中,有一个函数test, 对接收的参数进行某些操作。然后有个script标签去取api这个接口的数据,而如果 api 接口返回的数据【恰好】是长这样的(需要后端接口构造这样的格式): test({a:b}) 。那就相当于调用前面的test函数,并把{a:b}作为参数传入。

既然原理已经知道了,那就看如何检测请求了。这里以JQUERY为例来说明,其他库没特别研究过,应该也差不多。

(以下步骤没有看过jQuery源码,所以可能有略描述错误)
jQuery发起jsonp请求,大致可以分为几个步骤:

  • 新建一个script标签,并且构造 src属性,即把get参数拼上去,再生成一个callback=xxx的属性,最后得到的可能是这样一个URL:https://xxx.com/api/?a=b&callback=jQuery110206309371989766117_1506762129565&_=1506762129566
  • 把构造好的script标签插入到文档中
  • 然后在全局定义一个函数,例如在这个例子中,就是function jQuery110206309371989766117_1506762129565(){//do something} 作为回调函数,script标签获得结果后,就会调用这个函数。

想明白了这几步,那现在的问题是:如何知道文档被插入了一个script标签。
这里需要绑定dom事件:DOMNodeInserted 当文档中有元素插入,就会触发这个事件。具体的:

var html = document.getElementsByTagName('html')[0]
html.addEventListener("DOMNodeInserted", function(e){
    //do something
}, false);

在上面的代码中,取到根节点HTML元素,再绑定DOMNodeInserted 事件,这样一旦文档中有元素插入,就会触发。在根据传入的事件变量e来判断具体元素是否是script标签,并且script的scr属性是否是我们要拦截的URL (事件变量e还有很多属性,具体可以打印看看)。这样一旦确定了目标,就可以将全局函数callback函数重写了:

var html = document.getElementsByTagName('html')[0]
html.addEventListener("DOMNodeInserted", function(e){
    if (e.target.tagName == "SCRIPT" && e.srcElement.src.match('api') ){
        var src = e.srcElement.src;
        var callback = src.match(/callback=(\w+)/)
        callback = callback[1]
        window[callback] = function (data) {
            //重写callback方法
        }
    }
}, false);

在上面的代码中,首先判断这个插入的元素是否是script,然后src是否符合拦截需求。确定后,则从src属性中提取出callback参数,拿到回调函数的函数名,这时候在重写回调方法:window[callback] = function (data) {}

到了这里就大功告成了,其实整个原理也不复杂,大概总结一下流程:

  • 判断页面有插入了script标签,并且src属性是需要拦截的
  • 从src属性从提取出回调函数函数名
  • 重写回调函数