1. 程式人生 > >angular學習(十三)——Component

angular學習(十三)——Component

理解Components

AngularJS中,Component是一種特殊的directive,它的配置更簡單一些,非常適合元件化的app架構。使用web元件和使用Angular風格的app架構使得編寫app更為簡便。

Component的優點:

  • 比普通directive要簡單很多
  • 更加嚴謹,更加規範化
  • 更加適合元件化架構
  • component更容易升級到angular2

不使用Component的情況

  • 需要在編譯階段和預連結階段執行的directive,因為Component這時還不可用
  • 當你需要directive才有定義的選項時,如priority, terminal, multi-element
  • 當你需要directive由屬性,css的class而不是元素觸發時

Components的建立和配置

Components由angularjs的module使用.component()方法註冊。這個方法接受2個引數:

  • Component的名稱(字串型別)
  • Component的配置物件(注意,和.directive()不一樣,不是一個工廠方法而僅僅是個配置物件)

heroApp.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="uft-8"/>
    <title></title
>
</head> <script src="script/angular.min.js"></script> <body ng-app="heroApp"> <div ng-controller="MainCtrl as ctrl"> <b>Hero</b><br> <hero-detail hero="ctrl.hero"></hero-detail> </div> </body> <script> angular.module('heroApp'
, []).controller('MainCtrl', function MainCtrl() { this.hero = { name: 'Spawn' }; }); function HeroDetailController() { } angular.module('heroApp').component('heroDetail', { templateUrl: 'heroDetail.html', controller: HeroDetailController, bindings: { hero: '=' } });
</script> </html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

heroDetail.html:

<span>Name: {{$ctrl.hero.name}}</span>
  • 1
  • 1

Directive和Component之間的定義比較

屬性 Directive Component
bindings No Yes (binds to controller)
bindToController Yes (default: false) No (use bindings instead)
compile function Yes NO
controller Yes Yes (default function() {})
controllerAs Yes (default: false) Yes (default: $ctrl)
link functions Yes No
multiElement Yes No
priority Yes No
replace Yes (deprecated) No
require Yes Yes
restrict Yes No (restricted to elements only)
scope Yes (default: false) No (scope is always isolate)
template Yes Yes, injectable
templateNamespace Yes No
templateUrl Yes Yes, injectable
terminal Yes No
transclude Yes (default: false) Yes (default: false)

元件化app架構

如前所述,component使得使用元件化架構構建app更為容易,除此之外component還具備的能力具體如下:

  • Component只能控制它自己的檢視和資料:Component不會修改它自身scope之外的任何資料或DOM。通常情況下,AngularJS中可以通過scope繼承和watch可以修改任何地方的資料,這確實很實用,但是也會導致很難明確哪個部分對修改資料負責。這就是為什麼Component使用隔離範圍,也因此無法進行所有scope的操作。
  • Component有明確定義的公共api-輸入輸出:隔離範圍並不是全部,因為AngularJS是雙向繫結的。如果你傳一個物件到元件中,類似bindings: {item: '='},然後修改物件的屬性,修改會反映到它的父元件中。但是對於component來說,component確實只是修改了它自己的scope內的資料。這樣就很清晰的得知什麼資料什麼時候被修改。就此,component遵循一些簡單的約定:

    • 輸入需要使用<@繫結。<符號在1.5之後表示單向繫結,和=不同的是,它繫結的屬性在component的scope哪不會被watch,這意味著你可以在component的scope內給屬性設定一個新的值,它並不會更新父scope裡的值。但是如果父scope和component的scope引用的是同一個物件,比如你在component修改物件的屬性或者陣列中的元素,這種改變仍然會反映到父scope。因此在<繫結後不要在component內修改物件的屬性和陣列的元素。當輸入的值是字串時可以使用@繫結,特別是在繫結的值不會更改的時候。

      bindings: {
          hero: '<',
          comment: '@'
      }
      • 1
      • 2
      • 3
      • 4
      • 1
      • 2
      • 3
      • 4
    • 輸出使用&進行繫結,繫結的函式將作為component事件的回撥函式

      bindings: {
          onDelete: '&',
          onUpdate: '&'
      }
      • 1
      • 2
      • 3
      • 4
      • 1
      • 2
      • 3
      • 4
    • 在不操作輸入資料的情況下,component可以呼叫相應的輸出事件改變輸出資料。比如刪除,並不是hero自己刪除自己,而是通過相應的事件把自己傳給父component

      <!-- 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>
      • 1
      • 2
      • 3
      • 1
      • 2
      • 3
    • 這樣,父component來決定事件執行什麼(刪除item還是更新屬性)

      ctrl.deleteHero(hero) {
          $http.delete(...).then(function() {
              var idx = ctrl.list.indexOf(hero);
              if (idx >= 0) {
                  ctrl.list.splice(idx, 1);
              }
          }); 
      }
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
  • Component有明確定義的生命週期:每個元件都可以實現lifecycle hooks,這些方法會在component生命週期的相應的點被呼叫,可以實現的hook方法如下:

    • $onInit() - 在element上的所有controller構造和所有的繫結初始化之後,在element之上字首&的函式連結之前,每一個controller呼叫這個鉤子方法。這是個給controller寫一些初始化方法的好地方
    • $onChanges(changesObj) - 當單向繫結更新時呼叫,changesObj是一個hash鍵值對,key是已被修改的繫結屬性的name,value是一個物件,格式是{ currentValue, previousValue, isFirstChange() },使用這個hook可以只觸發元件內的更新,比如克隆繫結屬性的物件以防止它被外部意外更新
    • $doCheck() - 在每一個digest迴圈被呼叫,提供了一個機會可以在資料更改時驗證或者做一些操作。任何你希望進行的操作(比如對更改的響應)都必須通過這個hook呼叫。當$onChanges被呼叫時,實現這個hook沒什麼作用。比如,比如你想實現一個深度的equal函式檢查,或者檢查一個Date物件時,他將會非常有用,因為這是AngularJS的變化檢測器檢測不到這個變化,自然$onChanges也不會被呼叫。這個hook不包含任何引數,因此,如果想要檢測變化,你需要儲存之前的值,然後和現在的值進行比較。
    • $onDestroy() - 當controller包含的scope銷燬時,controller會呼叫這個hook。使用這個hook可以釋放一些外部資源,watch和事件處理程式
    • $postLink() - 在controller的element和子元素被連結後呼叫。和post-link函式類似,這個hook可以設定DOM事件的處理程式或者直接進行DOM操作。包含templateUrl指令的element不會被編譯和連結,因為它們要等template非同步載入。它們的編譯和連結要等template完成之後才會執行。這個hook非常和angular2中的ngAfterViewInit和ngAfterContentInit兩個hook類似。因為angular1和angular2編譯過程的不同,所以在anguar1升級到angular2時一定要小心。

    通過實現這些方法,你的component可以在生命週期中hook

  • app就是一棵component樹:理想的情況下,整個app是一棵component構成的樹,清晰的定義了輸入輸出,並儘量不使用雙向繫結。這樣更容易預測資料的變化和元件狀態。

Component樹示例

下面這個示例對上面對例子進行了擴充套件,並和我們剛講過到概念結合起來:

不再使用ngController,現在使用擁有多個hero的heroList元件象,為每個hero建立一個heroDetail元件。

heroDetail元件包含的新功能:

  • 一個delete按鈕,可以呼叫繫結到heroList元件的onDelete函式
  • 一個可以改變hero的location的輸入控制元件,該控制元件是可重用的editableField元件。不是editableField元件自己操作hero物件,它將改變傳給heroDetail元件,heroDetail元件再傳給heroList元件,由heroList元件更新原始資料。

heroApp.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="uft-8"/>
    <title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="heroApp">
<hero-list></hero-list>
</body>
<script>
    /**
     * index.js
     */
    angular.module('heroApp', []);



    /**
     * heroList.js
     */
    function HeroListController($scope, $element, $attrs) {
        var ctrl = this;

        // This would be loaded by $http etc.
        ctrl.list = [
            {
                name: 'Superman',
                location: ''
            },
            {
                name: 'Batman',
                location: 'Wayne Manor'
            }
        ];

        ctrl.updateHero = function(hero, prop, value) {
            hero[prop] = value;
        };

        ctrl.deleteHero = function(hero) {
            var idx = ctrl.list.indexOf(hero);
            if (idx >= 0) {
                ctrl.list.splice(idx, 1);
            }
        };
    }

    angular.module('heroApp').component('heroList', {
        templateUrl: 'heroList.html',
        controller: HeroListController
    });


    /**
     * heroDetail.js
     */
    function HeroDetailController() {
        var ctrl = this;

        ctrl.delete = function() {
            ctrl.onDelete({hero: ctrl.hero});
        };

        ctrl.update = function(prop, value) {
            ctrl.onUpdate({hero: ctrl.hero, prop: prop, value: value});
        };
    }

    angular.module('heroApp').component('heroDetail', {
        templateUrl: 'heroDetail.html',
        controller: HeroDetailController,
        bindings: {
            hero: '<',
            onDelete: '&',
            onUpdate: '&'
        }
    });


    /**
     * editableField.js
     */
    function EditableFieldController($scope, $element, $attrs) {
        var ctrl = this;
        ctrl.editMode = false;

        ctrl.handleModeChange = function() {
            if (ctrl.editMode) {
                ctrl.onUpdate({value: ctrl.fieldValue});
                ctrl.fieldValueCopy = ctrl.fieldValue;
            }
            ctrl.editMode = !ctrl.editMode;
        };

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

        ctrl.$onInit = function() {
            // Make a copy of the initial value to be able to reset it later
            ctrl.fieldValueCopy = ctrl.fieldValue;

            // Set a default fieldType
            if (!ctrl.fieldType) {
                ctrl.fieldType = 'text';
            }
        };
    }

    angular.module('heroApp').component('editableField', {
        templateUrl: 'editableField.html',
        controller: EditableFieldController,
        bindings: {
            fieldValue: '<',
            fieldType: '@?',
            onUpdate: '&'
        }
    });

</script>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122

heroList.html

<b>Heroes</b><br>
<hero-detail ng-repeat="hero in $ctrl.list" hero="hero" on-delete="$ctrl.deleteHero(hero)" on-update="$ctrl.updateHero(hero, prop, value)"></hero-detail>
  • 1
  • 2
  • 1
  • 2

heroDetail.html

<hr>
<div>
  Name: {{$ctrl.hero.name}}<br>
  Location: <editable-field field-value="$ctrl.hero.location" field-type="text" on-update="$ctrl.update('location', value)"></editable-field><br>
  <button ng-click="$ctrl.delete()">Delete</button>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

editableField.html

<span ng-switch="$ctrl.editMode">
  <input ng-switch-when="true" type="{{$ctrl.fieldType}}" ng-model="$ctrl.fieldValue">
  <span ng-switch-default>{{$ctrl.fieldValue}}</span>
</span>
<button ng-click="$ctrl.handleModeChange()">{{$ctrl.editMode ? 'Save' : 'Edit'}}</button>
<button ng-if="$ctrl.editMode" ng-click="$ctrl.reset()">Reset</button>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Component作為route模版

當使用ngRoute時,Component作為route的模版也是非常有用的。一個元件化的app,每一個檢視都是一個元件:

var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
  template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
  controller: function() {
    this.user = {name: 'world'};
  }
});
myMod.config(function($routeProvider) {
  $routeProvider.when('/', {
    template: '<home></home>'
  });
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

使用$routeProvider時,你常常可以避開一些樣板將已解決達route依賴傳到component中。1.5版本之後,ngRoute可以自動分配resolve到route的scope屬性$resolve,你也可以通過resolveAs配置屬性名。當用component時,你可以利用component的優勢,直接把resolve傳到你的component中,而不用建立額外的route控制器。

var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
  template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
  bindings: {
    user: '<'
  }
});
myMod.config(function($routeProvider) {
  $routeProvider.when('/', {
    template: '<home user="$resolve.user"></home>',
    resolve: {
      user: function($http) { return $http.get('...'); }
    }
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Component間的通訊

Directive需要其他Directive的controller來相互通訊。在component中可以為require屬性提供一個物件map來實現,map的key是所需controller構成的屬性,而map的值是所需元件的名稱。

不過要注意的是,所需的controller在它初始化完成之前是不可用的,但是可以確定的是當前controller在$onInit執行之前所需的controller是可用的。

下面的例子是將上一篇文章由Directive方式改為了component方式:

docsTabsExample.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="uft-8"/>
    <title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsTabsExample">
<my-tabs>
    <my-pane title="Hello">
        <p>Lorem ipsum dolor sit amet</p>
    </my-pane>
    <my-pane title="World">
        <em>Mauris elementum elementum enim at suscipit.</em>

        <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
    </my-pane>
</my-tabs>
</body>
<script>
    angular.module('docsTabsExample', [])
            .component('myTabs', {
                transclude: true,
                controller: ['$scope', function MyTabsController($scope) {
                    var panes = this.panes = [];

                    this.select = function (pane) {
                        angular.forEach(panes, function (pane) {
                            pane.selected = false;
                        });
                        pane.selected = true;
                    };

                    this.addPane = function (pane) {
                        if (panes.length === 0) {
                            this.select(pane);
                        }
                        panes.push(pane);
                    };
                }],
                templateUrl: 'my-tabs.html'

            })
            .component('myPane', {
                require: {
                    tabsCtrl: '^myTabs'
                },
                transclude: true,
                bindings: {
                    title: '@'
                },
                controller: function