如何使用 AngularJS 创建 pub/sub 模式
How to create a pub/sub pattern using AngularJS
我正在使用 angular (1.4.7) 编写 SPA,为了降低复杂性,我一直在尝试将持久性逻辑抽象为 factory/repository。
这没什么特别的,似乎工作正常。
我想实现的一个功能是 "parent" 作用域能够在用户更新一些个人信息时进行更新。
看例子https://jsfiddle.net/h1r9zjt4/
我查看了实现此功能的各种方法,我看到的几种方法是:
- 使用 $rootScope 共享公共对象
我一直试图避免使用作用域而只使用 controllerAs
语法。这似乎是在控制器和视图之间保持 strict/robust 分离的建议解决方案。
- 使用 $scope.$parent 访问所需的 属性
出于类似的原因,这将我的视图实现与我的控制器实现相结合。
- 控制器之间使用$on/$emit进行通信
这听起来像是最终的维护噩梦,这本质上意味着控制器了解其他控制器。不理想。
我理想的场景是 pub/sub 场景。
我的用户更新他们的详细信息将由存储库处理,存储库反过来向该存储库的所有订阅者发送命令或履行承诺。
这是标准的 angular 模式吗?如果不是,合适的替代方案是什么?
虽然它主要与 React 世界相关,但您正在寻找的是 Flux. It's even been ported to Angular in the form of flux-angular。
Flux 对您如何看待流经应用程序的数据实施了一种模式。
允许您发布和订阅更改的共享模型称为 stores。但是,您不会以传统的 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;
}
}
};
});
然后将您的商店注入控制器或指令以侦听更改。
将其视为 pub/sub 架构的订阅部分。
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 的松耦合模块中。这样 none 您的控制器将永远需要知道任何其他控制器。
序列化操作
如果您确保只发送可以序列化的动作,那么您可以跟踪在您的应用程序中触发的每个动作,这意味着可以通过简单地 re-playing再次执行相同的操作。
要了解这有多酷,请查看this video关于使用名为 Redux 的 Flux 实现的时间旅行。
单向数据
当数据仅沿一个方向流动时,更容易推理您的程序。当您使用 Flux 时,没有理由与您的 children.
以外的任何组件进行通信
A---+
/ \ |
/ v |
B D |
| / |
| / |
C-----+
在更传统的 pub/sub 体系结构中,如果指令 C 想要与指令 A 和 D 进行通信,则它必须维护一个复杂的纠缠层次结构,每次让一个层次结构变得越来越难以管理指令或控制器知道另一个。
不清楚数据流向哪个方向,因为指令可以相互通信,无论它们位于何处。
A <------------+
/ \ |
v v |
B D <----- [store]
| ^
v |
C --> [action] --+
使用 Flux,您的指令仅与它们的 children 和存储进行通信 — 数据在您的应用程序中单向流动,从而更容易弄清楚某个值是如何到达某处的,或者为什么一个函数是打电话。
使用$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
});
});
我用postaljs and inject a $bus to $scopes as show the blog An angular.js event bus with postal.js
请注意,博客中的代码片段抛出一个 Unable to get 属性 'length' of undefined,我将其修复为:
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);
我正在使用 angular (1.4.7) 编写 SPA,为了降低复杂性,我一直在尝试将持久性逻辑抽象为 factory/repository。
这没什么特别的,似乎工作正常。
我想实现的一个功能是 "parent" 作用域能够在用户更新一些个人信息时进行更新。
看例子https://jsfiddle.net/h1r9zjt4/
我查看了实现此功能的各种方法,我看到的几种方法是:
- 使用 $rootScope 共享公共对象
我一直试图避免使用作用域而只使用controllerAs
语法。这似乎是在控制器和视图之间保持 strict/robust 分离的建议解决方案。 - 使用 $scope.$parent 访问所需的 属性
出于类似的原因,这将我的视图实现与我的控制器实现相结合。 - 控制器之间使用$on/$emit进行通信
这听起来像是最终的维护噩梦,这本质上意味着控制器了解其他控制器。不理想。
我理想的场景是 pub/sub 场景。
我的用户更新他们的详细信息将由存储库处理,存储库反过来向该存储库的所有订阅者发送命令或履行承诺。
这是标准的 angular 模式吗?如果不是,合适的替代方案是什么?
虽然它主要与 React 世界相关,但您正在寻找的是 Flux. It's even been ported to Angular in the form of flux-angular。
Flux 对您如何看待流经应用程序的数据实施了一种模式。
允许您发布和订阅更改的共享模型称为 stores。但是,您不会以传统的 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;
}
}
};
});
然后将您的商店注入控制器或指令以侦听更改。
将其视为 pub/sub 架构的订阅部分。
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 的松耦合模块中。这样 none 您的控制器将永远需要知道任何其他控制器。
序列化操作
如果您确保只发送可以序列化的动作,那么您可以跟踪在您的应用程序中触发的每个动作,这意味着可以通过简单地 re-playing再次执行相同的操作。
要了解这有多酷,请查看this video关于使用名为 Redux 的 Flux 实现的时间旅行。
单向数据
当数据仅沿一个方向流动时,更容易推理您的程序。当您使用 Flux 时,没有理由与您的 children.
以外的任何组件进行通信 A---+
/ \ |
/ v |
B D |
| / |
| / |
C-----+
在更传统的 pub/sub 体系结构中,如果指令 C 想要与指令 A 和 D 进行通信,则它必须维护一个复杂的纠缠层次结构,每次让一个层次结构变得越来越难以管理指令或控制器知道另一个。
不清楚数据流向哪个方向,因为指令可以相互通信,无论它们位于何处。
A <------------+
/ \ |
v v |
B D <----- [store]
| ^
v |
C --> [action] --+
使用 Flux,您的指令仅与它们的 children 和存储进行通信 — 数据在您的应用程序中单向流动,从而更容易弄清楚某个值是如何到达某处的,或者为什么一个函数是打电话。
使用$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
});
});
我用postaljs and inject a $bus to $scopes as show the blog An angular.js event bus with postal.js
请注意,博客中的代码片段抛出一个 Unable to get 属性 'length' of undefined,我将其修复为:
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);