AngularJS: 绑定到服务属性的正确方法

我正在寻找如何绑定到 AngularJS 服务属性的最佳实践。

我已经通过多个示例了解了如何绑定到使用 AngularJS 创建的服务中的属性。

下面是两个关于如何绑定到服务中的属性的示例; 它们都可以工作。第一个示例使用基本绑定,第二个示例使用 $scope。$watch 绑定到服务属性

在绑定到服务中的属性时,这两个示例中的任何一个是首选的,还是有另一个我不知道的推荐选项?

这些示例的前提是,服务应该每5秒钟更新其属性“ lastUpdated”和“ call”。更新服务属性后,视图应该反映这些更改。这两个例子都很成功; 我想知道是否有更好的方法。

基本绑定

下面的代码可以在这里查看和运行: http://plnkr.co/edit/d3c16z

<html>
<body ng-app="ServiceNotification" >


<div ng-controller="TimerCtrl1" style="border-style:dotted">
TimerCtrl1 <br/>
Last Updated: {{timerData.lastUpdated}}<br/>
Last Updated: {{timerData.calls}}<br/>
</div>


<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
<script type="text/javascript">
var app = angular.module("ServiceNotification", []);


function TimerCtrl1($scope, Timer) {
$scope.timerData = Timer.data;
};


app.factory("Timer", function ($timeout) {
var data = { lastUpdated: new Date(), calls: 0 };


var updateTimer = function () {
data.lastUpdated = new Date();
data.calls += 1;
console.log("updateTimer: " + data.lastUpdated);


$timeout(updateTimer, 5000);
};
updateTimer();


return {
data: data
};
});
</script>
</body>
</html>

解决绑定到服务属性的另一种方法是在控制器中使用 $scope. $watch。

$范围。 $观察

下面的代码可以在这里查看和运行: http://plnkr.co/edit/dSBlC9

<html>
<body ng-app="ServiceNotification">
<div style="border-style:dotted" ng-controller="TimerCtrl1">
TimerCtrl1<br/>
Last Updated: {{lastUpdated}}<br/>
Last Updated: {{calls}}<br/>
</div>


<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
<script type="text/javascript">
var app = angular.module("ServiceNotification", []);


function TimerCtrl1($scope, Timer) {
$scope.$watch(function () { return Timer.data.lastUpdated; },
function (value) {
console.log("In $watch - lastUpdated:" + value);
$scope.lastUpdated = value;
}
);


$scope.$watch(function () { return Timer.data.calls; },
function (value) {
console.log("In $watch - calls:" + value);
$scope.calls = value;
}
);
};


app.factory("Timer", function ($timeout) {
var data = { lastUpdated: new Date(), calls: 0 };


var updateTimer = function () {
data.lastUpdated = new Date();
data.calls += 1;
console.log("updateTimer: " + data.lastUpdated);


$timeout(updateTimer, 5000);
};
updateTimer();


return {
data: data
};
});
</script>
</body>
</html>

我知道我可以使用 $rootscope。在服务和 $root 中广播。但是在我创建的其他示例中,控制器不会捕获在第一次广播中使用 $Broadcasting/$的情况,但是会在控制器中触发其他广播的调用。如果您知道一种解决 $rootscope 的方法。$广播问题,请提供答案。

但是,为了重述我前面提到的内容,我想了解如何绑定到服务属性的最佳实践。


更新

这个问题最初是在2013年4月提出并得到回答的。2014年5月,吉尔 · 伯曼提供了一个新的答案,我把它改成了正确答案。由于吉尔 · 伯曼的回答只有很少的赞成票,我担心的是,读到这个问题的人会忽略他的回答,而赞成其他更多的投票的回答。在你决定什么是最好的答案之前,我强烈推荐吉尔 · 伯曼的答案。

115224 次浏览

在我看来,$watch是最好的练习方法。

你实际上可以简化一下你的例子:

function TimerCtrl1($scope, Timer) {
$scope.$watch( function () { return Timer.data; }, function (data) {
$scope.lastUpdated = data.lastUpdated;
$scope.calls = data.calls;
}, true);
}

这就够了。

因为属性是同时更新的,所以您只需要一个手表。另外,因为它们来自一个相当小的对象,所以我将其更改为只观察 Timer.data属性。传递给 $watch的最后一个参数告诉它检查深度相等性,而不仅仅是确保引用是相同的。


为了提供一些上下文,我之所以选择这种方法而不是直接将服务价值放在范围上,是为了确保适当的关注点分离。您的视图不应该需要了解关于服务的任何信息才能进行操作。控制器的工作是将所有东西粘合在一起; 它的工作是从您的服务获取数据,并以任何必要的方式处理它们,然后向您的视图提供它所需要的任何细节。但是我不认为它的工作是将服务直接传递给视野。否则,控制器在那里干什么?AngularJS 开发人员在选择不在模板中包含任何“逻辑”(例如 if语句)时遵循了同样的原因。

公平地说,这里可能有多种观点,我期待着其他答案。

我觉得这个问题有上下文关系。

如果你只是简单地从一个服务中提取数据并将这些信息放射到它的视图中,我认为直接绑定到服务属性就可以了。我不想编写 很多样板代码来简单地将服务属性映射到要在我的视图中使用的模型属性。

此外,角度的表现基于两点。第一个问题是一个页面上有多少绑定。第二个问题是 getter 函数的开销有多大。米斯科谈到这个 给你

如果您需要对服务数据执行实例特定的逻辑(而不是应用在服务本身中的数据按摩) ,并且其结果会影响暴露给视图的数据模型,那么我会说 $watch 是合适的,只要该功能不是非常昂贵。对于代价高昂的函数,我建议将结果缓存在一个局部(到控制器)变量中,在 $watch 函数之外执行复杂的操作,然后将作用域绑定到该变量的结果。

作为一个警告,您不应该将 任何属性直接挂起在 $scope 之外。$scope变量不是你的模型。它引用了你的模型。

在我看来,简单地将信息从服务向下传递到视图的“最佳实践”是:

function TimerCtrl1($scope, Timer) {
$scope.model = {timerData: Timer.data};
};

然后您的视图将包含 \{\{model.timerData.lastupdated}}

考虑一些 第二种方法的利弊:

  • 0 \{\{lastUpdated}}而不是 \{\{timerData.lastUpdated}}\{\{timerData.lastUpdated}}也可以是 \{\{timer.lastUpdated}},我可能认为 \{\{timer.lastUpdated}}更易读(但是我们不要争论... ... 我给这一点一个中性评分,所以你自己决定)

  • 控制器作为标记的一种 API 可能很方便,如果数据模型的结构发生了某种变化,你可以(理论上)更新控制器的 API 映射,而不需要触摸 html 部分。

  • 然而,理论并不总是实践,当需要更改时,我通常发现自己不得不修改标记 还有控制器逻辑,即 无论如何。因此编写 API 的额外工作抵消了它的优势。

  • 此外,这种方法不是很干燥。

  • 如果你想把数据绑定到 ng-model,你的代码会变得更少干燥,因为你必须在控制器中重新打包 $scope.scalar_values来进行一个新的 REST 调用。

  • 有一个小的性能命中创建额外的观察者(s)。此外,如果数据属性附加到模型,不需要在特定的控制器中监视,它们将为深度监视器创建额外的开销。

  • 如果多个控制器需要相同的数据模型怎么办?这意味着您有多个 API 的更新与每个模型的变化。

$scope.timerData = Timer.data;现在开始听起来非常诱人... ... 让我们更深入地探讨一下最后一点... ... 我们刚才讨论的是什么样的模型变化?后端(服务器)上的模型?或者一个模型是创建和生活只在前端?在这两种情况下,本质上 数据映射 API属于 前端服务层前端服务层(一个角工厂或服务)。(请注意,您的第一个示例(我的偏好)在 服务层中没有这样的 API,这很好,因为它足够简单,不需要它。)

总而言之,一切都不必解耦。至于将标记与数据模型完全分离,缺点大于优点。


一般来说,控制器 不应该被 $scope = injectable.data.scalar所覆盖,相反,它们应该被 $scope = injectable.datapromise.then(..)$scope.complexClickAction = function() {..}所覆盖

作为实现数据解耦和视图封装的替代方法,将视图与模型分离确实有意义的唯一位置是 带着指令。但即使在那里,也不要在 controllerlink函数中使用 $watch标量值。这既不能节省时间,也不能使代码更易于维护和阅读。自从 角度的鲁棒性测试通常会测试结果 DOM以来,它甚至不能使测试变得更容易。相反,在一个指令要求您的 数据 API的对象形式,并赞成只使用由 ng-bind创建的 $watchers。


例子 Http://plnkr.co/edit/mveu1gkrtn4bqa3h9yio

<body ng-app="ServiceNotification">
<div style="border-style:dotted" ng-controller="TimerCtrl1">
TimerCtrl1<br/>
Bad:<br/>
Last Updated: \{\{lastUpdated}}<br/>
Last Updated: \{\{calls}}<br/>
Good:<br/>
Last Updated: \{\{data.lastUpdated}}<br/>
Last Updated: \{\{data.calls}}<br/>
</div>


<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
<script type="text/javascript">
var app = angular.module("ServiceNotification", []);


function TimerCtrl1($scope, Timer) {
$scope.data = Timer.data;
$scope.lastUpdated = Timer.data.lastUpdated;
$scope.calls = Timer.data.calls;
};


app.factory("Timer", function ($timeout) {
var data = { lastUpdated: new Date(), calls: 0 };


var updateTimer = function () {
data.lastUpdated = new Date();
data.calls += 1;
console.log("updateTimer: " + data.lastUpdated);


$timeout(updateTimer, 500);
};
updateTimer();


return {
data: data
};
});
</script>
</body>

更新 : 我最后回到这个问题,并补充说,我不认为这两种方法都是“错误的”。最初我写道乔希•大卫•米勒的回答是错误的,但现在回想起来,他的观点是完全正确的,尤其是他关于关注点分离的观点。

撇开关注点分离不谈(但是略有关联) ,还有一个我没有考虑到的原因。这个问题主要涉及从服务直接读取数据。但是,如果团队中的开发人员认为控制器需要在视图显示数据之前以某种简单的方式转换数据,该怎么办呢?(控制器是否应该完全转换数据是另一个问题。)如果她不首先复制对象,她可能会不知不觉地在另一个视图组件中导致使用相同数据的回归。

这个问题真正突出的是典型的角度应用程序(以及任何 JavaScript 应用程序)的体系结构缺陷: 关注点的紧耦合和对象可变性。我最近迷上了使用 React 还有不可变数据结构设计应用程序。这样做很好地解决了以下两个问题:

  1. 关注点分离 : 一个组件通过道具消耗它所有的数据,几乎不依赖于全局单件(比如 Angular 服务) ,并且对视图层次结构中发生的事情一无所知。

  2. 可变性 : 所有道具都是不可变的,这消除了不知情的数据变异的风险。

角度2.0现在正在轨道上大量借用反应,以实现上述两点。

我认为这是一个更好的方式 绑定到服务本身,而不是它的属性。

原因如下:

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.7/angular.min.js"></script>
<body ng-app="BindToService">


<div ng-controller="BindToServiceCtrl as ctrl">
ArrService.arrOne: <span ng-repeat="v in ArrService.arrOne">\{\{v}}</span>
<br />
ArrService.arrTwo: <span ng-repeat="v in ArrService.arrTwo">\{\{v}}</span>
<br />
<br />
<!-- This is empty since $scope.arrOne never changes -->
arrOne: <span ng-repeat="v in arrOne">\{\{v}}</span>
<br />
<!-- This is not empty since $scope.arrTwo === ArrService.arrTwo -->
<!-- Both of them point the memory space modified by the `push` function below -->
arrTwo: <span ng-repeat="v in arrTwo">\{\{v}}</span>
</div>


<script type="text/javascript">
var app = angular.module("BindToService", []);


app.controller("BindToServiceCtrl", function ($scope, ArrService) {
$scope.ArrService = ArrService;
$scope.arrOne = ArrService.arrOne;
$scope.arrTwo = ArrService.arrTwo;
});


app.service("ArrService", function ($interval) {
var that = this,
i = 0;
this.arrOne = [];
that.arrTwo = [];


$interval(function () {
// This will change arrOne (the pointer).
// However, $scope.arrOne is still same as the original arrOne.
that.arrOne = that.arrOne.concat([i]);


// This line changes the memory block pointed by arrTwo.
// And arrTwo (the pointer) itself never changes.
that.arrTwo.push(i);
i += 1;
}, 1000);


});
</script>
</body>

你可以在 这个傻瓜播放。

最优雅的解决方案。

app.service('svc', function(){ this.attr = []; return this; });
app.controller('ctrl', function($scope, svc){
$scope.attr = svc.attr || [];
$scope.$watch('attr', function(neo, old){ /* if necessary */ });
});
app.run(function($rootScope, svc){
$rootScope.svc = svc;
$rootScope.$watch('svc', function(neo, old){ /* change the world */ });
});

此外,我还编写了 EDA (事件驱动架构) ,因此我倾向于采用以下方法[过于简化的版本] :

var Service = function Service($rootScope) {
var $scope = $rootScope.$new(this);
$scope.that = [];
$scope.$watch('that', thatObserver, true);
function thatObserver(what) {
$scope.$broadcast('that:changed', what);
}
};

然后,我将一个侦听器放在我的控制器中所需的通道上,并以这种方式保持我的本地作用域最新。

总而言之,没有太多的“最佳实践”——更确切地说,它的 差不多吧偏好——只要您保持事物的坚固性并使用弱耦合。我提倡使用后一种代码的原因是,EDA 的 最低联轴器最低联轴器本质上是可行的。如果你不太关心这个事实,让我们避免在同一个项目一起工作。

希望这个能帮上忙。

基于上面的例子,我认为应该引入一种透明地将控制器变量绑定到服务变量的方法。

在下面的示例中,Controller $scope.count变量的更改将自动反映在 Service count变量中。

在生产环境中,我们实际上使用这个绑定来更新服务的 id,然后异步获取数据并更新其 service var。进一步绑定,这意味着当服务自身更新时,控制器会自动更新。

下面的代码可以在 http://jsfiddle.net/xuUHS/163/上看到

观看内容:

<div ng-controller="ServiceCtrl">
<p> This is my countService variable : \{\{count}}</p>
<input type="number" ng-model="count">
<p> This is my updated after click variable : \{\{countS}}</p>


<button ng-click="clickC()" >Controller ++ </button>
<button ng-click="chkC()" >Check Controller Count</button>
</br>


<button ng-click="clickS()" >Service ++ </button>
<button ng-click="chkS()" >Check Service Count</button>
</div>

服务/总监:

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


app.service('testService', function(){
var count = 10;


function incrementCount() {
count++;
return count;
};


function getCount() { return count; }


return {
get count() { return count },
set count(val) {
count = val;
},
getCount: getCount,
incrementCount: incrementCount
}


});


function ServiceCtrl($scope, testService)
{


Object.defineProperty($scope, 'count', {
get: function() { return testService.count; },
set: function(val) { testService.count = val; },
});


$scope.clickC = function () {
$scope.count++;
};
$scope.chkC = function () {
alert($scope.count);
};


$scope.clickS = function () {
++testService.count;
};
$scope.chkS = function () {
alert(testService.count);
};


}

绑定任何发送服务的数据都不是一个好主意(架构) ,但如果你还需要它,我建议你两种方法

1)你可以得到不在你服务内部的数据。您可以在控制器/指令中获取数据,并且在任何地方绑定数据都没有问题

2)你可以使用 angularjs 事件。无论何时,您都可以发送一个信号(来自 $rootScope)并在任何需要的地方捕获它。您甚至可以在那个 event Name 上发送数据。

也许这个能帮到你。 如果你需要更多的例子,这里是链接

Http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html

关于什么

scope = _.extend(scope, ParentScope);

ParentScope 是注入服务的位置?

参加派对迟到,但对于未来的谷歌员工——不要使用提供的答案。

JavaScript 有一种通过引用传递对象的机制,而它只传递“数字、字符串等”值的浅拷贝。

在上面的例子中,我们为什么不将服务公开到作用域中呢?代替了绑定服务的属性

$scope.hello = HelloService;

这个简单的方法将使角能够做双向绑定和所有的神奇的东西,你需要。不要使用观察者或不需要的标记来破解控制器。

如果您担心视图会意外地覆盖服务属性,那么使用 defineProperty使其具有可读性、可枚举性、可配置性,或者定义 getter 和 setter。通过使您的服务更加可靠,您可以获得很多控制权。

最后一个小贴士: 如果你花在控制器上的时间比花在服务上的时间多,那么你就错了。

在您提供的特定演示代码中,我建议您这样做:

 function TimerCtrl1($scope, Timer) {
$scope.timer = Timer;
}
///Inside view
\{\{ timer.time_updated }}
\{\{ timer.other_property }}
etc...

编辑:

如前所述,您可以使用 defineProperty控制服务属性的行为

例如:

// Lets expose a property named "propertyWithSetter" on our service
// and hook a setter function that automatically saves new value to db !
Object.defineProperty(self, 'propertyWithSetter', {
get: function() { return self.data.variable; },
set: function(newValue) {
self.data.variable = newValue;
// let's update the database too to reflect changes in data-model !
self.updateDatabaseWithNewData(data);
},
enumerable: true,
configurable: true
});

现在在我们的控制器中,如果我们这样做

$scope.hello = HelloService;
$scope.hello.propertyWithSetter = 'NEW VALUE';

我们的服务将改变 propertyWithSetter的价值,也张贴新的价值,以某种方式数据库!

或者我们可以采取任何方法。

参考 MDN 文档了解 defineProperty

我宁愿让我的观察者尽可能少。我的理由是基于我的经验,有人可能会从理论上来说。
使用观察者的问题在于,您可以使用作用域上的任何属性来调用您喜欢的任何组件或服务中的任何方法。
在现实世界的项目中,很快就会有一个 无法追踪(更确切地说是难以跟踪)链被调用,并且值被更改,这特别使得入职过程变得悲惨。