Angular装饰器的应用

angular.js官方网站上有关于decorator的文档,点击这里查看。干的事情很简单,就是注册一个service的装饰器,支持依赖注入;通过他允许我们去覆盖或者修改service的原本功能。

项目中有一个这样的场景,需要路由发生改变,但是呢,改变之后的路由和当前的路由除了url不同,其他的都是相同的,也就意味着controller是一样的,得到的模板内容是一样的。在这种场景下,希望的是路由会发生变化,但是不希望ng-view的元素不发生变化。你可能会说,直接用search或者hash来更改地址多好,再配合reloadOnSearch:false不就万事大吉了吗,事实是不符合老板要求。。。所以这里需要一种方案来解决这个问题。

既然只需要对ng-view部分做修改,他是一个指令,而指令在angular中其实也是按照service处理的。对于ng-view指令而言,其实就是通过$provide.factory注册了一个服务名字为ngViewDirective,由此想到能不能修改下这个指令定义的默认逻辑呢?答案是肯定的,通过decorator就可以做到。

这个需求可以换个说法,也就是在路由定义的时候,多指定一个参数state,值是一个字符串名字,在发生改变的时候,判断下当前的这个值和上次的值是否相同,且顺便判断下templatecontroller是否一样;如果都为true的话,就认为不需要更新ng-view元素了。

所以说下面就是实现部分了:

angular.module('ngRoute').decorator('ngViewDirective', ['$route', '$delegate', function ($route, $delegate) {
  // 保留原link函数
  var oldL = $delegate[0].link;
  // 修改compile值
  $delegate[0].compile = function () {
    var lastR = null;
    var el;
    // 新的link
    return function (scope, $element, attr, ctrl, $transclude) {
      // 代理的$transclude函数
      var trs = function (scope, cb) {
        var current = $route.current;
        if (lastR && current && lastR.state &&
            lastR.state === current.state &&
            lastR.controller === current.controller &&
            lastR.locals.$template === current.locals.$template) {
          // 返回保留的el
          return el;
        }
        lastR = current;
        el = $transclude(scope, cb);
        return el;
      };
      trs.$$boundTransclude = $transclude.$$boundTransclude;
      // 调用原始的link
      oldL(scope, $element, attr, ctrl, trs);
    }
  };
  // 把原service实例返回
  return $delegate;
}]);

注意上边的一些细节:

  1. 为什么是直接修改的$delegate[0].compile的值,而不是直接修改其link值呢?最开始的时候我也是这样想的,但是发现无效,为什么呢?后来想到了在指令处理过程中不会去判断link,而是去判断的compile,也就是说在angular的指令处理中,如果你写了link而没有写compile的话,angular会给你造一个compile,然后返回你定义的link

  2. 可以看到核心是把调用link时的最后一个参数$transclude给代理代理掉了,然后在其中根据上次的route信息和当前的route信息来判断是否需要调用真正的$transclude得到新的内容。

  3. 判断模板是否相同的时候,采用的是locals.$template,这是为了处理$templateUrl的情况,得到$templateUrl的模板内容后会给locals.$template赋值。

这样就实现了需求,有state且上次的route信息和当前的route信息中的state是相同的,然后再加上controller$template的判断就可以了。

使用的话很简单:

$routeProvider
  .when('/a', {
    template: 'a',
    controller: 'aa',
    state: 'a'
  })
  .when('/b', {
    template: 'a',
    controller: 'aa',
    state: 'a'
  })
  .when('/c', {
    template: 'c',
    controller: 'cc'
  })
  .otherwise('/a')

上边的配置,默认路由是/a,假设现在路由更新为/b,那么由于/a/b的路由配置中state相同都是a,且template和controller都是相同的,所以说此时不会创建新的ng-view,当然controller也不会重新实例化;然后路由更新到/c,一切回归正常的默认行为,内容发生更新。

decorator还是很有用的,把它用在合适的场景(虽然这个场景是不大合理的)会提供一种解决问题的新思路!

发布于: 2015年 11月 04日