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

Mik*_*Dev 162 data-binding angularjs angularjs-service angularjs-controller

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

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

下面我有两个如何绑定到服务中的属性的示例; 他们都工作.第一个示例使用基本绑定,第二个示例使用$ scope.$ watch绑定到服务属性

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

这些示例的前提是服务应每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>
Run Code Online (Sandbox Code Playgroud)

我解决绑定到服务属性的另一种方法是在控制器中使用$ 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>
Run Code Online (Sandbox Code Playgroud)

我知道我可以在服务器中使用$ rootscope.$ $和在控制器中使用$ root.$ on,但在我创建的其他使用$ broadcast/$的示例中,第一次广播不会被控制器,但在控制器中触发广播的其他呼叫.如果你知道一种方法来解决$ rootscope.$ broadcast问题,请提供答案.

但是为了重申我之前提到的内容,我想知道如何绑定到服务属性的最佳实践.


更新

这个问题最初是在2013年4月提出并回答的.2014年5月,Gil Birman提供了一个新答案,我将其更改为正确答案.由于吉尔·伯尔曼回答的票数很少,我担心的是,阅读这个问题的人会忽视他的回答而更多地支持其他答案.在你决定什么是最佳答案之前,我强烈推荐Gil Birman的答案.

Gil*_*man 99

考虑第二种方法的一些优点和缺点:

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

  • +1控制器可以很方便地作为标记的API,这样,如果数据模型的结构发生变化,您可以(理论上)更新控制器的API映射,而不会触及html部分.

  • -1然而,理论并不总是练习,我经常发现自己不得不修改标记控制器逻辑更改时呼吁,反正.因此,编写API的额外努力否定了它的优势.

  • -1此外,这种方法不是很干.

  • -1如果要将数据绑定到ng-model代码,则需要更少DRY,因为必须重新打包$scope.scalar_values控制器以进行新的REST调用.

  • -0.1创造额外观察者的性能微乎其微.此外,如果数据属性附加到不需要在特定控制器中监视的模型,则会为深度观察者创建额外的开销.

  • -1如果多个控制器需要相同的数据模型怎么办?这意味着您可以使用多个API来更新每个模型更改.

$scope.timerData = Timer.data;现在开始听起来很有诱惑力......让我们深入探讨最后一点......我们谈论的是什么样的模特变化?后端(服务器)上的模型?或者只创建并存在于前端的模型?在任何一种情况下,基本上数据映射API都属于前端服务层(角度工厂或服务).(请注意,您的第一个示例 - 我的偏好 - 在服务层中没有这样的API ,这很好,因为它很简单,它不需要它.)

总之,一切都不必解耦.至于将标记完全与数据模型分离,缺点超过了优点.


一般来说,控制器不应该散落$scope = injectable.data.scalar.相反,他们应该撒上$scope = injectable.data's promise.then(..)'和$scope.complexClickAction = function() {..}'s'

作为实现数据解耦和视图封装的替代方法,将视图与模型分离真正有意义的唯一方法是使用指令.但即使在那里,不要$watch在标量值controllerlink功能.这不会节省时间或使代码更易于维护或读取.它甚至不会使测试更容易,因为角度的强大测试通常会测试生成的DOM.相反,在一个指令中需要以对象形式请求您的数据API,并且仅使用$watch由其创建的ers ng-bind.


示例 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>
Run Code Online (Sandbox Code Playgroud)

更新:我终于回到这个问题补充说,我不认为这两种方法都是"错误的".最初我写过Josh David Miller的答案是不正确的,但回想起来,他的观点完全有效,尤其是关于分离关注点的观点.

抛开问题(但与切向相关),我没有考虑防御性复制的另一个原因.这个问题主要涉及直接从服务中读取数据.但是,如果团队中的开发人员决定控制器需要在视图显示之前以某种微不足道的方式转换数据,该怎么办?(控制器是否应该完全转换数据是另一种讨论.)如果她没有首先复制对象,她可能会在另一个视图组件中无意中导致消耗相同数据的回归.

这个问题真正突出的是典型的角度应用程序(实际上是任何JavaScript应用程序)的架构缺点:关注点的紧密耦合和对象的可变性.我最近迷恋于使用React 不可变数据结构构建应用程序.这样做可以很好地解决以下两个问题:

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

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

Angular 2.0现在有望从React大量借款以实现上述两点.

  • AngularJS的"十诫".这是声明的原因. (3认同)
  • 我使用 AngularJS 的次数越多,我对 AngularJS 的了解也就越多。我相信 AngularJS 控制器应该尽可能简单和精简。通过在控制器中添加 $watches 会使逻辑变得复杂。通过仅引用服务中的值,它会简单得多,并且似乎更像 AngularJS 的方式。– (2认同)

Jos*_*ler 78

从我的角度来看,$watch这将是最佳实践方式.

你可以实际简化你的例子:

function TimerCtrl1($scope, Timer) {
  $scope.$watch( function () { return Timer.data; }, function (data) {
    $scope.lastUpdated = data.lastUpdated;
    $scope.calls = data.calls;
  }, true);
}
Run Code Online (Sandbox Code Playgroud)

这就是你所需要的.

由于属性同时更新,您只需要一个手表.此外,由于它们来自一个相当小的物体,我改变它只是为了观察Timer.data物业.传递的最后一个参数$watch告诉它检查深度相等而不是仅仅确保引用是相同的.


为了提供一点上下文,我希望这种方法将服务值直接放在范围上的原因是为了确保关注点的正确分离.您的视图不需要知道有关您的服务的任何信息才能运行.控制器的工作是将所有东西粘在一起; 它的工作是从您的服务中获取数据并以任何必要的方式处理它们,然后为您的视图提供所需的任何细节.但我不认为它的工作就是将服务直接传递给视图.否则,控制器甚至在那里做什么?AngularJS开发人员在选择不在模板中包含任何"逻辑"(例如if语句)时遵循相同的推理.

公平地说,这里可能存在多种观点,我期待其他答案.

  • @Kyrm在某些情况下这可能是正确的,但在处理性能时,我们需要寻找"临床上显着"的性能增强,而不仅仅是统计上显着的通用性能增强.如果现有应用程序中存在性能问题,则应解决该问题.否则,这只是一个过早优化的情况,这会导致难以阅读,容易出错的代码,这些代码不遵循最佳实践,并且没有明显的优势. (11认同)
  • 你能详细说一下吗?你更喜欢$ watch,因为视图与服务的联系较少?即,`{{lastUpdated}}`vs.`{{timerData.lastUpdated}}` (3认同)
  • @BarDev,要在$ watch中使用`Timer.data`,必须在$ scope上定义`Timer`,因为传递给$ watch的字符串表达式是根据作用域来计算的.这是一个[plunker](http://plnkr.co/edit/EZTX0N),展示了如何使这项工作.objectEquality参数记录在这里[这里](http://docs.angularjs.org/api/ng.$ro​​otScope.Scope#$watch) - 第三个参数 - 但没有真正解释得太好. (2认同)
  • 表现明智,`$ watch`是非常低效的.请参阅http://stackoverflow.com/a/17558885/932632和http://stackoverflow.com/questions/12576798/angularjs-how-to-watch-service-variables/23010646#23010646上的答案 (2认同)

Zal*_*oza 19

晚会,但对于未来的Google员工 - 请勿使用提供的答案.

JavaScript有一种通过引用传递对象的机制,而它只传递值为"数字,字符串等"的浅表副本.

在上面的示例中,为什么不将服务公开给范围,而不是绑定服务的属性

$scope.hello = HelloService;
Run Code Online (Sandbox Code Playgroud)

这种简单的方法将使角度能够进行双向绑定和所需的所有神奇事物.不要使用观察者或不需要的标记来破坏您的控制器.

如果您担心您的视图会意外覆盖您的服务属性,请使用defineProperty它来使其可读,可枚举,可配置或定义getter和setter.通过使您的服务更加稳固,您可以获得很多控制权.

最后提示:如果你花时间在控制器上工作而不是服务,那么你做错了:(.

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

 function TimerCtrl1($scope, Timer) {
   $scope.timer = Timer;
 }
///Inside view
{{ timer.time_updated }}
{{ timer.other_property }}
etc...
Run Code Online (Sandbox Code Playgroud)

编辑:

如上所述,您可以使用控制服务属性的行为 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
});
Run Code Online (Sandbox Code Playgroud)

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

$scope.hello = HelloService;
$scope.hello.propertyWithSetter = 'NEW VALUE';
Run Code Online (Sandbox Code Playgroud)

我们的服务将改变价值,propertyWithSetter并以某种方式将新值发布到数据库!

或者我们可以采取任何我们想要的方法.

请参阅MDN文档defineProperty.

  • 最佳答案,如果您正在阅读本文,请尝试这一个 (2认同)

Sco*_*lvi 12

我认为这个问题有一个背景因素.

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

此外,角度的性能基于两件事.第一个是页面上有多少个绑定.第二个是吸气功能有多贵.Misko 在这里谈到这一点

如果您需要对服务数据执行特定于实例的逻辑(而不是在服务本身中应用的数据按摩),并且结果会影响暴露给视图的数据模型,那么我会说$ watcher是合适的,因为只要功能不是非常昂贵.在功能昂贵的情况下,我建议将结果缓存在本地(到控制器)变量中,在$ watcher函数之外执行复杂操作,然后将范围绑定到结果.

作为警告,您不应该直接在$ scope范围内挂起任何属性.该$scope变量不是你的模型.它引用了您的模型.

在我看来,"最佳实践"只是简单地将服务中的信息辐射到视图中:

function TimerCtrl1($scope, Timer) {
  $scope.model = {timerData: Timer.data};
};
Run Code Online (Sandbox Code Playgroud)

然后你的观点将包含{{model.timerData.lastupdated}}.

  • 我没有用我的"警告"来说明你应该总是使用点(意思是不要把它挂在$ scope上,而是关闭$ scope.model).如果您有$ scope.model.someStringProperty,并且在视图中引用了model.someStringProperty,它将会更新,因为内部观察者将在对象上,而不是prop. (7认同)

Tom*_*Tom 6

基于上面的例子,我想我会把控制器变量透明地绑定到服务变量.

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

在生产中,我们实际上使用此绑定来更新服务上的id,然后异步地获取数据并更新其服务变量.进一步绑定意味着当服务更新时控制器会自动更新.

可以在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>
Run Code Online (Sandbox Code Playgroud)

服务/控制器:

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

}
Run Code Online (Sandbox Code Playgroud)