Angular1組件通訊方式總結
這裏需要將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組件通訊方式總結