详解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 相关文章推荐
jQuery+ajax实现顶一下,踩一下效果
Jul 17 Javascript
浅析Javascript使用include/require
Nov 13 Javascript
JS+CSS 制作的超级简单的下拉菜单附图
Nov 22 Javascript
Javascript改变CSS样式(局部和全局)
Dec 18 Javascript
jQuery实现的多选框多级联动插件
May 02 Javascript
详解JavaScript跨域总结与解决办法
Oct 31 Javascript
js css3实现图片拖拽效果
Mar 04 Javascript
解决webpack打包速度慢的解决办法汇总
Jul 06 Javascript
HTML5+JS+JQuery+ECharts实现异步加载问题
Dec 16 jQuery
vue与vue-i18n结合实现后台数据的多语言切换方法
Mar 08 Javascript
使用layui日期控件laydate对开始和结束时间进行联动控制的方法
Sep 06 Javascript
JavaScript canvas仿代码流瀑布
Feb 10 Javascript
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 curl获取网页内容(IPV6下超时)的解决办法
2013/07/16 PHP
PHP中file_exists()判断中文文件名无效的解决方法
2014/11/12 PHP
php中$_POST与php://input的区别实例分析
2015/01/07 PHP
详解PHP数组赋值方法
2015/11/07 PHP
WordPress中设置Post Type自定义文章类型的实例教程
2016/05/10 PHP
php 下 html5 XHR2 + FormData + File API 上传文件操作实例分析
2020/02/28 PHP
jquery.ui.draggable中文文档
2009/11/24 Javascript
javascript 跨浏览器开发经验总结(五) js 事件
2010/05/19 Javascript
提交表单时执行func方法实现代码
2013/03/17 Javascript
JavaScript的漂亮的代码片段
2013/06/05 Javascript
CSS3,HTML5和jQuery搜索框集锦
2014/12/02 Javascript
JavaScript生成的动态下雨背景效果实现方法
2015/02/25 Javascript
CSS javascript 结合实现悬浮固定菜单效果
2015/08/23 Javascript
node-http-proxy修改响应结果实例代码
2016/06/06 Javascript
什么是JavaScript注入攻击?
2016/09/14 Javascript
浅谈jquery高级方法描述与应用
2016/10/04 Javascript
AngularJS实现ajax请求的方法
2016/11/22 Javascript
微信小程序 picker 组件详解及简单实例
2017/01/10 Javascript
ES6中新增的Object.assign()方法详解
2017/09/22 Javascript
Angular实现的简单查询天气预报功能示例
2017/12/27 Javascript
vue 多入口文件搭建 vue多页面搭建的实例讲解
2018/03/12 Javascript
详解关于表格合并span-method方法的补充(表格数据由后台动态返回)
2019/05/21 Javascript
JavaScript JSON数据处理全集(小结)
2019/08/15 Javascript
9种python web 程序的部署方式小结
2014/06/30 Python
Python中用于转换字母为小写的lower()方法使用简介
2015/05/19 Python
Python使用QQ邮箱发送Email的方法实例
2017/02/09 Python
美国最受欢迎的童装品牌之一:The Children’s Place
2016/07/23 全球购物
澳大利亚头发和美容产品购物网站:OZ Hair & Beauty
2020/03/27 全球购物
以思科路由器为例你写下单臂路由的配置命令
2013/08/03 面试题
校园歌咏比赛主持词
2014/03/18 职场文书
毕业留言寄语大全
2014/04/10 职场文书
2014企业年终工作总结
2014/12/23 职场文书
银行催款通知书
2015/04/17 职场文书
Python Pycharm虚拟下百度飞浆PaddleX安装报错问题及处理方法(亲测100%有效)
2021/05/24 Python
Spring 使用注解开发
2022/05/20 Java/Android
使用pd.merge表连接出现多余行的问题解决
2022/06/16 Python