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

时间:2013-04-04 00:14:01

标签: angularjs data-binding 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>

我解决绑定到服务属性的另一种方法是在控制器中使用$ 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的答案。

10 个答案:

答案 0 :(得分:99)

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

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

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

  • -1 但是,理论并不总是在练习,我通常会在调用变更时修改标记控制器逻辑, 反正。因此,编写API的额外努力否定了它的优势。

  • -1 此外,这种做法并非非常干燥。

  • -1 如果您想将数据绑定到ng-model,您的代码会变得更少干,因为您必须重新打包控制器中的$scope.scalar_values进行新的REST呼叫。

  • -0.1 创造额外观察者的性能微不足道。此外,如果数据属性附加到不需要在特定控制器中观看的模型,则会给深度观察者带来额外的开销。

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

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

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


通常的控制器不应该被$scope = injectable.data.scalar所占据。相反,它们应该加上$scope = injectable.datapromise.then(..)$scope.complexClickAction = function() {..}&#39>

作为实现数据解耦和视图封装的另一种方法,将视图与模型分离真正有意义的唯一地方是指令。但即使在那里,也不要$watchcontroller函数中的link标量值。这不会节省时间或使代码更易于维护和读取。它甚至不会使测试更容易,因为角度的强大测试通常会测试生成的DOM 。相反,在指令中以对象形式要求您的数据API ,并且仅使用$watch创建的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>

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

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

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

  1. 关注点分离:组件通过道具消耗所有数据,并且几乎不依赖于全局单例(例如Angular服务),并且知道<视图层次结构

  2. 上面上面发生了什么。

  3. 可变性:所有道具都是不可变的,可以消除不知情的数据突变的风险。

  4. Angular 2.0现在有望从React大量借款以达到上述两点。

答案 1 :(得分: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语句)时遵循相同的推理。

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

答案 2 :(得分:19)

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

JavaScript有一种通过引用传递对象的机制,而它只传递一个浅的副本,用于值&#34;数字,字符串等&#34;。

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

$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的值,并以某种方式将新值发布到数据库!

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

请参阅defineProperty的{​​{1}}。

答案 3 :(得分:12)

我认为这个问题有一个背景成分。

如果您只是从服务中提取数据&amp;将信息传递给它的视图,我认为直接绑定到服务属性就好了。我不想编写a lot of boilerplate code来简单地将服务属性映射到我在视图中使用的模型属性。

此外,角度表现基于两件事。第一个是页面上有多少个绑定。第二个是吸气功能有多贵。 Misko谈到了这个here

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

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

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

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

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

答案 4 :(得分:6)

Building on the examples above I thought I'd throw in a way of transparently binding a controller variable to a service variable.

In the example below changes to the Controller $scope.count variable will automatically be reflected in the Service count variable.

In production we're actually using the this binding to update an id on a service which then asynchronously fetches data and updates its service vars. Further binding that means that controllers automagically get updated when the service updates itself.

The code below can be seen working at http://jsfiddle.net/xuUHS/163/

View:

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

Service/Controller:

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

}

答案 5 :(得分: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> 

您可以在this plunker上播放。

答案 6 :(得分:1)

我宁愿让观察者尽可能少。我的理由是基于我的经验,有人可能在理论上辩解 使用观察者的问题在于,您可以使用范围内的任何属性来调用您喜欢的任何组件或服务中的任何方法。
在一个真实世界的项目中,很快你就会得到一个不可追踪的(更好地说难以追踪)被调用的方法链和改变的值,这特别使得入职过程悲剧。

答案 7 :(得分:0)

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

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

2)你可以使用angularjs事件。你可以随时发送一个信号(来自$ rootScope)并在任何你想要的地方捕获它。你甚至可以发送关于该eventName的数据。

也许这可以帮到你。 如果您需要更多示例,请点击链接

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

答案 8 :(得分:-1)

怎么样?
#r

ParentScope是注入服务的地方吗?

答案 9 :(得分:-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);
    }
};

然后,我在我的控制器中放置了一个所需频道的监听器,并以这种方式保持我的本地范围更新。

总之,最佳实践&#34; - 相反,它的主要是偏好 - 只要你保持SOLID并使用弱耦合。我提倡后一种代码的原因是因为EDA本质上具有最低耦合可行性。如果你不太关心这个事实,那就让我们一起避免同一个项目。

希望这会有所帮助......