AngularJS 1.x 小记(三)

AngularJS 1.x 小记(三)

组件(Component)

在AngularJS 1.5中新加了组件的概念,它的意图是希望从AngularJS 1.x向AngularJS 2.0迁移时能更加平顺,AngularJS团队也提倡使用组件化模式开发Web应用。那么组件是什么呢?其实组件就是指令的一种特性形式,它规避了一些指令中晦涩难理解的东西,比如 compile 函数, link 函数, scope , restrict 等,所以组件的目的就是能让我们更为傻瓜式的创建指令,能更好的遵循组件化的开发模式,提高性能以及更容易向AngularJS 2.0迁移。

创建组件

我们可以使用Module的 component 方法创建组件:

var mainModule = angular.module("mainModule", []);

mainModule.component("myComponent", {
  templateUrl: "myTemplate.html",
  controller: function() {

  },
  bindings: {
    name: "="
  }
});

component 方法的第一个参数是组件名称,命名规则和使用方法与指令一样,第二个参数和创建指令有点不同,它并不是一个函数,而是一个对象,在该对象中对组件的配置和在指令中的配置方式很类似。

我们先来看看组件和指令之间有哪些区别:

  • 组件中不提供手动配置作用域,默认的作用域就是隔离域。
  • 组件中通过 bindings 属性进行数据绑定,除了 = , @ , & 三种绑定方式以外还增加了一种 < 方式,既单向绑定,但不限于字符串。从而保证了组件有自己的清晰的输入输出API。并且通过 bindings 对象绑定的属性直接绑定在组件的Controller上。
  • 组件的Controller默认名称为 $ctrl ,当然也可以使用 controllerAs 属性指定Controller的名称。
  • 组件只能以标签形式使用。
  • 组件中没有 link 函数, compile 函数, priority 属性, restrict 属性。
  • 组件只能控制自身的输入输出,组件不允许修改属于自己隔离域以外的任何数据和DOM元素。一般情况下,AngularJS通过作用域(Scope)继承的特性支持跨层级修改数据的能力,但是如果当修改数据职责不清晰或不恰当的时候就会导致各种问题,所以这也就是组件的作用域默认都是隔离域的原因。

使用起来和指令比较类似:

// modules.js
var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
  this.person = {
    name: "Jason"
  }
})

mainModule.component("myComponent", {
  templateUrl: "myTemplate.html",
  controller: function() {

  },
  bindings: {
    person: "="
  }
});
<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Component</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="MyController as mc">
      <my-component person="mc.person"></my-component>
    </div>
  </body>
</html>

<!--myTemplate.html-->
<span>Name: {{$ctrl.person.name}}</span>

组件的生命周期

在组件的整个生命周期里,AngularJS提供了五个关键点的方法,可供我们监听到组件的运行状态:

  • $onInit() :该方法在组件及其所有 binding 初始化之后被调用,从而我们就有了一个清晰的地方统一存放数据初始化的逻辑:
    var mainModule = angular.module("mainModule", []);
    
    mainModule.component("myComponent", {
      templateUrl: "myTemplate.html",
      controller: function() {
        this.name = "jason";
      }
    });
    
    // 将初始化数据的逻辑放在onInit方法中
    
    mainModule.component("myComponent", {
      templateUrl: "myTemplate.html",
      controller: function() {
        this.$onInit = function() {
          this.name = "jason";
        }
      }
    });
  • $onChanges(changesObj) :当组件中单向绑定的属性值发生变化时被调用,这里要注意的是只有绑定属性值的引用发生变化时才能监听到,如果只是在指令内对属性进行修改,该方法是无法监听到的。通过该方法的参数可以获取到被修改数据当前的值、修改之前的值、是否时第一次修改:
    mainModule.component("myComponent", {
      templateUrl: "myTemplate.html",
      controller: function() {
        this.$onChanges = function(changesObj) {
          if(changesObj.name) {
            // name当前的值
            var nameCurrentValue = changesObj.name.currentValue;
            // name修改前的值
            var namePreviousValue = changesObj.name.previousValue;
            // 是否是第一次修改
            var isFirstChange = changesObj.name.isFirstChange();
          }
        }
      },
      bindings: {
        name: "<"
      }
    });
  • $doCheck() :该方法和 $onChanges(changesObj) 作用类似,但是该方法可以监听到在指令内对属性进行修改的行为:
    mainModule.component("myComponent", {
      templateUrl: "myTemplate.html",
      controller: function() {
        // 当name在指令内修改时
        this.name = "Green";
        this.$doCheck = function() {
          // doCheck方法会被调用
        }
      },
      bindings: {
        name: "<"
      }
    });
  • $onDestroy() :当作用域被销毁时调用该方法。
  • $postLink() :当指令所在标签与子标签链接时调用该方法。

组件化开发

我们先来看看示例效果:

Y3AjqyV

既然是组件化开发,那么我们来看看上面这个示例有几个组件:


从上图可以看到,整个示例一共用了三个组件,其中有两个组件进行了复用,下面我们来看看每个组件是如何定义的。

personList组件

该组件主要用来初始化数据源,定义对数据源操作的函数:

mainModule.component("personList", {
  templateUrl: "personList.html",
  controller: function() {
    this.$onInit = function() {
      this.list = [{
        name: "Jason",
        job: "Developer"
      },{
        name: "Green",
        job: "Doctor"
      }];
    };

    this.updatePerson = function(person, job, value) {
      person[job] = value;
    };

    this.deletePerson = function(person) {
      var idx = this.list.indexOf(person);
      if(idx >= 0) {
        this.list.splice(idx, 1);
      }
    };
  }
});

首先在 $onInit 函数中初始化数据源,定义了 Person 对象数组,然后定义了更新指定 Person 对象的方法 updatePerson 及删除指定 Person 对象的方法deletePerson 。

再来看看它的UI模板文件 personList.html :

<b>Person</b><br>
<person-detail ng-repeat="person in $ctrl.list" person="person" on-update="$ctrl.updatePerson(person, job, value)" on-delete="$ctrl.deletePerson(person)"></person-detail>

该文件共有两部分,第一部分是用原生HTML标签定义了标题,第二部分是使用了另外一个组件 personDetail 。 ng-repeat 指令是AngularJS内置的指令,作用不言而喻,就是循环数据源,同时组件也跟据循环次数增加。 person , on-update , on-delete 是在 personDetail 组件中定义的数据绑定属性,用大白话解释就是, personDetail 组件中的 person 变量与 personList 组件中的 Person 对象进行了绑定, personDetail 组件中的 onUpdate 和 onDelete 方法分别与 personList 组件中的 updatePerson 和 deletePerson 方法进行了绑定。

personDetail组件

该组件主要用于展示Person对象的具体内容:

mainModule.component("personDetail", {
  templateUrl: "personDetail.html",
  bindings: {
    person: "<",
    onUpdate: "&",
    onDelete: "&"
  },
  controller: function() {
    this.update = function(job, value) {
      this.onUpdate({person: this.person, job: job, value: value});
    };

    this.delete = function() {
      this.onDelete(this.person);
    }
  }
});
<hr>
<div>
  Name: {{$ctrl.person.name}}<br>
  Job: <editable-field field-value="$ctrl.person.job" on-update="$ctrl.update('job', value)"></editable-field>
  <button ng-click="$ctrl.delete()">Delete</button>
</div>

在 personDetail.html 文件里,首先访问了 person 对象的 name 属性,将其展示出来,注意,这里由 $ctrl.preson 访问到的其实是单向绑定的 personList 组件中的person 对象。而且在 update 函数中调用了与 personList 组件的 updatePerson 函数绑定的 onUpdate 函数,也就是子组件调用了父组件的方法。然后使用了第三个组件editableField ,该组件同样有一些属性和方法和 personDetail 组件中对应的属性和方法进行了绑定。最后增加了一个按钮,并使用 ng-click 指令指定了按钮的点击事件。

editableField组件

该组件的主要作用是展示并修改 person 对象中的 job 属性:

mainModule.component("editableField", {
  templateUrl: "editableField.html",
  bindings: {
    fieldValue: "<",
    onUpdate: "&"
  },
  controller: function() {
    this.$onInit = function() {
      this.editMode = false;
      this.fieldValueCopy = this.fieldValue;
    };

    this.handModelChange = function() {
      if(this.editMode) {
        this.onUpdate({job: "job", value: this.fieldValue});
        this.fieldValueCopy = this.fieldValue;
      }
      this.editMode = !this.editMode;
    };

    this.reset = function() {
      this.fieldValue = this.fieldValueCopy;
    };
  }
});

从最开始的运行效果中可以看到 editableField 是有形态变化的,所以在 $onInit函数中定义了是否为编辑模式的标识符 editMode 以及代表输入框内容的 fieldValue变量,因为有 reset 功能,所以还定义存储修改之前值的变量 fieldValueCopy 。然后定义了点击 Edit 或 Save 按钮触发的函数 handModelChange ,并在该函数中调用了和 personDetail 组件的 update 函数绑定的 onUpdate 函数,同样由子组件调用了父组件的方法。还定义了点击 Reset 按钮触发的函数 reset 。

<span ng-switch="$ctrl.editMode">
  <input ng-switch-when="true" type="text" ng-model="$ctrl.fieldValue">
  <span ng-switch-default>{{$ctrl.fieldValue}}</span>
</span>
<button ng-click="$ctrl.handModelChange()">{{$ctrl.editMode ? "Save" : "Edit"}}</button>
<button ng-if="$ctrl.editMode" ng-click="$ctrl.reset()">Reset</button>

在 editableField.html 文件中展示了 person 对象的 job 属性,定义了修改 job属性的输入框以及两个按钮。这里出现了一组之前没见过的AngularJS内置指令, ng-switch 、 ng-switch-when 、 ng-switch-default ,这三个指令一般组合使用,作用类似 if else 语句,通过这组指令和 editMode 变量就可以达到动态变换DOM元素的功能。

最后来看看简单的 index.html 文件:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <person-list></person-list>
  </body>
</html>

从上面的这个示例中可以看出在 editableField 和 personDetail 组件中都没有真正意义上去修改数据源,而是通过函数绑定一路将修改数据源的行为传递到了定义数据源的组件 personList 中,由它最后真正完成对数据源的修改,这也遵循了组件不允许修改属于自己隔离域以外的任何数据和DOM元素的原则。


本文出处:http://www.tuicool.com/articles/reqAFn


最后编辑于 2016-12-23 10:25