AngularJS:我应该将指令的链接函数转换为控制器吗?

AngularJS: Should I convert directive's linking function to a controller?

我听说在使用隔离作用域的指令中使用 controllerAs 语法和 bindToController: true 是一种很好的做法。参考资料:one, two

假设,我有这样的指令:

angular.module('MyModule').directive('MyDirective', function(User) {
  return {
    scope: {
      name: '='
    },
    templateUrl: 'my-template.html',
    link: function(scope) {
      scope.User = User;
      scope.doSomething = function() {
        // Do something cool
      };
    }
  };
});
<!-- my-template.html -->
<div>
  User Id: {{ User.id }}
  Name: {{ name }}
  <button ng-click="doSomething()">Do it</button>
</div>

如您所见,该指令中没有控制器。但是,为了能够利用 controllerAsbindToController: true,我必须有一个控制器。

将链接函数转换为控制器是最佳做法吗?

angular.module('MyModule').directive('MyDirective', function(User) {
  return {
    scope: {
      name: '='
    },
    templateUrl: 'my-template.html',
    bindToController: true,
    controllerAs: 'myCtrl',
    controller: function() {
      this.User = User;
      this.doSomething = function() {
        // Do something cool
      };
    }
  };
});
<!-- my-template.html -->
<div>
  User Id: {{ myCtrl.User.id }}
  Name: {{ myCtrl.name }}
  <button ng-click="myCtrl.doSomething()">Do it</button>
</div>

我的理解是指令的控制器应该用作公开指令的机制 API 以进行指令到指令的通信。

考虑到 Angular 2.0,谁能阐明当今的最佳实践是什么?

根据最新的 documentation 这仍然是推荐的做法 "use controller when you want to expose an API to other directives. Otherwise use link." 我也想听听其他人的意见以及他们正在使用的方法。

分享来自 here 的内容,(我没有足够的声誉将其作为评论)

“where do I put code, in ‘controller’ or ‘link’?”

  • 编译前? – 控制器
  • 编译后? – Link

Couple of things to note:

  1. controller ‘$scope’ 和 link ‘scope’ 是一回事。不同之处在于发送到控制器的参数是通过依赖注入到达那里的(因此需要将其称为“$scope”),其中发送到 link 的参数是基于标准顺序的函数。所有 angular 示例在上下文中都会使用“scope”,但出于理智原因,我通常将其称为 $scope:http://plnkr.co/edit/lqcoJj?p=preview

  2. 本例中的$scope/scope就是父控制器传入的那个

  3. 指令中的
  4. ‘link’实际上是‘post-link’函数(见下面的渲染管线)。由于 pre-link 很少使用,'link' 选项只是设置 'post-link' 函数的快捷方式。

那么,什么是真实世界的例子?好吧,当我决定时,我会这样做:

  • “我只是在做模板和范围的事情吗?” – 进入控制器
  • “我要添加一些酷豆 jquery 库吗?​​” – 进入 link

答案归功于 jasonmore

更新

(在底部我添加了一个 code/plnkr 来显示方法)

除了你提到的那篇文章:https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs#3-3-match-controllers-with-directives, which basically not only advocates the pattern you are asking for, but component based front-end in general, I have found: http://joelhooks.com/blog/2014/02/11/lets-make-full-ass-angularjs-directives/(提倡尽量少用link函数并以ui-bootstrap为例使用这种模式的地方)。我非常同意这两篇文章。

关于 Angular2.0 的另一件事:angular2.0 中不再有 $scope -- https://www.youtube.com/watch?v=gNmWybAyBHI&t=12m14s,所以如果你能尽可能多地摆脱 $scope可能,那么过渡应该更平滑。

我也犯了一个小错误:

Still, I prefer to define all functions in controller and just call them via link's scope. Ideally it is just one call: scope.init ctrl.init(/*args*/) (where ctrl is directive's controller).


在某种程度上这是一个品味问题,但是有一些合理的理由让 link 函数尽可能地精简:

  1. link 函数中的逻辑不易测试。当然,您可以在单元测试中编译该指令并测试其行为,但 link 函数本身是一个黑框。

  2. 如果您必须使用 controller(比方说指令间通信),那么您最终会在两个地方放置代码。这很令人困惑,但是如果您决定将 link 函数精简,那么所有可以放在 controller 中的东西都应该放在 controller.

    [=103= 中]
  3. 您不能直接向 link 函数注入额外的依赖项(您仍然可以使用那些注入到主指令函数的依赖项)。 controller的方法就没有这个问题。为什么重要:

    • 通过使依赖关系更接近需要它们的上下文,它可以保持更好的代码结构
    • 有 non-JS 背景的人来到 angular 仍然对函数闭包在 JS 中的工作方式有疑问

那么 link 函数中需要输入什么:

  1. 元素插入 DOM 后需要 运行 的所有内容。如果 $element 暴露了 $on('linked') 事件那么基本上这一点是无效的。
  2. 抓取对控制器的引用 require:ed。同样,如果可以将它们直接注入 controller...

不过,我更喜欢在 controller 中定义所有函数,然后通过 link 的范围调用它们。理想情况下,它只是一个调用:scope.init.


Misko Hevery 曾多次说过 DDO 远非完美且易于理解,它演变成了现在的样子。我很确定,如果设计决策是预先做出的,那么就会有一个地方来放置指令的逻辑——就像在 angular2.0.

中一样

现在回答您是否应该将 link 函数转换为 controller 的问题。它确实取决于许多标准,但如果代码是积极开发的,那么可能值得考虑。我的经历(以及我谈过的几个人)可以用这张图片来说明:

关于 angular2.0 -- 这将是一个结构性转变,所以从那个角度来看应该没什么大不了的,但 controller 的方法似乎更接近于directives/components 将通过 ES6 类.

在 v2.0 中声明

最后一件事:在某种程度上这是一个品味问题,但也有一些合理的理由让 CONTROLLER 函数保持精简(通过将逻辑委托给服务)。


更新 -- PLNKR

PLNKR举例方法:

html

<input ng-model="data.name"/>

<top-directive>
  <my-directive my-config="data">

  </my-directive>
</top-directive>

js

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.data = { name : 'Hello, World'};
});

app.controller('MyCtrl', function($scope){

  var self = this;

  this.init = function(top){
    this.topCtrl = top;
    this.getTopName = top.getName.bind(top);
    this.getConfigName = function(){return this.config.name}; 
    console.log('initilizing', this, $scope, this.getConfigName, this.getTopName());
  }

  // if you want to $watch you have to inject $scope
  // you have access to the controller via name defined 
  // in contollerAs
  $scope.$watch('myCtrl.config', function(){
    console.log('config changed', self.getConfigName());
  }, true);
});

app.directive('topDirective', function(){

  return {
    controller : function(){
      this.name = "Hello, Top World";
      this.getName = function(){return this.name};
    }
  }

});

app.directive('myDirective', function(){

  return {

    require: ['myDirective', '^topDirective'],

    controller : 'MyCtrl',
    bindToController: true,
    controllerAs: 'myCtrl',

    template : '{{myCtrl.getConfigName() + " --- " + myCtrl.getTopName()}} ',

    scope : {
     config : "=myConfig",
    },

    link : function(scope, element, attrs,  Ctrls){
      Ctrls[0].init(Ctrls[1]);
    }
  }

});

我认为最好的做法是移动初始化代码 and/or,在指令的控制器中公开 API 函数,因为它有两个目的:

1. Intialization of $scope 
2. Exposing an API for communication between directives

作用域初始化

假设您的指令定义了一个子作用域(或继承作用域)。如果您在 link 函数内部初始化作用域,则子作用域将无法通过作用域继承访问此处定义的任何作用域变量。这是因为父 link 函数总是在子 link 函数之后执行。因此,范围初始化的正确位置是在控制器函数内部。

公开控制器API

子指令可以通过指令定义对象上的 'require' 属性 访问父指令的控制器。这允许指令进行通信。为了使其工作,必须完全定义父控制器,以便可以从子指令的 link 函数访问它。实现它的最佳位置是在控制器函数本身的定义中。父控制器函数总是在子控制器函数之前被调用。

最后的想法

重要的是要了解 link 函数和控制器函数有两个截然不同的目的。 controller 函数专为初始化和指令通信而设计,linker 函数专为运行 时间行为而设计。根据您的代码的意图,您应该能够决定它是属于控制器,还是属于 linker。

是否应将任何初始化作用域的代码从 link 函数移动到控制器函数?

是的,这是控制器功能存在的主要原因之一:初始化范围,并允许其范围参与原型范围继承。

是否应该将 $watch 处理程序从 link 函数移动到控制器函数?

没有。 link 函数的目的是连接行为并可能操纵 DOM。在 link 函数中,所有指令都已编译,并且所有子 link 函数都已执行。这使它成为连接行为的理想场所,因为它已尽可能接近 DOM 就绪(直到渲染阶段之后才真正 DOM 就绪)。

我将从你的最后一句话开始。这完全取决于您想如何编写 angular 代码。如果您想坚持为 angular 1.x 编写好的代码的指导方针,那么甚至不必费心去想什么是理想的。但是,如果您想为 Angular 的下一版本以及即将到来的 Web 技术做好准备,我建议您开始采用新概念并根据您今天编写代码的方式调整它们。请记住,在这种情况下没有对错。

谈到 angular 2.0 和 ES6,我想强调指令的概念将更符合 Web 组件技术。

在Angular2.0(根据当前设计)将摆脱定义指令的复杂方式;那不是 DDO。因此,我认为如果您开始以这种方式思考会更好。一个组件将只有一个视图和一个控制器。

例如,

@ComponentDirective({
    selector:'carousel',
    directives:[NgRepeat]
})
export class Carousel{  
    constructor(panes:Query<CarouselItem>) {
        this.items= panes;
    }

    select(selectedCarouselItem:CarouselItem) { ... }
}

以上代码是用 AtScript(typescript 和 ES6 的超集)编写的,但您也可以在 ES5 中表达相同的内容。你可以看到事情会变得多么简单。在 np 中有这样的概念,例如 link 函数或编译等

另外,上面组件的视图会直接绑定到上面class;所以你已经可以找到与 controllerAs 语法的相似之处。

因此,从本质上讲,我建议您首先了解 Web Components 背后的总体思路,以及 Web 开发的未来可能如何,然后我认为您会开始编写 Angular 1.x 记住这一点的代码。

总而言之,尝试以支持当前版本 Angular 的方式编写代码,但如果您认为您的代码的某些部分可以包含下一版本的某些概念,那么请执行它。我不相信它会伤害你。尽量保持简单,因为 Angular 的新版本会更简单。

我建议您阅读以下帖子:

  1. https://www.airpair.com/angularjs/posts/component-based-angularjs-directives
  2. http://eisenbergeffect.bluespire.com/all-about-angular-2-0/
  3. https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs
  4. http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html