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


162

我正在寻找有关如何绑定到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>

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

$ 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,但是在我创建的其他示例中,第一次广播未使用$ broadcast / $捕获了控制器,但是在控制器中触发了广播的其他呼叫。如果您知道解决$ rootscope。$ broadcast问题的方法,请提供答案。

但是要重申一下我前面提到的内容,我想知道有关如何绑定到服务属性的最佳实践。


更新资料

最初在2013年4月提出并回答了这个问题。2014年5月,Gil Birman提供了一个新答案,我将其更改为正确答案。由于吉尔·伯曼(Gil Birman)的回答几乎没有票数,因此我担心的是,阅读此问题的人会无视他的回答,而赞成其他票数更多的回答。在您决定最佳答案之前,我强烈建议您使用Gil Birman的答案。


我认为乔什·大卫·米勒的答案比吉尔·伯曼的答案更好。使用$ watch,$ watchGroup和$ watchCollection甚至可以使其变得更好。在大中型应用程序上,关注点的分离非常重要。
乔纳森

@bardev我认为这两个答案都没有用,新开发人员会完全理解他们。
Zalaboza '16

您询问的问题是关于引用对象的本地JavaScript变量行为的,我在下面添加了一些解释
Zalaboza

Answers:


100

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

  • 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的,promise.then(..)'s和$scope.complexClickAction = function() {..}

作为实现数据解耦和视图封装的另一种方法,将视图与模型分离的唯一有意义的地方是使用伪指令。但即使在那儿,也不要$watchcontrolleror link函数中标量值。那将不会节省时间,也不会使代码更具可维护性和可读性。它甚至都不会使测试变得容易,因为角度的强大测试通常会测试最终的DOM。相反,在指令中,要求您以对象形式提供数据API,并且只喜欢使用$watch由创建的er 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>

更新:我终于回到这个问题,补充说,我认为这两种方法都不是“错误的”。最初我写的是乔什·戴维·米勒(Josh David Miller)的回答是错误的,但回想起来,他的观点是完全正确的,尤其是他关于关注点分离的观点。

除了关注点分离(但与切向相关)之外,我没有考虑防御复制的另一个原因。这个问题主要涉及直接从服务读取数据。但是,如果您团队中的开发人员决定控制器需要在视图显示之前以某种简单的方式转换数据呢?(控制器是否应该完全转换数据是另一个讨论。)如果她不首先复制该对象,则可能会不经意地导致另一个使用相同数据的视图组件的回归。

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

  1. 关注点分离:通过道具的组件消耗所有它的数据,并拥有小到无的全球单身人士(如角服务)的依赖,并一无所知发生什么上面视图层次结构中它。

  2. 可变性:所有道具都是不可变的,从而消除了不知不觉间数据突变的风险。

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


1
我对AngularJS的研究越深入,对我的了解越多。我相信AngularJS控制器应该尽可能的简单和轻巧。通过在控制器中添加$ watches,它会使逻辑复杂化。仅通过引用服务中的值,它就简单得多,并且似乎更像是AngularJS方式。–
Mike Barlow-BarDev 2014年

3
AngularJS的“十诫”。它是声明性的,是有原因的。
pilau 2014年

乔什·大卫·米勒(Josh David Miller)的回答不是“错误”。有时间和地方放所有东西。
2014年

我认为您是对的,@ FireCoding。我正在计划更新答案。
吉尔·伯曼

@GilBirman很好的答案。您介意编写该指令的示例吗?为了说明您所说的话,“将视图与模型解耦的唯一真正的地方是使用指令。[...]相反,在指令中,要求您的数据API为对象形式,而只使用$ ng-bind创建的观察者。”
klode 2015年

78

在我看来,$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语句)中包含任何“逻辑”时,遵循相同的推理。

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


3
你能详细说明一下吗?您是否更喜欢$ watches,因为视图与服务的耦合较少?即,{{lastUpdated}}{{timerData.lastUpdated}}
Mark Rajcok

2
@BarDev(Timer.data在$ watch中使用)Timer必须在$ scope上定义,因为传递给$ watch的字符串表达式是根据作用域计算的。这是一个显示如何进行这项工作的插件这里记录 objectEquality参数-第3个参数-但解释得不太好。
Mark Rajcok


11
@Kyrm在某些情况下可能是正确的,但是在处理性能时,我们需要寻找“具有临床意义的”性能增强,而不仅仅是统计上有意义的广义增强。如果现有应用程序中存在性能问题,则应予以解决。否则,这只是过早优化的一种情况,这会导致难以阅读,易出错的代码没有遵循最佳实践,也没有表现出任何好处。
2014年

1
假设观察者使用一个函数来调用getter,那么是。它运作良好。我还是服务返回集合中实例的支持者,而es5的getter和setter功能相当不错。
Josh David Miller

19

晚会晚了,但对于未来的Google员工-不要使用提供的答案。

JavaScript具有一种通过引用传递对象的机制,而它仅传递值“数字,字符串等”的浅表副本。

在上面的示例中,为什么不将服务绑定到作用域中,而不是绑定服务的属性

$scope.hello = HelloService;

这种简单的方法将使angular能够进行双向绑定以及您需要的所有神奇的东西。不要用观察者或不必要的标记来入侵您的控制器。

并且,如果您担心视图会意外覆盖服务属性,请使用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


可以肯定的是我上面用所描述的$scope.model = {timerData: Timer.data};,只是将其附加到模型上而不是直接在Scope上。
Scott Silvi

1
AFAIK,js对象参考适用于Service中的所有内容。编码如下:$ scope.controllerVar = ServiceVar,在$ scope.controllerVar中执行的所有操作也都在ServiceVar中完成。
王凯

@KaiWang同意,除非您决定使用DefineAttribute,否则您可以使您的服务具有setter函数,以防止控制器意外更改您的服务数据。
Zalaboza '16

12

我认为这个问题有情境因素。

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

此外,角度性能基于两件事。第一个是页面上有多少个绑定。第二个问题是getter函数有多昂贵。Misko 在这里谈论这个

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

需要注意的是,您不应将任何属性直接挂在$ scope之外。该$scope变量不是你的模型。它引用了您的模型。

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

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

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


仔细考虑该建议,不是JavaScript专家的人可以尝试使用字符串属性来实现。在那种情况下,javascript不会引用该对象,而是原始复制字符串。(这很糟糕,因为如果它发生更改,它不会被更新)
Valerio

7
我不是用我的“ caveat”覆盖了这一点,但您应该始终使用点(这意味着不要将其挂在$ scope上,而应该挂在$ scope.model上)。如果您有$ scope.model.someStringProperty,并且在视图中引用了model.someStringProperty,它将被更新,因为内部观察者将位于对象上,而不是道具上。
Scott Silvi

6

在上面的示例的基础上,我认为我将以透明的方式将控制器变量绑定到服务变量。

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

在生产中,我们实际上是使用this绑定来更新服务上的ID,然后该服务异步获取数据并更新其服务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);
    };

}

这是一个很棒的解决方案,谢谢您,这对我
很有帮助

2

我认为这是绑定服务本身而不是绑定属性的更好方法。

原因如下:

<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> 

你可以在这个塞子


1

我宁愿尽量少看我的手表。我的理由是根据我的经验,有人可能会在理论上进行争论。
使用观察程序的问题在于,您可以使用作用域上的任何属性来调用您喜欢的任何组件或服务中的任何方法。
在一个现实世界的项目中,很快您将以不可追踪(最好说很难追踪)的方法链被调用,并且值被更改,这特别使入职过程变得悲惨。


0

绑定任何发送服务的数据不是一个好主意(体系结构),但是如果您需要它,我建议您使用2种方法来实现

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

2)您可以使用angularjs事件。无论何时,您都可以发送信号(来自$ rootScope),并在任意位置捕获它。您甚至可以发送有关该eventName的数据。

也许这可以帮助您。如果您需要更多示例,请点击这里

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


-1

关于什么

scope = _.extend(scope, ParentScope);

ParentScope是哪里的注入服务?


-2

最优雅的解决方案...

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 本质上具有最低的可行耦合。而且,如果您不太担心这个事实,那么让我们避免一起处理同一项目。

希望这可以帮助...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.