TAT.svenzeng 用AOP改善javascript代码
In Web开发 on 2013年08月12日 by view: 38,899
13

Aop又叫面向切面编程,用过spring的同学肯定对它非常熟悉,而在js中,AOP是一个被严重忽视的技术点,这篇就通过下面这几个小例子,来说说AOP在js中的妙用.

1, 防止window.onload被二次覆盖.
2,无侵入的统计代码.
3, 分离表单请求和校验.
4,给ajax请求动态添加参数.
5,职责链模式.
6, 组合代替继承.

先给出before和after这2个“切面”函数. 顾名思义,就是让一个函数在另一个函数之前或者之后执行,巧妙的是,before或者after都可以和当前的函数公用this和arguments, 这样一来供我们发挥的地方就多着了.

处理window.onload被二次覆盖.

前段时间看到QQ群里有个人问问题,要改写window.onload, 怎么才能不把以前的window.onload函数覆盖掉.

最原始的方案肯定是直接在原来的window.onload里添上你的新代码.

这样的坏处非常明显,需要去改动原有的函数, 是侵入性最强的一种做法.

另外一种稍微好点的方案是用中间变量保存以前的window.onload;

这样一来,多了一个讨厌的中间变量__onload, 来管理它也要花费一些额外的成本.

试想一下这个场景,当人觉得天气冷,出门的时候很自然选择穿上一件貂皮大衣,而不是把自己的皮扯掉换成貂皮. 动态装饰的好处就体现出来了,完全不会侵入之前的函数.

无侵入的统计代码

本身跟逻辑没有任何关联的统计代码要被硬插进函数里, 这点相信很多搞过上报的同学都很不爽. 比如下面这段代码, 用来统计一个创建1000个节点的函数在用户的电脑上要花费多少时间.

用aop的方式,不再需要在函数内部做改动,先定义一个通用的包装器.

只要一行代码,便能给任何函数都加上统计时间的功能.

分离表单请求和校验

我们在提交表单之前经常会做一些校验工作,来确定表单是不是应该正常提交. 最糟糕的写法是把验证的逻辑都放在send函数里面.

而更好的方式是把所有的校验规则用策略模式放到一个集合里,返回false或者true来决定是否通过验证. 这样可以随意的选择和更换校验规则.

这样还有一个缺点,校验和发送请求这2个请求耦合到了一个函数里面, 我们用aop来把它们分离开来, 把validata做成插件化,真正的即插即用. 只需把send函数改成:

过最前面Function.prototype.before的代码不难看出,我们约定,当前一个函数返回false, 就会阻断下一个函数的执行, 所以当validata返回false的时候, 便不再继续执行send. 而因为之前提到的before函数可以和当前函数公用this和arguments, 所以value参数也能顺利的传递到validata函数里.

给ajax请求动态添加参数

第一个例子里window.onload是用的after后置装饰, 这里是用before前置装饰. 在ajax请求之前动态添加一些参数.

我们遇到过很多跨域的请求, jsonp和iframe都是很常用的方式. 之前在我们的项目里,用参数retype=jsonp表示是jsonp请求, retype=iframe表示是iframe请求. 除此之外这2个请求的参数没有任何区别. 那么可以用before把retype参数动态装饰进去.

先定义一个ajax请求的代理函数.

这个函数里面没有逻辑处理和分支语句,它也不关心自己是jsonp请求还是iframe请求. 它只负责发送数据, 是一个单一职责的好函数.

接下来在发送请求前放置一个before装饰器.

开始发送请求:

职责链模式.

职责链模式在js中典型的应用场景是事件冒泡. 将所有子节点和父节点连成一条链,并沿着这条链传递事件,直到有一个节点能够处理它为止. 职责链模式是消除过多的if else语句的神器.

拿最近做的一个需求来举例, 有个文件上传的功能, 提供了控件,html5, flash, 表单上传这4种上传方式. 根据它们的优先级以及浏览器支持情况来判断当前选择哪种上传方式. 在我进行改造之前,它的伪代码大概是这样:

当然实际的代码远不只这么多,其中还包括了各种控件初始化,容错等情况。有天我需要屏蔽掉flash,看起来是很简单的需求,但难度实际跟在心脏旁边拆掉一根毛线血管类似.

如果试试职责链模式呢, 看看事情将变得多简单:

第一步先改写之前的after函数,使得返回一个对象时阻断职责链的传递,而返回null时继续传递请求。

接下来把每种控件的创建方式都包裹在各自的函数中, 确保没有逻辑交叉和相互污染.

最后用职责链把它们串起来:

可以预见,某天我又需要屏蔽掉flash, 那时的我只需要改动这一行代码. 改成:

组合代替继承

很多时候我们在设计程序的时候,会遇到使用组合还是继承的问题. 通常来讲, 使用组合更灵活轻巧. 还是拿之前文件上传来举例.

我定义了一个超类Upload, 衍生出4个子类.
Plugin_Upload, Html5_Upload, Flash_Upload以及Form_Upload.

Plugin_Upload会继承父类,得到Upload的大部分功能, 然后对控件上传的一些特性进行个性定制. 比如其它3种上传方式都是选择文件后便开始上传. 而控件上传在开始上传之前会经过一轮文件扫描.

第一种做法是Plugin_Upload继承Upload, 然后重写它的start_upload方法.

用更轻的组合方式, 可以直接给原来的start_upload函数装饰上扫描功能, 甚至不需要衍生一个额外的子类.

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2013/08/yong-aop-gai-shan-javascript-dai-ma/

  1. zcdll 2016 年 11 月 29 日

    从 无侵入的统计代码 这一行开始,配图就错位了。就是多了一张和上一张一样的配图,剩下的图得依次上移一位,最后一张图删掉。

  2. 张策 2016 年 11 月 29 日

    从 无侵入的统计代码 这一行开始,配图就错位了。就是多了一张和上一张一样的配图,剩下的图得依次上移一位,最后一张图删掉。

  3. Kpaxqin 2014 年 12 月 8 日

    讲得很棒,我最近也在用aop的方式来做组件的生命周期事件触发,结合本文似乎更多的思路也被打开了。
    要挑点毛病的话就是配图,有些地方对不方,要反复看几遍才能理解。
    还有就是Function.prototype.before/after侵入了原型,值得商榷,个人不是太喜欢,团队大了过后总会有人for-in的时候不加hasOwn。
    还有无论是return false还是return null都与正常的函数返回相重合,用来阻断函数链很怪,个人觉得可以参考prototypeJS的$break:
    window.$stopChain = {};//随便什么引用类型的值都行
    在before/after中 ret === window.$stopChain 时才阻断随后的调用
    这么做优点是消除了return的二义性,缺点则是代码中会随处可见$stopChain这个有点奇怪的东西

    • Kpaxqin 2014 年 12 月 10 日

      还有个问题是最初的实现版本中,before可以阻断函数链,after却不行,对于是否允许切面函数阻断函数链其实是个值得探讨的问题,特别是after,若不允许,则before和after行为不一致,若允许,则after势必要覆盖原函数的返回值
      after : function(fn, afterFn){
      return function(){
      var ret = fn.apply(this, arguments);

      if (ret !== func.$stopChain){
      ret = afterFn.apply(this, arguments);//此处覆盖了原函数的返回值
      }

      return ret;
      }
      }

      引申下来就成了before和after的【地位】问题:
      若仅仅是原函数的add-on,则它们应该【静默执行】,before不阻断原函数执行,after不覆盖原函数返回,多数传统语言中的AOP其实是这个路子,应用的场景如:日志、统计等
      还有种情况就是责任链式AOP,在责任链中原函数和它的before、after在业务上是同等重要的,因此before必须要能阻断,after也必须要能覆盖。楼主提到的表单验证就是典型应用场景

      个人觉得有必要明确地区分这两种AOP场景,各用一套工具函数来实现

  4. 佛山小冰火人博客 2014 年 8 月 6 日

    天猫聚划算抢购风暴618晨盛大开幕啦!到目前活动不断传来捷报,贝尔莱德D07到目前为止已爆售1400台,天猫年中大促疯抢中..

    夏日炎炎,细菌滋生,相信每个家庭都需要一台可以抑菌、防过敏的挂烫机,据新闻报道,今年夏天许多人因为皮肤过敏进医院就医,大家提前备货吧!

    这次D07挂烫机是全网唯一双杆抑菌的挂烫机史无前例的冰点价,只需??8元,还有多重好礼玩转手机淘宝,越早买,机会越多!多重好礼,等你来抢购。

    贝尔莱德年中大促,D07震撼全场

    天猫聚划算抢购风暴618晨盛大开幕啦!到目前活动不断传来捷报,贝尔莱德D07到目前为止已爆售1400台,天猫年中大

  5. TZ 2014 年 6 月 9 日

    用图片来展示代码, 这…. 这也太不专业了

  6. 21paradox 2014 年 4 月 11 日

    function before(func1){
    return function(func2){
    return function(){
    func2();
    func1.apply(this,arguments);
    };
    };
    };
    var listener1 = function(e){
    console.log(‘this is listener1’);
    };

    listener1();

    listener1 = before(listener1)( function(){
    console.log(‘this is listener2’);
    });
    listener1();

    结合lz的代码想出来的,哈哈。

  7. xtx 2013 年 12 月 3 日

    第九张图片很精辟,拜读了。

  8. horcrux 2013 年 11 月 29 日

    实践之后,个人不太喜欢这种方法,因为这种方法侵入了Function.prototype…有代码洁癖不太喜欢这样
    而且有一点弄不明白
    你说的:防止window.onload被二次覆盖
    但是这样window.onload = ( window.onload || function(){} ).before(function(){});就不算二次覆盖吗

  9. 土豆 2013 年 10 月 27 日

    你好,我有幸看到你的StreetFighter的源代码,但是具体的实现思路不是很清楚。能否大致介绍一下您的实现思路?谢谢!

  10. Lydiafly 2013 年 8 月 30 日

    “无侵入的统计代码”这部分的插图有问题

  11. 小克 2013 年 8 月 14 日

    配图貌似有点问题……

  12. jeff 2013 年 8 月 14 日

    无侵入的统计代码 的配图是否错了

发表评论