详解AngularJS的通信机制


Posted in Javascript onJune 18, 2015

现在几乎满世界的人都在问! 外面有人么? 这里是 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 下找到例子的代码。

Javascript 相关文章推荐
js 实现图片预加载(js操作 Image对象属性complete ,事件onload 异步加载图片)
Mar 25 Javascript
JS target与currentTarget区别说明
Aug 28 Javascript
js去空格技巧分别去字符串前后、左右空格
Oct 21 Javascript
jquery实现增加删除行的方法
Feb 03 Javascript
js父页面中使用子页面的方法
Jan 09 Javascript
jQuery插件EasyUI获取当前Tab中iframe窗体对象的方法
Aug 05 Javascript
Bootstrap Navbar Component实现响应式导航
Oct 08 Javascript
jQuery实现单击按钮遮罩弹出对话框效果(1)
Feb 20 Javascript
jQuery实现可拖动进度条实例代码
Jun 21 jQuery
原生JS+Canvas实现五子棋游戏
May 28 Javascript
基于Cookie常用操作以及属性介绍
Sep 07 Javascript
JQuery获取元素尺寸、位置及页面滚动事件应用示例
May 14 jQuery
javascript背景时钟实现方法
Jun 18 #Javascript
移动Web中图片自适应的两种JavaScript解决方法
Jun 18 #Javascript
javascript随机显示背景图片的方法
Jun 18 #Javascript
利用JavaScript的AngularJS库制作电子名片的方法
Jun 18 #Javascript
javascript实现根据时间段显示问候语的方法
Jun 18 #Javascript
javascript显示中文日期的方法
Jun 18 #Javascript
使用AngularJS制作一个简单的RSS阅读器的教程
Jun 18 #Javascript
You might like
PHP实现网上点歌(二)
2006/10/09 PHP
手把手教你使用DedeCms的采集的图文教程
2007/03/11 PHP
php下使用以下代码连接并测试
2008/04/09 PHP
PHP 中关于ord($str)&amp;gt;0x80的详细说明
2012/09/23 PHP
PHP中的reflection反射机制测试例子
2014/08/05 PHP
PHP date函数常用时间处理方法
2015/05/11 PHP
浅谈PHP中pack、unpack的详细用法
2018/03/12 PHP
Ext.FormPanel 提交和 Ext.Ajax.request 异步提交函数的区别
2009/11/12 Javascript
jquery判断checkbox(复选框)是否被选中的代码
2010/10/20 Javascript
JavaScript异步调用定时方法并停止该方法实现代码
2012/03/16 Javascript
js+css实现的简单易用兼容好的分页
2013/12/30 Javascript
js日期对象兼容性的处理方法
2014/01/28 Javascript
JS+CSS实现Li列表隔行换色效果的方法
2015/02/16 Javascript
基于jQuery Bar Indicator 插件实现进度条展示效果
2015/09/30 Javascript
JavaScript事件 &quot;事件对象&quot;的注意要点
2016/01/14 Javascript
深入理解jQuery.data() 的实现方式
2016/11/30 Javascript
jQuery实现获取h1-h6标题元素值的方法
2017/03/06 Javascript
利用原生js实现html5小游戏之打砖块(附源码)
2018/01/03 Javascript
ES6 迭代器(Iterator)和 for.of循环使用方法学习(总结)
2018/02/08 Javascript
JQuery常用简单动画操作方法回顾与总结
2019/12/07 jQuery
使用JavaScript实现网页秒表功能(含开始、暂停、继续、重置功能)
2020/06/05 Javascript
[02:02]特效爆炸!DOTA2珍宝之瓶待你开启
2018/08/21 DOTA
Python引用模块和查找模块路径
2016/03/17 Python
Python使用Selenium模块实现模拟浏览器抓取淘宝商品美食信息功能示例
2018/07/18 Python
利用Pandas和Numpy按时间戳将数据以Groupby方式分组
2019/07/22 Python
如何将你的应用迁移到Python3的三个步骤
2019/12/22 Python
DataFrame 数据合并实现(merge,join,concat)
2020/06/14 Python
Python sublime安装及配置过程详解
2020/06/29 Python
英国家电直销:Appliances Direct
2016/09/22 全球购物
Gap英国官网:Gap UK
2018/07/18 全球购物
《美丽的黄昏》教学反思
2014/02/28 职场文书
环境卫生倡议书
2014/08/29 职场文书
标准大学生职业生涯规划书写作指南
2014/09/18 职场文书
地道战观后感500字
2015/06/04 职场文书
答谢酒会主持词
2015/07/02 职场文书
springboot+rabbitmq实现智能家居实例详解
2022/07/23 Java/Android