简体   繁体   English

如何使用AngularJS创建发布/订阅模式

[英]How to create a pub/sub pattern using AngularJS

I am writing a SPA using angular (1.4.7) and to keep the complexity down I'd been attempting to abstract the persistence logic to a factory/repository. 我正在使用angular(1.4.7)编写SPA,并且为了降低复杂度,我一直试图将持久性逻辑抽象到工厂/存储库。

This is nothing special and seems to work fine. 这没什么特别的,似乎可以正常工作。

One feature I'd like to implement is the ability for a "parent" scope to update when the user updates some personal information. 我要实现的一项功能是当用户更新某些个人信息时,“父”范围可以更新的功能。

See the example https://jsfiddle.net/h1r9zjt4/ 参见示例https://jsfiddle.net/h1r9zjt4/

I've taken a look at various ways of implementing this and a few ways I've seen are: 我已经研究了实现此目标的各种方法,并且看到了以下几种方法:

  • Use $rootScope to share common objects 使用$ rootScope共享公共对象
    I've been attempting to steer away from using scopes and to only use the controllerAs syntax. 我一直试图避免使用范围,而只使用controllerAs语法。 This seems to be the advised solution to keep a strict/robust separation between controller and view. 这似乎是建议的解决方案,以在控制器和视图之间保持严格/牢固的分隔。
  • Use $scope.$parent to access the required property 使用$ scope。$ parent访问所需的属性
    For similar reasons, this couples my view implementation to my controller implementation. 出于类似的原因,这将我的视图实现与控制器实现耦合在一起。
  • Use $on/$emit to communicate between controllers 使用$ on / $ emit在控制器之间进行通信
    As well as sounding like an eventual maintenance nightmare, this inherently means that controllers know about other controllers. 听起来像是最终的维护噩梦,这从本质上讲意味着控制器知道其他控制器。 Not ideal. 不理想。

My ideal scenario would be to have a pub/sub scenario. 我理想的方案是发布/预订方案。

My user updating their details will be handled by the repository which in-turn sends a command or fulfils a promise to all subscribers of that repository. 我的用户更新其详细信息将由存储库处理,该存储库依次向该存储库的所有订户发送命令或兑现承诺。

Is this a standard angular pattern? 这是标准的角度图案吗? If not, what would a suitable alternative be? 如果没有,合适的替代方案是什么?

Although it's mostly associated with the React world, what you are looking for is Flux . 尽管它主要与React世界相关,但您正在寻找的是Flux It's even been ported to Angular in the form of flux-angular . 它甚至以flux-angular的形式移植到Angular。

Flux enforces a pattern for how you think about data flowing through your application. Flux为您如何考虑流经应用程序的数据提供了一种模式。

The shared models that allow you to publish and subscribe to changes with are called stores . 允许您发布和订阅更改的共享模型称为store However, you don't speak to them in the conventional pubsub way. 但是,您不会以传统的pubsub方式与他们交谈。

Stores 商店

A store is responsible for looking after some data and handling any actions that you trigger. 商店负责照顾一些数据并处理您触发的任何操作。 For instance a store for a counter might look something like this: 例如,柜台的商店可能看起来像这样:

app.store('CounterStore', function() {
  return {
    count: 0,
    increment: function() {
      this.count = this.count + 1;
      this.emitChange();
    },
    decrement: function() {
      this.count = this.count - 1;
      this.emitChange();
    },
    exports: {
      getCount: function() {
        return this.count;
      }
    }
  };
});

Then inject your store into a controller or directive to listen for changes. 然后将您的商店注入到控制器或指令中以侦听更改。

Think of this as the subscription part of a pub/sub architecture. 将此视为发布/订阅体系结构的订阅部分。

app.directive('Counter', function() {
  return {
    template: '<div ng-bind='count'></div>',
    controller: function($scope, CounterStore) {
      $scope.listenTo(CounterStore, function() {
        $scope.count = CounterStore.getCount();
      });
    }
  };
});

Actions 操作

The other piece in the Flux puzzle is dispatching actions. 助焊剂难题中的另一部分是调度动作。 This is the publishing part of a pub/sub architecture, on steroids. 这是类固醇上pub / sub体系结构的发布部分。

Rather than emitting events like you could do with the root scope's event emitter, you dispatch serializable actions and Flux does the rest for you. 而不是像使用根作用域的事件发射器那样发出事件,而是调度可序列化的操作,而Flux则为您完成其余的工作。

Let's define a final directive to control the counter in the previous directive, using Flux. 让我们使用Flux定义最终指令来控制上一个指令中的计数器。

app.directive('CounterControls', function() {
  return {
    template: '<button ng-click="inc()">+</button>' + 
              '<button ng-click="dec()">-</button>',
    controller: function($scope, flux) {
      $scope.inc = function() {
        flux.dispatch('increment')
      };

      $scope.dec = function() {
        flux.dispatch('decrement');
      };
    }
  };
});

This code doesn't even know about the store! 这段代码甚至都不知道商店! It just knows that these are the actions that should be dispatched when these buttons are clicked. 它只是知道这些是单击这些按钮时应分派的动作。

Once these actions have been dispatched, Flux uses the name of the action to call the appropriate functions within the stores. 一旦分派了这些动作,Flux将使用动作的名称来调用商店中的适当功能。 These stores update their data and if necessary, they emit a change, notifying the subscribers so that they can update their data too. 这些存储更新数据,并在必要时发出更改,通知订户,以便他们也可以更新数据。

It might seem like a lot of code for sharing a counter between two directives, but it's a very powerful idea and in the long term will keep the architecture of your application clean and concise. 在两个指令之间共享计数器似乎是很多代码,但这是一个非常强大的想法,从长远来看,它将使您的应用程序体系结构简洁明了。

Conclusion 结论

Flux is a pretty cool architecture. Flux是一个非常酷的体系结构。 Here's a run down of why it might suit you better than the other solutions you mentioned. 这是为什么它比您提到的其他解决方案更适合您的原因。

Separation of Concerns 关注点分离

Flux allows you to move all state management code out into loosely coupled modules called stores . Flux允许您将所有状态管理代码移到称为存储的松散耦合模块中。 This way none of your controllers will ever have to know about any other controllers. 这样,您的任何控制器都无需知道其他任何控制器。

Serializable Actions 可序列化的动作

If you make sure that you only dispatch actions that can be serialized, then you can keep a track of every action that's fired in your application, meaning it's possible to recreate any state, by simplying re-playing the same actions again. 如果确保只调度可以序列化的动作,则可以跟踪应用程序中触发的每个动作,这意味着可以通过简单地重新播放相同的动作来重新创建任何状态。

To get some idea of just how cool this can be, check out this video about time travel with a Flux implementation called Redux. 要想知道它有多酷,可以观看有关视频旅行的视频该视频使用名为Redux的Flux实现。

One Way Data 单向数据

It's easier to reason about your program when data only flows in one direction. 当数据仅在一个方向上流动时,更容易对程序进行推理。 When you use Flux, there's no reason to ever communicate with any components other than your children. 使用Flux时,除了孩子之外,没有任何理由与其他组件进行通信。

  A---+
 / \  |
/   v |
B   D |
|  /  |
| /   |
C-----+

In a more traditional pub/sub architecture, if directive C wanted to communicate with directive A and D it would have to maintain a complex entangled hierarchy, which gets more and more difficult to manage each time you let one directive or controller know about another. 在更传统的发布/订阅体系结构中,如果指令C要与指令A和D进行通信,则它必须维护一个复杂的纠缠层次结构,每当您让一个指令或控制器知道另一个指令或控制器时,层次结构将变得越来越难管理。

It's not clear which way the data is flowing because directives can communicate with eachother, regardless of where they are. 目前尚不清楚数据以哪种方式流动,因为指令无论在哪里都可以相互通信。

  A <------------+
 / \             |
v   v            |
B   D  <----- [store]
|                ^
v                |
C --> [action] --+

With Flux, your directives only communicate with their children and with stores — data flows in one direction round your application, making it much easier to work out how a value got somewhere, or why a function was called. 使用Flux,您的指令仅与他们的子代和商店进行通信-数据在您的应用程序中沿一个方向流动,这使确定值如何到达某个位置或为何调用函数变得容易得多。

Using $on/$emit is definitely a viable option, but you need to be careful not to overuse it, since it might lead to a very complex application that is hard to debug and track. 使用$on/$emit绝对是一个可行的选择,但是您需要注意不要过度使用它,因为它可能会导致难以调试和跟踪的非常复杂的应用程序。

Another way (which i think is better in most cases) is to use services. 另一种方式(我认为在大多数情况下更好)是使用服务。 Since services are singletons by nature, data on a service will be shared across all application. 由于服务本质上是单例的,因此服务上的数据将在所有应用程序之间共享。

So you can have a service that is injected in both the parent and child controller and once a change is made on the child, it will update a property on the service and the parent will $watch that attribute and act upon change: 因此,您可以在父级和子级控制器中注入一个服务,并且在子级上进行更改后,它将更新该服务上的属性,并且父级将$watch该属性并根据更改执行操作:

var app = angular.module('myApp', []);

app.factory('sharedService', function() {
    return {
        sharedAttr: ''
    }

});    

app.controller('childCtrl', function($scope, sharedService) {
    $scope.onAttrChange = function() {
        sharedService.sharedAttr = 'Value Changed';
    }
});

app.controller('parentCtrl', function($scope, sharedService) {
    $scope.$watch(function() {
            return sharedService.sharedAttr;
        },
        function(newVal, oldVal) {
            //do something with newValue
        });
});    

I use postaljs and inject a $bus to $scopes as show the blog An angular.js event bus with postal.js 我使用postaljs并将$ bus注入到$ scopes中,如显示博客postal.js的angular.js事件总线

Note that code snippet at blog throws an Unable to get property 'length' of undefined , I fixed it as: 请注意,博客中的代码段引发了无法获取undefined的属性“ length” ,我将其修复为:

app.config(function($provide) {
$provide.decorator('$rootScope', [
    '$delegate',
    function($delegate) {
        Object.defineProperty($delegate.constructor.prototype,
            '$bus', {
                get: function() {
                    var self = this;

                    return {
                        subscribe: function() {
                            var sub = postal.subscribe.apply(postal, arguments);

                            self.$on('$destroy',
                                function() {
                                    sub.unsubscribe();
                                });
                        },
                        //Fix to avoid postaljs v 2.0.4:513 Unable to get property 'length' of undefined
                        channel: function() { return postal.channel.apply(postal,arguments);  },
                        publish: function() { postal.publish.apply(postal,arguments); }
                    };
                },
                enumerable: false
            });

        return $delegate;
    }
]);

Subscribe controller: 订阅控制器:

var subscription = $scope.$bus.subscribe({
            channel: "organizations",
            topic: "item.changed",
            callback: function(data, envelope) {
                // `data` is the data published by the publisher.
                // `envelope` is a wrapper around the data & contains
                // metadata about the message like the channel, topic,
                // timestamp and any other data which might have been
                // added by the sender.
            }
        });

Publish controller: 发布控制器:

channel = $scope.$bus.channel('organizations');
channel.publish("item.changed",data);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM