原文釋出在個人獨立部落格上,連結:http://pengisgood.github.io/2016/01/31/communication-between-multiple-angular-apps/
通常情況下,在 Angular 的單頁面應用中不同的 Controller 或者 Service 之間通訊是一件非常容易的事情,因為 Angular 已經給我們提供了一些便利的方法:$on
,$emit
,$broadcast
。
在這裡用一個簡單的例子來演示一下這三個方法的用途,完整版程式碼也可以參考這裡:
style.css
1 body { 2 background-color: #eee; 3 } 4 5 #child { 6 background-color: red; 7 } 8 9 #grandChild { 10 background-color: yellow; 11 } 12 13 #sibling { 14 background-color: pink; 15 } 16 17 .level { 18 border: solid 1px; 19 margin: 5px; 20 padding: 5px; 21 }
index.html
1 <body ng-app="app" ng-controller="ParentCtrl" class='level'> 2 <h2>Parent</h2> 3 <button ng-click="broadcastMsg()">Broadcast msg</button> 4 <button ng-click="emitMsg()">Emit msg</button> 5 <pre>Message from: {{message}}</pre> 6 <div id='child' ng-controller="ChildCtrl" class='level'> 7 <h2>Child</h2> 8 <button ng-click="broadcastMsg()">Broadcast msg</button> 9 <button ng-click="emitMsg()">Emit msg</button> 10 <pre>Message from: {{message}}</pre> 11 12 <div id='grandChild' ng-controller="GrandChildCtrl" class='level'> 13 <h2>Grand child</h2> 14 15 <pre>Message from: {{message}}</pre> 16 </div> 17 </div> 18 <div id='sibling' ng-controller="SiblingCtrl" class='level'> 19 <h2>Sibling</h2> 20 <button ng-click="broadcastMsg()">Broadcast msg</button> 21 <button ng-click="emitMsg()">Emit msg</button> 22 <pre>Message from: {{message}}</pre> 23 </div> 24 </body>
app.js
1 var app = angular.module('app', []) 2 app.controller('ParentCtrl', function($scope) { 3 $scope.message = '' 4 5 $scope.broadcastMsg = function() { 6 $scope.$broadcast('msg_triggered','parent') 7 } 8 9 $scope.emitMsg = function() { 10 $scope.$emit('msg_triggered','parent') 11 } 12 13 $scope.$on('msg_triggered', function(event, from){ 14 $scope.message = from 15 }) 16 }) 17 18 app.controller('ChildCtrl', function($scope) { 19 $scope.message = '' 20 $scope.broadcastMsg = function() { 21 $scope.$broadcast('msg_triggered','child') 22 } 23 24 $scope.emitMsg = function() { 25 $scope.$emit('msg_triggered','child') 26 } 27 28 $scope.$on('msg_triggered', function(event, from){ 29 $scope.message = from 30 }) 31 }) 32 33 app.controller('GrandChildCtrl', function($scope) { 34 $scope.message = '' 35 36 $scope.$on('msg_triggered', function(event, from){ 37 $scope.message = from 38 }) 39 }) 40 41 app.controller('SiblingCtrl', function($scope) { 42 $scope.message = '' 43 $scope.broadcastMsg = function() { 44 $scope.$broadcast('msg_triggered','sibling') 45 } 46 47 $scope.emitMsg = function() { 48 $scope.$emit('msg_triggered','sibling') 49 } 50 51 $scope.$on('msg_triggered', function(event, from){ 52 $scope.message = from 53 }) 54 })
在上面的例子中我們可以看出,利用 Angular 已有的一些 API 能夠很方便的在不同 Controller 之間通訊,僅需要廣播事件即可。
上面的程式碼之所以能工作,是因為我們一直都有著一個前提,那就是這些 Controller 都在同一個 ng-app 中。那麼,如果在一個頁面中存在多個 ng-app 呢?(儘管並不推薦這樣做,但是在真實的專案中,尤其是在一些遺留專案中,仍然會遇到這種場景。)
先看一個簡單的例子:
style.css
1 .app-container { 2 height: 200px; 3 background-color: white; 4 padding: 10px; 5 } 6 7 pre { 8 font-size: 20px; 9 }
index.html
1 <body> 2 <div class="app-container" ng-app="app1" id="app1" ng-controller="ACtrl"> 3 <h1>App1</h1> 4 <pre ng-bind="count"></pre> 5 <button ng-click="increase()">Increase</button> 6 </div> 7 <hr /> 8 <div class="app-container" ng-app="app2" id="app2" ng-controller="BCtrl"> 9 <h1>App2</h1> 10 <pre ng-bind="count"></pre> 11 <button ng-click="increase()">Increase</button> 12 </div> 13 </body>
app.js
1 angular 2 .module('app1', []) 3 .controller('ACtrl', function($scope) { 4 $scope.count = 0; 5 6 $scope.increase = function() { 7 $scope.count += 1; 8 }; 9 }); 10 11 angular 12 .module('app2', []) 13 .controller('BCtrl', function($scope) { 14 $scope.count = 0; 15 16 $scope.increase = function() { 17 $scope.count += 1; 18 }; 19 });
Angular 的啟動方式
直接執行這段程式碼,我們會發現第二個 ng-app 並沒有工作,或者說第二個 ng-app 並沒有自動啟動。為什麼會這樣呢?相信對 Angular 瞭解比較多的人會馬上給出答案,那就是 Angular 只會自動啟動找到的第一個 ng-app,後面其他的 ng-app 沒有機會自動啟動。
如何解決這個問題呢?我們可以手動啟動後面沒有啟動的ng-app。舉個例子:
hello_world.html
1 <!doctype html> 2 <html> 3 <body> 4 <div ng-controller="MyController"> 5 Hello {{greetMe}}! 6 </div> 7 <script src="http://code.angularjs.org/snapshot/angular.js"></script> 8 9 <script> 10 angular.module('myApp', []) 11 .controller('MyController', ['$scope', function ($scope) { 12 $scope.greetMe = 'World'; 13 }]); 14 15 angular.element(document).ready(function() { 16 angular.bootstrap(document, ['myApp']); 17 }); 18 </script> 19 </body> 20 </html>
手動啟動需要注意兩點:一是當使用手動啟動方式時,DOM 中不能再使用 ng-app 指令;二是手動啟動不會憑空建立不存在的 module,因此需要先載入 module 相關的程式碼,再呼叫angular.bootstrap
方法。如果你對 Angular 的啟動方式還是不太明白的話,請參考官方文件。
現在關於Angular 啟動的問題解決了,可能有的人會問,如果我的頁面中在不同的地方有很多需要手動啟動的 ng-app 怎麼辦呢?難道我要一遍一遍的去呼叫angualar.bootstrap
嗎?這樣的程式碼看上去總覺得哪裡不對,重複的程式碼太多了,因此我們需要重構一下。這裡重構的方式可能多種多樣,我們採用的方式是這樣的:
main.js
1 $(function(){ 2 $('[data-angular-app]').each(function(){ 3 var $this = $(this) 4 angular.bootstrap($this, [$this.attr('data-angular-app'])) 5 }) 6 })
先將程式碼中所有的 ng-app 改為 data-angular-app,然後在 document ready 的時候用 jQuery 去解析 DOM 上所有的data-angular-app
屬性,拿到 ng-app 的值,最後用手動啟動的方式啟動 Angular。
Mini Pub-Sub
趟過了一個坑,我們再回到另一個問題上,如何才能在多個 ng-app 中通訊呢?畢竟它們都已經不在相同的 context 中了。這裡需要說明一下,在 Angular 中 ng-app 在 DOM 結構上是不能有巢狀關係的。每個 ng-app 都有自己的 rootScope,我們不能再直接使用 Angular 自己提供的一些 API 了。因為不管是 $broadcast
還是$emit
,它們都不能跨越不同的 ng-app。相信瞭解釋出訂閱機制的人(尤其是做過 WinForm 程式的人)能夠很快想到一種可行的解決方案,那就是我們自己實現一個簡易的釋出訂閱機制,然後通過釋出訂閱自定義的事件在不同的 ng-app 中通訊。
聽起來感覺很簡單,實際上做起來也很簡單。Talk is cheap, show me the code.
首先我們需要一個管理事件的地方,詳細的解釋[參考 StackOverflow 上的這個帖(http://stackoverflow.com/a/2969692/3049524)。
event_manager.js
1 (function($){ 2 var eventManager = $({}) 3 4 $.subscribe = function(){ 5 eventManager.bind.apply(eventManager, fn) 6 } 7 8 $.publish = function(){ 9 eventManager.trigger.apply(eventManager, fn) 10 } 11 })(jQuery)
暫時只實現了兩個 API,一個subscribe
用於訂閱事件,publish
用於釋出事件。
訂閱事件:
1 $.subscribe('user_rank_changed', function(event, data){ 2 $timeout(function(){ 3 // do something 4 }) 5 })
釋出事件:
1 $.publish('user_rank_changed', {/*some data*/})
這裡用了一個小 trick,因為我們的事件釋出訂閱都是採用的 jQuery 方式,為了讓 Angular 能夠感知到 scope 上資料的變化,我們將整個回撥函式包在了$timeout
中,由 JavaScript 自己放到時間迴圈中去等到空閒的時候開始執行,而不是使用$scope.$apply()
方法,是因為有些時候直接呼叫該方法會給我們帶來另一個Error: $digest already in progress
的錯誤。雖然也可以用$rootScope.$$phase || $rootScope.$apply();
這種方式來規避,但是個人認為還是略顯 tricky,沒有$timeout
的方式優雅。
因為我們用的是原生的 JavaScript 的事件機制,所以即使我們的 Controller 或者 Service 處於不同的 ng-app 中,我們也能夠輕鬆地相互傳輸資料了。
改進原則
在Angular 的單頁面應用中,我們儘量一個應用只有一個 ng-app,然後通過 Module 對業務進行模組劃分,而不是 ng-app。不到萬不得已,不要和 jQuery 混著用,總是使用 Angular 的思維方式進行開發,否則一不小心就會掉進資料不同步的坑中。
本人的個人獨立部落格將會逐步遷移至:http://pengisgood.github.io/。
http://pengisgood.github.io/2016/01/31/communication-between-multiple-angular-apps/