AngularJs中组件---component的相关总结

目录

前言

  组件component是AngularJs中一个非常重要的概念,一个项目的前端代码结构可以完全以组件component为单位进行实现,也就是说项目的不同页面(比如列表页、详情页、编辑页)或者通用功能(比如表格渲染组件、分页组件)都可以设计为一个独立的component,再通过路由配置关联起来,实现功能需求。本篇以AngularJs官方文档为根据,对组件相关内容进行总结。


综述

  在Angular中,component是一种特殊的指令(directive),相比于directive,它的配置要更简单,这很适合于基于组件的应用程序结构,也使得使用类似于Web Components或者Angular 2风格的应用程序结构来编写代码要更容易。

组件component的优点

  • 比原生指令(directive)配置更加简单
  • 提供了更加健全的默认设置和最佳实践
  • 可以优化基于component的应用结构
  • 编写component这种directive使得升级到Angular 2更加容易

什么时候不要用component

  • 对那些依赖于DOM元素操作和绑定监听事件的指令不要使用,因为在component中是不能直接编译链接函数的。
  • 当需要定义配置一些更高级的directive选项时不要使用,选项比如:priority, terminal, multi-element。
  • 当你想通过一个属性或者CSS类来触发一个指令,而不是通过一个元素来触发时,不要使用。

创建并配置component组件

  要想注册一个component组件,可以使用Angular模型module上的.component()方法。(模型module是通过调用angular.module()来返回的。)该方法接收两个参数:

  • 组件名称(字符串)
  • 组件配置对象(注意,不同于.directive()方法,该方法不需要使用工厂函数)

示例

  1. 创建heroDetail.js文件,定义一个组件component

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function HeroDetailController() {
    //do something
    }

    angular.module('heroApp').component('heroDetail', {
    templateUrl: 'heroDetail.html',
    controller: HeroDetailController,
    bindings: {
    hero: '='
    }
    });
  2. 创建heroDetail.html文件,定义组件的DOM结构

    1
    <span>Name: {{$ctrl.hero.name}}</span>
  3. 创建index.js文件,增加控制器,为model变量初始化

    1
    2
    3
    4
    5
    angular.module('heroApp', []).controller('MainCtrl', function MainCtrl() {
    this.hero = {
    name: 'Spawn'
    };
    });
  4. 创建index.html文件,定义文档结构,引入定义好的组件heroDetail(驼峰写法要转换成短横线”-“相连)

    1
    2
    3
    4
    <div ng-controller="MainCtrl as ctrl">
    <b>Hero</b><br>
    <hero-detail hero="ctrl.hero"></hero-detail>
    </div>

结果如下图:

另外,也可以使用$compileProvider在module的配置阶段引入组件。

附:directive与component的比较

directive component
bindings No Yes(绑定到controller)
bindToController Yes(默认:false) No(使用bindings来替代)
compile function Yes No
controller Yes Yes(默认:function(){})
controllerAs Yes(默认:false) Yes(默认:$ctrl)
link functions Yes No
multiElement Yes No
priority Yes No
require Yes Yes
restrict Yes No(仅限于元素elements)
scope Yes(默认:false) No(scope总是独立的)
template Yes Yes(可注入)
templateNamespace Yes No
templateUrl Yes Yes(可注入)
terminal Yes No
transclude Yes(默认:false) Yes(默认:false)

基于component组件的应用程序结构

  如上所述,component使得按组件结构来组织应用程序变得更加容易。基于component组件的应用程序结构主要有以下几个特性:

  • component只控制它自己的视图和数据
      组件从不应修改超出它们作用域scope的任何数据或者DOM元素。一般来讲,在Angular中,通过作用域继承(inheritance)和监视(watches),我们能够修改程序中的任何数据。这是有实用性的,但是也可能导致一些问题-尤其当不明确到底程序中的哪一部分对数据修改负责时。这也就是为什么component这种directive采用了独立隔离作用域,这样对所有scope的操作就不可能发生了。
  • component具有定义完备的公用API-Inputs 和 Outputs
      然而,独立作用域也有捉襟见肘的时候,因为Angular采用了“双向绑定”。假设你给组件传入了这样一个对象:bindings: {item: '='},然后修改其中一个属性,你会发现这个变化也会反映在父级组件中。但是对于组件来说,为了更容易的判定什么数据发生变化,何时变化,只应该由拥有该数据的组件来对其进行修改。因此,component需要遵循一些简单的约定:
  1. 输入Inputs应该使用<@绑定。
        <符号表明“单向绑定”,是从1.5版本开始可用的。它和=的区别在于:组件作用域中的相关属性没有被监视(watched),这意味着如果你给属性赋了一个新值,父级作用域并不会因此而更新。但是要注意,父级和本级组件都是指向同一个对象,因此如果你在当前组件中修改了对象属性或者数组元素,父级组件还是能感受到这个变化。综上,一般性的规律是在组件作用域中永远不要修改对象或者数组属性。
        @绑定可以用于当输入是字符串,尤其是绑定的值不会发生改变时。

    1
    2
    3
    4
    bindings: {
    hero: '<',
    comment: '@'
    }
  2. 输出Outputs应该使用&来绑定。这说的输出是作为组件事件的回调函数。

    1
    2
    3
    4
    bindings: {
    onDelete: '&',
    onUpdate: '&'
    }
  3. 相比于直接操作输入数据,组件应该将变化的数据交付给正确的输出事件来处理。比如一次删除操作,组件不会直接删掉hero本身,而是通过正确的事件将数据传递给拥有该数据属性的组件。

    1
    2
    3
    <!-- note that we use kebab-case for bindings in the template as usual -->
    <editable-field on-update="$ctrl.update('location', value)"></editable-field><br>
    <button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
  4. 通过这种方式,父级组件可以来决定如何处理(比如,是删除一个元素还是更新属性。)

    1
    2
    3
    4
    5
    6
    7
    8
    ctrl.deleteHero(hero) {
    $http.delete(...).then(function() {
    var idx = ctrl.list.indexOf(hero);
    if (idx >= 0) {
    ctrl.list.splice(idx, 1);
    }
    });
    }
  • component具有定义完备的生命周期
      每一个组件都可以执行一套“声明周期钩子”,在生命周期的特定点,以下方法可以被调用:
  1. $onInit()
      满足以下条件时会在controller上调用该方法:
       a. 当一个元素上的全部controller都已经构建完成
       b. 它们的bindings已经初始化
       c. 在该元素上指令directives的pre & post linking functions执行之前

  将初始化代码放在这个方法中是很合适的。

  1. $onChanges(changesObj)
      在任何时刻,只要单向绑定的数据发生更新,就会调用该方法。changesObj是一个哈希表,它的键是发生变化的绑定属性的属性名称,它的值是一个对象,形式为:{ currentValue, previousValue, isFirstChange()}。比如当需要复制一个绑定的值以防外部值发生突变的时候,就可以触发该方法。
  2. $doCheck()
      该方法提供了一个检测并处理变化的接口。当检测到变化想做出相应处理时,任何的处理函数都必须从这个钩子调用。但是,当$onChanges已经调用时,这个钩子是不起作用的。举例来说,当你想进行一次“js对象深度相等比较”(这个概念应该参考js的深拷贝和浅拷贝),或者要检测一个Date对象这类操作时,它们的变化不会被Angular的变化检测器检测到,因此不会触发$onChanges,这时候就可以使用$doCheck()。这个钩子的调用没有参数,如果检测到了变化,必须手动保存旧值来和当前值比较。
  3. $onDestroy()
      当一个controller的作用域已经销毁,会调用该函数。使用该函数可以释放外部资源,watches监听函数和其他事件处理函数。
  4. $postLink()
      当控制器对应的元素及其子元素已经”link”完成(关于”link”,要单独分析$compile的相关内容),会调用该函数。和directive中的post-link function类似,这个钩子可以用来设置DOM元素事件处理函数,或者进行直接的DOM操作。注意一个例外,当前的controller对应的组件可能有子组件,那些包含有templateUrl的子元素在调用该函数的时候并没有”link”完成,因为它们要等待templateUrl指定的模板template异步加载完成,因此在完成之前它自己的编译和链接就会被挂起。这个指令可以看做是和Angular 2中的ngAfterViewInitngAfterContentInit作用类似。
  • 一个应用程序就是一棵组件树
      理论上最佳的状态是,整个应用应该由不同层级的component构成一棵树,能够处理定义完备的输入输出,并且具有最少的双向数据绑定。这样,就能够更加轻松地预测数据何时变化以及组件的状态是什么。

使用案例

  具体案例,详细参见这一篇使用AngularJs封装表格内容渲染(含分页)插件。整个表格插件就是一个大的component,实现过程中用到了上述很多特性。