現在幾乎滿世界的人都在問! 外面有人麼? 這裡是USS AngularJS, 我們遇到麻煩了,我們的服務講得是克靈貢語(Klingon) 而我們的控制器不能同它們的Ferengi 指示通信了. 有人能幫助我們麼!
我已經不知道有多少次遇到這種有關什麼才是AngularJS裡面的組件通信的最佳方式這樣的問題了. 很多時候答案都會是為此使用$rootScope 對象去向任何想要收聽的人廣播$broadcast出一條訊息. 然而,那還真不是做這件事的最佳方式. 組件之間廣播訊息意味著它們需要多少知道一些其它組件編碼的細節,這樣就限制了它們的模組化和重用.
本文我將展示如何為AngularJS中的內部元件通訊使用發布/訂閱模式.
AngularJS 有多種方式可供你用於組件之間的通信,而最常使用的方法卻需要你知道太多有關那些組件如何通信的細節,這樣就增加了組件之間的耦合度,並降低了它們的模組性和內聚程度. 這樣也就使得你的組件很難在其它應用程序中重用.
透過使用發布/訂閱設計模式,我們可以降低組件之間的耦合度,並將它們的之間通信的細節封裝起來. 這將能幫助增加你組件的模組化程度,可測試性以及可重用性.
我將會描述的發布/訂閱模式實現由 Eric Burley, @eburley 在它的帖子angularjs.org 觀察, 有關發布訂閱模式.. 中推薦過。
我所描述的範例應用程序,會向你展示你可以將發布/訂閱模式如何運用於內部控制器通信以及控制器的服務通信. 你可以在GitHub上我的資源庫angularjs- pubsub 下面找到原始碼.
首先我們需要一條通訊管道
首先我們來講講用於處理發布和訂閱資訊的服務。我定義了一個服務接口,提供了發布和訂閱資訊的方法,我們可以用它來處理我們想要用來交換的資訊。
在下面的程式碼中,我定義了兩個內部資訊; _EDIT_DATA_, 用來表示我們需要編輯跟隨資訊傳過來的數據,和 _DATA_UPDATED_, 用來表示我們的資料已經被改變。這些都是定義在內部的,使用者沒辦法存取它們的,這有助於隱藏具體實作。
而對於每個訊息,有兩個方法; 一個用來發布訊息推送給訂閱者,另一個可以讓訂閱者註冊一個回調方法,當接收到訊息的時候,這個方法就會被呼叫。
用來向訂閱者發布資訊方法是 editData,在第 9 行,還有 dataUpated,在第 19 行。它們透過 $rootScope.$broadcast 方法向待處理事件推播私有通知。
用來註冊事件的方法,透過 $scope.$on 建立監聽,當接收到廣播的訊息之後,就會輪流執行那些被訂閱者註冊到服務上的事件。同時,由於訂閱者需要自己的 scope 作為參數傳過來,我們可以用它來執行監聽的信息,從而避免了維護監聽者列表這些複雜的處理。註冊事件的方法是 onEditData,在 13 行,還有 onDataUpdated 在 23 行。
為了隱藏實作細節,我使用了 Revealing Module (揭示模組:好醜的名字)模式,只回傳那些我希望讓使用者使用的方法。
angular.module(['application.services']) // define the request notification channel for the pub/sub service .factory('requestNotificationChannel', ['$rootScope', function ($rootScope) { // private notification messages var _EDIT_DATA_ = '_EDIT_DATA_'; var _DATA_UPDATED_ = '_DATA_UPDATED_'; // publish edit data notification var editData = function (item) { $rootScope.$broadcast(_EDIT_DATA_, {item: item}); }; //subscribe to edit data notification var onEditData = function($scope, handler) { $scope.$on(_EDIT_DATA_, function(event, args) { handler(args.item); }); }; // publish data changed notification var dataUpdated = function () { $rootScope.$broadcast(_DATA_UPDATED_); }; // subscribe to data changed notification var onDataUpdated = function ($scope, handler) { $scope.$on(_DATA_UPDATED_, function (event) { handler(); }); }; // return the publicly accessible methods return { editData: editData, onEditData: onEditData, dataUpdated: dataUpdated, onDataUpdated: onDataUpdated }; }])
發布訊息
發布訊息很簡單,首先我們需要在我們的控制器裡為requestNotificationChannel 引入一些依賴. 你可以在下面dataService的定義第二行看到這個. 當事件發生時,如果需要向需要了解有變化發生的其它物件發送訊號, 你只需要呼叫requestNotificationChannel上恰當的通知方法就可以了. 如果你注意到了dataService的saveHop, deleteHop 和addHop 方法, 你就會看到它們都呼叫了requestNotificationChannel 上的dataUpdated方法, 這個方法將會給偵聽器發送訊號,偵聽器則已經用onDataUpdated 方法註冊過了.
// define the data service that manages the data .factory('dataService', ['requestNotificationChannel', function (requestNotificationChannel) { // private data var hops = [ { "_id": { "$oid": "50ae677361d118e3646d7d6c"}, "Name": "Admiral", "Origin": "United Kingdom", "Alpha": 14.75, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Bittering hops derived from Wye Challenger. Good high-alpha bittering hops. Use for: Ales Aroma: Primarily for bittering Substitutions: Target, Northdown, Challenger", "Type": "Bittering", "Form": "Pellet", "Beta": 5.6, "HSI": 15.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} , { "_id": { "$oid": "50ae677361d118e3646d7d6d"}, "Name": "Ahtanum", "Origin": "U.S.", "Alpha": 6.0, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Distinctive aromatic hops with moderate bittering power from Washington. Use for: Distinctive aroma Substitutes: N/A", "Type": "Aroma", "Form": "Pellet", "Beta": 5.25, "HSI": 30.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} , { "_id": { "$oid": "50ae677361d118e3646d7d6e"}, "Name": "Amarillo Gold", "Origin": "U.S.", "Alpha": 8.5, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Unknown origin, but character similar to Cascade. Use for: IPAs, Ales Aroma: Citrus, Flowery Substitutions: Cascade, Centennial", "Type": "Aroma", "Form": "Pellet", "Beta": 6.0, "HSI": 25.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} , { "_id": { "$oid": "50ae677361d118e3646d7d6f"}, "Name": "Aquila", "Origin": "U.S.", "Alpha": 6.5, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Aroma hops developed in 1988. Limited use due to high cohumolone.Used for: Aroma hops Substitutes: ClusterNo longer commercially grown.", "Type": "Aroma", "Form": "Pellet", "Beta": 3.0, "HSI": 35.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} , { "_id": { "$oid": "50ae677361d118e3646d7d70"}, "Name": "Auscha (Saaz)", "Origin": "Czech Republic", "Alpha": 3.3, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": " Use for: Pilsners and Bohemian style lagers Aroma: Delicate, mild, clean, somewhat floral -- Noble hops Substitute: Tettnanger, LublinExamples: Pulsner Urquell", "Type": "Aroma", "Form": "Pellet", "Beta": 3.5, "HSI": 42.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} , ]; // sends notification that data has been updated var saveHop = function(hop) { requestNotificationChannel.dataUpdated(); }; // removes the item from the array and sends a notification that data has been updated var deleteHop = function(hop) { for(var i = 0; i < hops.length; i++) { if(hops[i]._id.$oid === hop._id.$oid) { hops.splice(i, 1); requestNotificationChannel.dataUpdated(); return; } }; }; // internal function to generate a random number guid generation var S4 = function() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); }; // generates a guid for adding items to array var guid = function () { return (S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase(); }; // function to add a hop to the array and sends a notification that data has been updated var addHop = function(hop) { hops.id.$oid = guid(); hops.push(hop); requestNotificationChannel.dataUpdated(); }; // returns the array of hops var getHops = function() { return hops; }; // returns a specific hop with the given id var getHop = function(id) { for(var i = 0; i < hops.length; i++) { if(hops[i]._id.$oid === id) { return hops[i]; } }; }; // return the publicly accessible methods return { getHops: getHops, getHop: getHop, saveHop: saveHop, deleteHop: deleteHop, addHop: addHop } }]);
接收事件通知
从 requestNotificationChannel 接收事件通知也很简单,额外的我们只需要回调处理器来在消息被发送时使用通知来做一些自己的处理. 我们将再次需要添加一些依赖到面向我们的控制器、服务以及指令的 requestNotificationChannel 上, 你可以在下面代码的第二行中看到这些. 接下来我们需要定义一个事件回调处理器来对事件通知做出回应,你可以在下面的第五行代码中看到. 然后我们需要通过调用 onDataUpdated 方法来吧我们的回调处理器注册到requestNotificationChannel,并传入来自控制器和回调处理器的范围, 我们在第9行代码中做了这些事情.
//define the controller for view1 .controller('view1-controller', ['$scope', 'dataService', 'requestNotificationChannel', function($scope, dataService, requestNotificationChannel) { $scope.hops = dataService.getHops(); var onDataUpdatedHandler = function() { $scope.hops = dataService.getHops(); } requestNotificationChannel.onDataUpdated($scope, onDataUpdatedHandler); $scope.onEdit = function(hop) { requestNotificationChannel.editData(hop); } $scope.onDelete = function(hop) { dataService.deleteHop(hop); } }]);
用于控制器通信的控制器
我们也可以将 the requestNotificationChannel 用于控制器间的通信. 我们只需要有一个控制器扮演发布者的角色,而另外一个控制器扮演订阅者的角色就行了. 如果你观察到前段代码第11行view1-controller的onEdit方法,你会看到它发送了一个editData消息,消息包含需要使用 requestNotificationChannel 编辑的项. 下面的 view2-controller 从第5行到第9行将它的 onEditDataHandler 用 requestNotificationChannel 进行了注册. 如此无论何时view1-controller一旦发送editData消息,带上要修改的项,view2-controller都会受到editData消息的通知,获得该项并将其更新到它的模型.
//define the controller for view1 .controller('view2-controller', ['$scope', 'dataService', 'requestNotificationChannel', function($scope, dataService, requestNotificationChannel) { $scope.hop = null; var onEditDataHandler = function(item) { $scope.hop = item; }; requestNotificationChannel.onEditData($scope, onEditDataHandler); $scope.onSave = function() { dataService.saveHop($scope.hop); $scope.hop = null; } $scope.onCancel = function() { $scope.hop = null; } }]);
写一个好的接口文档
有一件事情可能会被忽略,我们在组件间用了通信接口,而这些接口,它们需要一个好的文档来说明应当如何使用。上面的例子中,如果没有文档,用户肯定不会知道 onEditData 会给回调函数传一个待编辑数据。所以当你开始用这个模式,用好的技巧在于,给方法写注释文档,以确保通知服务明确知道发生了什么事情。
总结
好了,我们探讨了如何在你的 AngularJS 应用中使用订阅/发布模式来实现模块间通信。该模式可以让你的模块从内部消息解耦,更便于复用。你甚至可以把模块之间的通信全部替换成订阅/发布模式。尤其当你的服务中有很多异步请求,以及你希望把数据缓存在服务中,从而减少和服务器通信的时候,这种模式相当有效。
我希望这对你有所帮助,你可以在我的 GitHub 仓库 angularjs-pubsub 下找到例子的代码。