在angularJS中操纵DOM:最佳实践?

时间:2016-05-27 09:44:32

标签: angularjs angularjs-directive

我们正在使用AngularJS构建一个大型Web应用程序。 我们对不同的情况使用自定义指令。当谈到做DOM操作,绑定事件等时...我们定义了在自定义指令的link函数中操作DOM的函数,但是我们从控制器调用它(我们定义函数在$scope所以给定的控制器可以访问它。我认为这样做的有条理的方法是为每个函数定义一个单独的自定义指令,并直接从模板中使用它,但在我们的情况下,我不知道这样做会有什么吸引力,我们已经很多自定义指令,所以我们正在做的事情很糟糕(定义在指令中操作DOM并从控制器调用它的函数),这甚至有意义,或者就像我们在控制器中操作DOM一样?对我们来说,它有点关注分离,我们从来没有定义在控制器中操作DOM的函数,只在指令中,但是从控制器调用它似乎不是那么正确,是吗?

显示我们的自定义指令的示例:

angular.module('exp', []).directive('customdirectiveExp', ['', function(){
// Runs during compile
return {
    name: 'customDirectiveExp',
    controller: "ControllerExp",
    controllerAs: "ctrl",
    templateUrl: 'templateExp',
    link: function($scope, iElm, iAttrs, controller) {

        /* These function will be called from the ControllerExp when it needs so.
         Function can do any things like manipulating the DOM, addin
         event listner ...
        */
        scope.manipulateDom1 = function(){
            // DOM manipualtion
        };

        scope.manipulateDom2 = function(){
            // DOM manipualtion
        };

        scope.manipulateDom3 = function(){
            // DOM manipualtion
        };

    }
};
}]);

1 个答案:

答案 0 :(得分:9)

我认为“不要操纵控制器中的DOM”咒语是从日子开始的,当时指令主要/仅使用链接函数(或指令控制器,只是一种与其他指令相互通信的方式)。

目前建议的最佳实践是使用“组件”(可以通过指令实现),其中基本上所有指令逻辑都留在控制器中。 (例如,在Angular 2中没有链接函数,每个component /指令基本上都是一个类/控制器(加上一些元数据)。)

在这种情况下,我认为在指令的控制器中操作指令的模板中的DOM是完全没法的。

我们的想法是保持模板/ HTML声明。比较以下片段:

<!--
  `SomeController` reaches out in the DOM and
  makes changes to `myComponent`'s template --- BAD
-->
<div ng-controller="SomeController">
  ...
  <my-component></my-component>
  ...
</div>

VS

<div ng-controller="SomeController">
  ...
  <!--
    `myComponent`'s controller makes changes to
    `myComponent`'s template --- OK
  -->
  <my-component></my-component>
  ...
</div>

在第一个(坏)示例中,myComponent将具有不同的行为/外观,具体取决于它出现在DOM中的位置(例如,它是否在SomeController下?)。更重要的是,很难找出其他(无关)部分可能会改变myComponent的行为/外观。

在第二个(好的)示例中,myComponent的行为和外观将在整个应用程序中保持一致,并且很容易找到它的内容:我只需查看指令的定义(一个地方)。

但有几点需要注意:

  1. 您不希望将DOM操作代码与其他逻辑混合使用。 (这会使你的代码不易维护,更难测试)。

  2. 通常,当所有孩子都到位(编译+链接)时,你想在后链接阶段操纵DOM。在控制器实例化期间运行DOM操作代码意味着模板内容尚未处理。

  3. 通常,当您的控制器未在指令的上下文中实例化时,您不希望运行DOM操作,因为这意味着您总是需要一个已编译的模板来测试您的控制器。这是不可取的,因为它会使单元测试变慢,即使您只想测试非DOM / HTML相关的控制器逻辑的其他部分。

  4. 那么,我们能做什么?

    • 在专用函数中隔离DOM操作代码。适当时会调用此函数(见下文),但所有DOM交互都在一个地方,这样可以更容易查看。

    • 将该函数公开为控制器方法,并从指令的链接函数(而不是在控制器初始化期间)调用它。这可以确保DOM处于所需状态(如果有必要),并且还可以将“独立”控制器实例化与DOM操作分离。

    我们获得了什么:

    • 如果您的控制器被实例化为指令编译/链接的一部分,则将调用该方法并按预期操作DOM。

    • 在单元测试中,如果您不需要DOM操作逻辑,您可以直接实例化控制器并测试它的业务逻辑(独立于任何DOM或编译)。

    • 您可以更好地控制DOM操作的发生时间(在单元测试中)。例如。您可以直接实例化控制器,但仍然传入$element,进行您可能要做的任何断言,然后手动调用DOM操作方法并断言元素已正确转换。传递模拟的$element以及添加事件侦听器等内容也更容易,而无需设置真正的DOM。

    这种方法的缺点(暴露方法并从链接函数调用它)是额外的样板。如果您使用的是Angular 1.5.x,则可以使用指令控制器生命周期挂钩(例如$onInit$postLink)来节省样板,而无需使用链接功能,只需抓住控制器并在其上调用一个方法。 (额外功能:将1.5.x组件语法与生命周期挂钩一起使用,可以更轻松地迁移到Angular 2。)

    示例:

    在v1.5.x之前

    .directive('myButton', function myButtonDirective() {
      // DDO
      return {
        template: '<button ng-click="$ctrl.onClick()></button>',
        scope: {}
        bindToController: {
          label: '@'
        }
        controllerAs: '$ctrl',
        controller: function MyButtonController($element) {
          // Variables - Private
          var self = this;
    
          // Functions - Public
          self._setupElement = _setupElement;
          self.onClick = onClick;
    
          // Functions - Definitions
          function _setupElement() {
            $element.text(self.label);
          }
    
          function onClick() {
            alert('*click*');
          }
        },
        link: function myButtonPostLink(scope, elem, attrs, ctrl) {
          ctrl._setupElement();
        }
      };
    })
    

    v1.5.x之后

    .component('myButton', {
      template: '<button ng-click="$ctrl.onClick()></button>',
      bindings: {
        label: '@'
      }
      controller: function MyButtonController($element) {
        // Variables - Private
        var self = this;
    
        // Functions - Public
        self.$postLink = $postLink;
        self.onClick = onClick;
    
        // Functions - Definitions
        function $postLink() {
          $element.text(self.label);
        }
    
        function onClick() {
          alert('*click*');
        }
      }
    })