详解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技巧收藏
Apr 07 Javascript
javascript 获取select下拉列表值的代码
Sep 07 Javascript
基于jquery的9行js轻松实现tab控件示例
Oct 12 Javascript
javascript 动态创建表格的2种方法总结
Mar 04 Javascript
JavaScript中setFullYear()方法的使用详解
Jun 11 Javascript
javascript结合Flexbox简单实现滑动拼图游戏
Feb 18 Javascript
Bootstrap每天必学之日期控制
Mar 07 Javascript
js select实现省市区联动选择
Apr 17 Javascript
微信小程序自定义导航隐藏和显示功能
Jun 13 Javascript
基于DOM节点删除之empty和remove的区别(详解)
Sep 11 Javascript
详解用Node.js写一个简单的命令行工具
Mar 01 Javascript
微信公众号平台接口开发 获取微信服务器IP地址方法解析
Aug 14 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
星际争霸 Starcraft 游戏介绍
2020/03/14 星际争霸
php将文本文件转换csv输出的方法
2014/12/31 PHP
YII视图整合kindeditor扩展的方法
2016/07/13 PHP
thinkPHP5.0框架安装教程
2017/03/25 PHP
经典的带阴影的可拖动的浮动层
2006/06/26 Javascript
javascript 写类方式之七
2009/07/05 Javascript
JavaScript的单例模式 (singleton in Javascript)
2010/06/11 Javascript
浅谈Javascript中substr和substring的区别
2015/09/30 Javascript
jQuery点击输入框显示验证码图片
2016/05/19 Javascript
JavaScript蒙板(model)功能的简单实现代码
2016/08/04 Javascript
原生JS下拉加载插件分享
2016/12/26 Javascript
Vuex利用state保存新闻数据实例
2017/06/28 Javascript
Angular动态绑定样式及改变UI框架样式的方法小结
2018/09/03 Javascript
对vue下点击事件传参和不传参的区别详解
2018/09/15 Javascript
vue自定义全局共用函数详解
2018/09/18 Javascript
JavaScript 反射和属性赋值实例解析
2019/10/28 Javascript
ES2020 新特性(种草)
2020/01/12 Javascript
python基础教程之popen函数操作其它程序的输入和输出示例
2014/02/10 Python
在Python的Flask框架下使用sqlalchemy库的简单教程
2015/04/09 Python
在Python的web框架中编写创建日志的程序的教程
2015/04/30 Python
基于python(urlparse)模板的使用方法总结
2017/10/13 Python
python3+PyQt5+Qt Designer实现扩展对话框
2018/04/20 Python
简单了解python数组的基本操作
2019/11/26 Python
Python迭代器Iterable判断方法解析
2020/03/16 Python
英国版MAC彩妆品牌:Illamasqua
2018/04/18 全球购物
实习护理工作自我评价
2013/09/25 职场文书
生物制药毕业生自荐信
2013/10/16 职场文书
八一建军节活动方案
2014/02/10 职场文书
篮球赛口号
2014/06/18 职场文书
服务员态度差检讨书
2014/10/28 职场文书
师范生见习报告范文
2014/11/03 职场文书
学校拾金不昧表扬信
2015/01/16 职场文书
解除租赁合同协议书
2016/03/21 职场文书
2019最新劳动仲裁申请书!
2019/07/08 职场文书
详解TS数字分隔符和更严格的类属性检查
2021/05/06 Javascript
关于vue-router-link选择样式设置
2022/04/30 Vue.js