1. 程式人生 > >Angular1組件通訊方式總結

Angular1組件通訊方式總結

按鈕 list組件 emit image img 技術 共享 ng-click 場景

這裏需要將Angular1分為Angular1.5之前和Angular1.5兩個不同的階段來講,兩者雖然同屬Angular1,但是在開發模式上還是有較大區別的。在Angular1.4及以前,主要是基於HTML的,將所有view劃分為不同的HTML片段,通過路由,transclude,include等方式,按照用戶行為切換顯示不同界面。對於每個template內部來講,可以是純HTML,也可以是自定義的directive。directive之間可以有層級關系,也可以沒有層級關系。在Angular1.5中,引入了Component API,鼓勵使用單向數據流及Component tree 開發模式,在這種情況下,整個應用完全是基於組件的,除了root Component,其他Component都有自己的父級組件,組件之間主要是靠由上到下的單向數據流動來通信的,在這種模式下,directive不允許有自己的template,只能夠來封裝DOM操作。

在Angular1中提供了很多組件(指令)之間的通訊方式,本文主要來將這些方式一一列舉出來,便於總結和使用。

directive通訊方式1-共享Service

在Angular1中,所有service都是單例的,這意味著一旦應用啟動之後,在內存中會維護一些註冊的service實例,在代碼中任意地方對這些service的操作,都會導致其發生改變,而其他controller或者directive獲取該service的值,也是發生改變之後的值。在這種情況下,一個directive對service進行寫操作,另外一個directive對它進行讀操作,兩者就能通過該service進行通訊。

service定義

class HomeService {
  constructor() {
    this.names = [];
  }
  addName(name){
    this.names.push(name);
  }
}
10 export default HomeService;

directiveA定義

function aDirective(homeService) {
    "ngInject";
    return {
        restrict: ‘E‘,
        template: `name:<input type=‘text‘ ng-model=‘showValue‘>
  <br><button ng-click="addName()">addName</button>`,
        link: (scope, element, attrs) => {
            scope.addName 
= () => { if (scope.showValue) { homeService.addName(scope.showValue); } } } }; } export default aDirective;

directiveB定義

function bDirective(homeService) {
    "ngInject";
    return {
        restrict: ‘E‘,
        template: `<ul>
            <li ng-repeat="list in lists">{{list}}</li>
        </ul>`,
        link: (scope, element, attrs) => {
            scope.lists=homeService.names;
        }
    };
}

export default bDirective;

HTML

<main class="home">
  <h2>共享service實現Directive通信</h2>
  <div>
    <a-directive></a-directive>
    <b-directive></b-directive>
  </div>
</main>

在這裏,我們在homeService中定義了一個addName方法,用來給該service的names中添加元素,然後讓directiveA調用addName方法添加元素,隨著names屬性的變化,directiveB的list也會顯示添加的內容,結果如下:

技術分享圖片

directive通訊方式2-自定義事件$broadcast及$emit

Angular1提供了scope之間事件的傳播機制,使用scope.$emit可以往父級scope傳播自定義事件並攜帶數據,使用scope.$broadcast可以給所有子scope廣播事件和數據,所有需要接收到的scope需要使用scope.$on(eventName,callback)方法來監聽事件。該機制很有用,但是我認為能不用就不用,主要是在實際開發中大多數是使用$rootScope來廣播事件,每廣播一次,整個scope下面的$$listeners都要去檢測是否有對應的事件監聽從而執行,如果scope層級較深,那麽效率不會很高。除此之外,在各種directive,controller中監聽自定義事件會導致混亂,代碼不好去追蹤,而且有可能引發命名沖突。

directiveA定義

function aDirective(homeService,$rootScope) {
    "ngInject";
    return {
        restrict: ‘E‘,
        template: `name:<input type=‘text‘ ng-model=‘showValue‘ class=‘about‘>
  <br><button ng-click="addName()">addName</button>`,
        link: (scope, element, attrs) => {
            scope.addName = () => {
                if(scope.showValue){
                    $rootScope.$broadcast(‘addName‘,scope.showValue);
                }        
            }
        }
    };
}

export default aDirective;

directiveB定義

function bDirective(homeService) {
    "ngInject";
    return {
        restrict: ‘E‘,
        template: `<ul>
            <li ng-repeat="list in lists">{{list}}</li>
        </ul>`,
        link: (scope, element, attrs) => {
            scope.lists=[];
            scope.$on(‘addName‘,(...params)=>{
                scope.lists.push(params[1]);
            });
        }
    };
}

export default bDirective;

HTML

 <section>
2   <event-adirective class="about"></event-adirective>
3   <event-bdirective></event-bdirective>
4 </section>

在這裏,DirectiveA使用rootScope來廣播事件,directiveB來監聽事件,然後將事件傳遞的參數添加到lists數組當中去,結果同上。

directive通訊方式3-在link函數中使用attrs通訊

每個directive在定義的時候都有一個link函數,函數簽名的第三個參數是attrs,代表在該directive上面的所有atrributes數組,attrs提供了一些方法,比較有用的是$set和$observe,前者可以自定義attr或修改已經有的attr的值,後者可以監聽到該值的變化。利用這種方式,我們可以讓在位於同一個dom元素上的兩個directive進行通訊,因為它們之間共享一個attrs數組。

directiveA定義

function aDirective($interval) {
    "ngInject";
    return {
        restrict: ‘A‘,
        link: (scope, element, attrs) => {
            let deInterval=$interval(()=>{
              attrs.$set(‘showValue‘, new Date().getTime());
            },1000);
            scope.$on(‘$destroy‘,()=>{
                $interval.cancel(deInterval);
            });
        }
    };
}

export default aDirective;

directiveB定義

function bDirective() {
    "ngInject";
    return {
        restrict: ‘E‘,
        template: `<span>{{time|date:‘yyyy-MM-dd HH:mm:ss‘}}</span>`,
        link: (scope, element, attrs) => {
            attrs.$observe(‘showValue‘, (newVal)=>{
                scope.time=newVal;
                console.log(newVal);
            });
        }
    };
}

export default bDirective;

HTML

1 <div>
2   <h2>{{ $ctrl.name }}</h2>
3   <directive-b directive-a></directive-b>
4 </div>

這裏讓directiveA不斷修改showValue的值,讓directiveB來observe該值並顯示在template中,結果如下:

技術分享圖片

directive通訊方式4-使用directive的Controller+require來進行通訊

在directive中,可以在自己的controller中定義一些方法或屬性,這些方法或者屬性可以在其他directive中使用require來引入目標directive,然後在自己的link函數中的第四個參數中就可以拿到目標directive的實例,從而操作該實例,進行兩者通訊。

directiveA定義

function aDirective() {
    "ngInject";
    return {
        restrict: ‘E‘,
        transclude: true,
        scope: {},
        template: `<div><div ng-transclude></div><ul>
            <li ng-repeat="list in lists">{{list}}</li>
        </ul></div>`,
        controller: function($scope) {
            "ngInject";
            $scope.lists  = [];
             this.addName = (item) => {
                    $scope.lists.push(item);
            }
        }
    };
}

export default aDirective;

directiveB定義

function bDirective() {
    "ngInject";
    return {
        restrict: ‘E‘,
        require:‘^^aCtrlDirective‘,
        template: `name:<input type=‘text‘ ng-model=‘showValue‘>
  <br><button ng-click="addName()">addName</button>
       `,
        link: (scope, element, attrs,ctrls) => {
            scope.addName=function(){
                if(scope.showValue){
                    ctrls.addName(scope.showValue);
                }       
            }
        }
    };
}

export default bDirective;

HTML

<a-ctrl-directive>
  <div>
    <div>
      <b-ctrl-directive class=‘ctrls‘></b-ctrl-directive>
    </div>
    </div>
  </a-ctrl-directive>

在directiveA中定義自己的controller,暴露addName方法給外部,然後再directiveB中require引用directiveA,在directiveB的link函數中調用addName方法,從而操作directiveA的lists數組,lists並沒有在directiveB中定義。

Component通訊方式-單向數據流+$onChanges hook方法

在Angular1.5之後,為了更好的升級到Angular2,引入了Component API,並鼓勵使用單向數據流加組件樹開發模式,這和之前的directive相比,開發模式發生了比較大的變化。雖然component本身僅僅是directive語法糖,但是其巨大意義在於讓開發者脫離之前的HTML為核心,轉而適應組件樹開發方式,這種模式也是目前主流前端框架都鼓勵使用的模式,如Angular2,React及Vue。在這種模式下,上述幾種通訊方式仍然有效,但是並不是最佳實踐,Component由於天生具有層級關系,所以更鼓勵使用單向數據流+生命周期Hook方法來進行通訊。

這裏我們模擬一個查詢的場景,使用三個組件來完成該功能,最外層的一個searchBody組件,用來作為該功能的根組件,searchFiled組件用來接收用戶輸入,並提供查詢按鈕,searchList組件用來顯示查詢結果。

searchList定義(為了便於查看,我將該組件的HTMl及核心js一起展示)

import template from ‘./searchList.html‘;
import controller from ‘./searchList.controller‘;

let searchListComponent = {
  restrict: ‘E‘,
  bindings: {
    searchMessages:‘<‘,
    searchText:‘<‘
  },
  template,
  controller
};

class SearchListController {
    constructor() {
        this.name = ‘searchList‘;
    }
    $onInit() {
        this.initialMessages = angular.copy(this.searchMessages);
    }
    $onChanges(changesObj) {
        if (changesObj.searchText && changesObj.searchText.currentValue) {
            this.searchMessages = this.initialMessages.filter((message) => {
                return message.key.indexOf(this.searchText) !== -1;
            })
        }
    }


}

export default SearchListController;

<div>
  <ul class="search-ul">
    <li ng-repeat="item in $ctrl.searchMessages">
      {{item.key+"-"+item.val}}
    </li>
  </ul>
</div>

這裏定義了一個controller,將searchList的所有邏輯都放在該controller中,6-9行在component定義中使用單向綁定<來定義來將其父組件上的數據綁定到controller上。18-20行在$onInit方法中初始化保留一份原始數據供查詢使用。21-27行使用Angular1.5中Component的生命周期hook方法,當父組件中綁定的數據發生變化之後,都會觸發該方法,該方法有一個參數,changesObj代表本次發生變化的對象,我們需要監聽changesObj.searchText的變化,並按照searchText的最新值來過濾searchMessages.

searchField定義

import template from ‘./searchField.html‘;
import controller from ‘./searchField.controller‘;

let searchFieldComponent = {
  restrict: ‘E‘,
  bindings: {},
  template,
  controller,
  require:{
    searchBody:‘^searchBody‘
  }
};

class SearchFieldController {
  constructor() {
    this.searchWords = ‘‘;
  }
 
  doSearch(){
    if(!this.searchWords) return;
    this.searchBody.doSearch(this.searchWords);
  }

}

export default SearchFieldController;


<div>
  <input type="text" name="" value="" ng-model="$ctrl.searchWords">
  <button ng-click="$ctrl.doSearch()">search</button>
</div>

searchField的作用是使用input接受用戶輸入的查詢參數,然後在點擊button的時候調用searchBody的doSearch方法,來通知最外層的searchBody更新searchText。

searchBody定義

import template from ‘./searchBody.html‘;
import controller from ‘./searchBody.controller‘;

let searchBodyComponent = {
  restrict: ‘E‘,
  bindings: {},
  template,
  controller
};
class SearchBodyController {
  constructor() {
    this.searchTitle = ‘searchBody‘;
  }
  $onInit(){
    this.messages=[
      {key:"erer",val:"ererererererere"},
      {key:"1111",val:"111111111111111"},
      {key:"2222",val:"222222222222222"},
      {key:"3333",val:"333333333333333"},
      {key:"4444",val:"444444444444444"},
      {key:"5555",val:"555555555555555"},
      {key:"6666",val:"666666666666666"},
      {key:"7777",val:"777777777777777"},
      {key:"8888",val:"888888888888888"}
    ]
  }

  doSearch(text){
    this.searchText=text;
  }
}

export default SearchBodyController;

<div>
  <h1>{{ $ctrl.searchTitle }}</h1>
  <search-field></search-field>
  <search-list search-messages="$ctrl.messages" search-text="$ctrl.searchText"></search-list>
</div>

在上述代碼中的37-38行,引用searchField和searchList兩個組件,並將searchBody的messages及searchText作為最初的數據源傳遞給searchList組件,然後再searchField中點擊查詢按鈕,會調用searchBody的doSearch方法,改變searchBody的searchText的值,然後觸發searchList中的$onChanges方法,從而過濾相關結果,可以看到所有數據都是從上到下單向流動的,組件之間都是靠數據來通信的。

Angular1組件通訊方式總結