【问题标题】:How to create a pub/sub pattern using AngularJS如何使用 AngularJS 创建发布/订阅模式
【发布时间】:2015-11-29 12:03:47
【问题描述】:

我正在使用 Angular (1.4.7) 编写 SPA,为了降低复杂性,我一直在尝试将持久性逻辑抽象到工厂/存储库。

这没什么特别的,似乎工作正常。

我想实现的一个功能是当用户更新一些个人信息时,“父”作用域能够更新。

查看示例https://jsfiddle.net/h1r9zjt4/

我查看了实现此功能的各种方法,我看到的几种方法是:

  • 使用 $rootScope 共享公共对象
    我一直在尝试避免使用范围,而只使用 controllerAs 语法。这似乎是在控制器和视图之间保持严格/稳健分离的建议解决方案。
  • 使用 $scope.$parent 访问所需的属性
    出于类似原因,这会将我的视图实现与我的控制器实现结合起来。
  • 使用 $on/$emit 在控制器之间进行通信
    除了听起来像是最终的维护噩梦外,这本质上意味着控制器了解其他控制器。不理想。

我的理想方案是发布/订阅方案。

我的用户更新他们的详细信息将由存储库处理,该存储库又向该存储库的所有订阅者发送命令或履行承诺。

这是标准的角度模式吗?如果没有,合适的替代方案是什么?

【问题讨论】:

  • 别忘了看看使用 RxJs,这是 Angular 2 的发展方向。我更喜欢 Flux,尽管它们不是相互排斥的,因为它可以让你更好地控制数据如何通过你的应用程序。
  • 这其实很有趣!我之前在各种应用程序中使用过 Rx,所以它是我熟悉的概念。如果你有时间,你会考虑贡献一个 RxJs 的例子吗?

标签: javascript angularjs publish-subscribe


【解决方案1】:

虽然它主要与 React 世界相关,但您正在寻找的是 Flux。它甚至以flux-angular 的形式移植到Angular。

Flux 强制执行一种模式,让您了解如何看待流经应用程序的数据。

允许您发布和订阅更改的共享模型称为商店。但是,您不会以传统的 pubsub 方式与他们交谈。

商店

商店负责处理一些数据并处理您触发的任何操作。例如,专柜的商店可能看起来像这样:

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;
      }
    }
  };
});

然后将你的 store 注入到控制器或指令中以监听变化。

将其视为发布/订阅架构的订阅部分。

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

动作

Flux 难题的另一部分是调度动作。这是 pub/sub 架构的发布部分,非常重要。

您无需像使用根作用域的事件发射器那样发射事件,而是分派可序列化的操作,而 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');
      };
    }
  };
});

这段代码甚至不知道商店!它只知道这些是点击这些按钮时应该调度的动作。

一旦调度了这些动作,Flux 就会使用动作的名称来调用商店中的适当函数。这些商店更新他们的数据,如有必要,他们会发出更改,通知订阅者,以便他们也可以更新他们的数据。

在两个指令之间共享一个计数器似乎有很多代码,但这是一个非常强大的想法,从长远来看,它将使您的应用程序架构保持简洁。

结论

Flux 是一个非常酷的架构。以下是为什么它可能比您提到的其他解决方案更适合您的原因。

关注点分离

Flux 允许您将所有状态管理代码移出到称为 stores 的松散耦合模块中。这样,您的任何控制器都不必知道任何其他控制器。

可序列化操作

如果您确保只调度可序列化的操作,那么您可以跟踪应用程序中触发的每个操作,这意味着可以通过简单地重新播放相同的操作来重新创建任何状态。

要了解这有多酷,请查看 this video 关于使用名为 Redux 的 Flux 实现的时间旅行。

单向数据

当数据只流向一个方向时,更容易推理您的程序。当您使用 Flux 时,没有理由与您的孩子以外的任何组件进行通信。

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

在更传统的 pub/sub 架构中,如果指令 C 想要与指令 A 和 D 进行通信,则必须维护一个复杂的纠缠层次结构,每次让一个指令或控制器知道时,管理起来就会变得越来越困难关于另一个。

不清楚数据的流动方式,因为指令可以相互通信,无论它们在哪里。

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

使用 Flux,您的指令仅与它们的子指令和存储通信 - 数据在您的应用程序中沿一个方向流动,从而更容易计算出一个值是如何到达某处的,或者为什么调用一个函数。

【讨论】:

  • 丹骑着他那匹英勇的骏马进来了!这正是我想要做的。我来自微服务世界,写冗长的高度耦合类的想法让我成为一只悲伤的熊猫。非常感谢您的帮助。
【解决方案2】:

使用$on/$emit 绝对是一个可行的选择,但您需要注意不要过度使用它,因为它可能会导致非常复杂的应用程序难以调试和跟踪。

另一种方式(我认为在大多数情况下更好)是使用服务。由于服务本质上是单例的,因此服务上的数据将在所有应用程序之间共享。

因此,您可以在父控制器和子控制器中都注入一个服务,一旦对子控制器进行更改,它将更新服务上的属性,并且父级将 $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
        });
});    

【讨论】:

  • 我忘了提到这是我调查过的事情。这不一定是一个糟糕的架构,我认为它有它的位置,但理想情况下我正在寻找可以通过大型应用程序扩展的东西。这是对我的问题的正确答案,因此您会得到 +1 :)
【解决方案3】:

我使用postaljs 并将$bus 注入到$scopes,如博客An angular.js event bus with postal.js 所示

请注意,博客中的代码 sn-p 会抛出 无法获取未定义的属性“长度”,我将其修复为:

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;
    }
]);

订阅控制器:

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.
            }
        });

发布控制器:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-14
    • 1970-01-01
    相关资源
    最近更新 更多