AngularJS:绑定到服务属性的正确方法
我正在寻找如何绑定到AngularJS中的服务属性的最佳做法。
我已经通过多个示例来了解如何绑定到使用AngularJS创build的服务中的属性。
下面我有两个如何绑定到服务中的属性的例子; 他们都工作。 第一个示例使用基本绑定,第二个示例使用$ scope。$ watch绑定到服务属性
当绑定到服务中的属性时,这两个示例中的哪一个都是首选的,或者还有其他的select,我不知道会被推荐?
这些例子的前提是,服务应该每隔5秒更新其属性“lastUpdated”和“calls”。 一旦服务属性更新,视图应该反映这些更改。 这两个例子都成功了 我想知道是否有更好的方法来做到这一点。
基本绑定
下面的代码可以在这里查看和运行: 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。$ broadcast,在控制器上使用$ root。$ on,但是在其他例子中,我已经创build了在第一个广播中使用$ broadcast / $的方法。控制器,但广播的其他呼叫在控制器中触发。 如果您知道解决$ rootscope。$ broadcast问题的方法,请提供答案。
但要重申我之前提到的,我想知道如何绑定到服务属性的最佳做法。
更新
这个问题最初是在2013年4月问及答案的。2014年5月,吉尔·伯尔曼提供了一个新的答案,我将其改为正确的答案。 由于吉尔·伯尔曼的答案很less有人赞成,我担心的是,读这个问题的人会无视他的回答,而赞成其他答案,而且还有更多的选票。 在你决定什么是最好的答案之前,我强烈推荐Gil Birman的答案。
考虑第二种方法的一些利弊 :
-
0
{{lastUpdated}}
而不是{{timerData.lastUpdated}}
,这可能很容易{{timer.lastUpdated}}
,我可能会争辩是更可读(但我们不要争辩…我给这个指出一个中立的评级,所以你自己决定) -
+1控制器可以很方便地作为标记的一种API,这样,如果数据模型的结构发生变化,您可以(理论上)更新控制器的API映射,而不必接触html部分。
-
-1然而,理论并不总是实践, 无论如何 ,我经常需要修改标记和控制器逻辑。 所以编写API的额外努力否定了它的优势。
-
-1此外,这种方法不是非常干燥。
-
-1如果你想把数据绑定到
ng-model
你的代码变得更加干燥,因为你必须重新打包控制器中的$scope.scalar_values
来创build一个新的REST调用。 -
-0.1创build额外观察者的performance很小。 另外,如果数据属性附加到不需要在特定控制器中观察的模型,则会为深度观察者创build额外的开销。
-
-1如果多个控制器需要相同的数据模型? 这意味着你有多个API来更新每个模型的变化。
$scope.timerData = Timer.data;
现在已经开始听起来很有吸引力了…让我们深入一点,深入探讨最后一点…我们在谈论什么样的模型变化? 后端(服务器)上的模型? 还是仅仅在前端创造和生存的模型? 在任何一种情况下, 数据映射API本质上属于前端服务层 (angular色工厂或服务)。 (请注意,您的第一个示例 – 我的首选项 – 在服务层中没有这样的API,这很好,因为它足够简单,不需要它。)
总之 ,一切都不必解耦。 就标记完全脱离数据模型而言,缺点大于优点。
一般来说,控制器不应该包含$scope = injectable.data.scalar
的内容。 相反,他们应该洒$scope = injectable.data
$scope.complexClickAction = function() {..}
的, $scope.complexClickAction = function() {..}
promise.then(..)
和$scope.complexClickAction = function() {..}
的
作为实现数据解耦和视图封装的一种替代方法, 将视图从模型中分离出来的唯一方法是使用指令 。 但即使在那里,也不要$watch
在controller
或link
函数中$watch
标量值。 这不会节省时间或使代码更加可维护,也不可读。 它甚至不会使testing更简单,因为angular度的强健testing通常会testing生成的DOM 。 相反,在一个指令要求你的数据API的对象forms,并支持只使用ng-bind
创build的$watch
。
示例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>
更新 :我终于回到这个问题补充说,我不认为这两种方法都是“错误的”。 原来我写的是乔希·大卫·米勒的答案是不正确的,但回想起来,他的观点是完全有效的,尤其是他关于分离关注点的观点。
抛开关注点(但切线相关),还有一个我没有考虑的防御性拷贝的原因。 这个问题主要涉及直接从服务中读取数据。 但是如果你的团队中的开发人员决定在视图显示之前控制器需要以一些微不足道的方式转换数据呢? (控制器是否应该转换数据是另一个讨论。)如果她不先制作对象的副本,她可能会不知不觉地导致消耗相同数据的另一个视图组件中的回归。
这个问题真正强调的是典型的angular度应用程序(以及任何JavaScript应用程序)的架构缺点:关注的紧密耦合和对象可变性。 我最近迷恋于使用React 和不可变的数据结构构build应用程序。 这样做可以很好地解决以下两个问题:
-
问题分离 :一个组件通过道具消耗所有的数据,并且几乎不依赖于全局单例(比如Angular服务),并且对视图层次结构中发生的事情一无所知。
-
可变性 :所有道具都是不变的,消除了不知情的数据变异的风险。
现在Angular 2.0正在从React中大量借用来实现上述两点。
从我的angular度来看, $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开发者select不在模板中包含任何“逻辑”(例如if
语句)时,遵循相同的推理。
公平地说,这里可能有多种观点,我期待着其他答案。
我认为这个问题有一个上下文组成部分。
如果您只是从服务中提取数据并将这些信息放到视图中,我认为直接绑定到服务属性就好了。 我不想写很多样板代码来简单地将服务属性映射到在我的视图中使用的模型属性。
此外,angular度的performance基于两件事情。 首先是页面上有多less个绑定。 其次是getter函数是多么昂贵。 Misko 在这里谈论这个
如果您需要在服务数据上执行特定于实例的逻辑(而不是在服务本身中应用的数据按摩),并且这会影响暴露给视图的数据模型,那么我会说$ $观察者是合适的,因为只要function不是非常昂贵。 在昂贵的函数的情况下,我会build议将结果caching到本地(到控制器)variables,在$ watcher函数之外执行复杂的操作,然后将范围绑定到结果。
作为一个警告,你不应该挂在你的$范围直接任何属性。 $scope
variables不是你的模型。 它引用了你的模型。
在我看来,简单地将服务信息从服务中传递给观察者的“最佳实践”
function TimerCtrl1($scope, Timer) { $scope.model = {timerData: Timer.data}; };
然后你的视图将包含{{model.timerData.lastupdated}}
。
晚会,但未来的谷歌不要使用提供的答案。
js有一个通过引用传递对象的独特机制,而它只传递一个值“数字,string等”的浅拷贝。
在上面的例子中,而不是绑定服务的属性。 为什么我们不把服务暴露给范围?
$scope.hello = HelloService;
这个简单的方法将使angular能够做2路绑定和所有你需要的神奇的东西。 不要用监视器或未知的标记来破解你的控制器。
如果你担心你的意见不小心覆盖了你的服务属性。 精简使用DefineAttribute()使其可读,可枚举,可configuration,设置getter和setter。 你把它命名。 您可以通过使您的服务更加稳固而获得很多控制权。
最后提示:如果你花时间在你的控制器上工作,更多的是你的服务。 那么你做错了:(。
在你提供的特定演示代码中,我会build议你这样做
function TimerCtrl1($scope, Timer) { $scope.timer = Timer; } ///Inside view {{ timer.time_updated }} {{ timer.other_property }} etc...
3岁以上的post太多,但如果有人需要更多的信息让我知道
编辑:
正如我上面提到的,你可以使用defineProperty来控制你的服务属性的行为
例:
// Lets expose a property named "propertyWithSetter" on our service // and hook a setter function that automaticly save new value to db ! Object.defineProperty(self, 'propertyWithSetter', { get: function() { return self.data.variable; }, set: function(newValue) { self.data.variable = newValue; //lets 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
的价值,并以某种方式向数据库发布新的价值!
或者我们可以采取我们想要的任何方法。 请参考https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
感谢upvotes 🙂
基于上面的例子,我想我会抛出一种将控制器variables透明地绑定到服务variables的方式。
在下面的示例中,对Controller $scope.count
variables的更改将自动反映在Service count
variables中。
在生产中,我们实际上是使用这个绑定来更新一个服务上的id,然后asynchronous获取数据并更新其服务variables。 进一步的绑定意味着当服务更新自身时,控制器会自动更新。
下面的代码可以在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); }; }
我认为这是一个更好的方式绑定服务本身,而不是它的属性。
原因如下:
<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>
你可以在这个笨蛋上玩 。
我宁愿让我的观察者尽可能less。 我的理由是基于我的经验,有人可能会在理论上争论。
使用观察者的问题是您可以使用任何范围的属性来调用您喜欢的任何组件或服务中的任何方法。
在现实世界的项目中,很快你会得到一个不可追踪的 ( 更难说的)被调用的方法链,价值被改变,这特别使得入职过程变得悲惨。
绑定任何数据,发送服务不是一个好主意(架构),但如果你需要它,我build议你2种方法来做到这一点
1)你可以得到不在你服务的数据。你可以在你的控制器/指令中获得数据,你不会有任何问题绑定到任何地方
2)你可以使用angularjs事件。只要你想,你可以发送一个信号(从$ rootScope),并捕获它,无论你想要的。你甚至可以在该eventName发送数据。
也许这可以帮助你。 如果你需要更多的例子,这里是链接
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html
关于什么
scope = _.extend(scope, ParentScope);
ParentScope是一个注入服务?
最优雅的解决scheme…
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); } };
然后,我把一个监听器放在我的控制器上,并保持我的本地范围是最新的。
总之,只要你保持SOLID和弱耦合,“最佳实践”并不是什么“最佳实践”,而是它的主要偏好。 我提倡后者的原因是因为EDA具有最低的偶合性。 如果你不太关心这个事实,那么让我们避免一起在同一个项目上工作。
希望这可以帮助…