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

Posted

技术标签:

【中文标题】AngularJS:绑定到服务属性的正确方法【英文标题】:AngularJS : The correct way of binding to a service properties 【发布时间】:2013-03-25 21:25:23 【问题描述】:

我正在寻找如何在 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 的答案。

【问题讨论】:

我认为 Josh David Miller 的回答比 Gil Birman 的要好。使用 $watch、$watchGroup 和 $watchCollection 甚至可以让它变得更好。关注点分离对于大中型应用非常重要。 @bardev 我认为这两个答案都没有用,新开发人员会完全错误地理解它们。 你问的问题是关于引用对象的原生 JavaScript 变量行为,我在下面添加了一点解释 【参考方案1】:

迟到了,但对于未来的 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 的值并以某种方式将新值发布到数据库!

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

有关defineProperty,请参阅MDN documentation。

【讨论】:

很确定这就是我上面所描述的,$scope.model = timerData: Timer.data;,只是将它附加到模型而不是直接在 Scope 上。 AFAIK,js 对象引用适用于服务中的所有内容。像这样编码:$scope.controllerVar = ServiceVar,你在$scope.controllerVar中所做的一切也在ServiceVar中完成。 @KaiWang 同意,除非你决定使用 DefineAttribute,否则你可以让你的服务有一个 setter 函数来防止控制器意外改变你的服务数据。【参考方案2】:

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

【讨论】:

【参考方案3】:

最优雅的解决方案...

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 具有最低耦合本质上可行的。如果你不太关心这个事实,让我们避免一起做同一个项目。

希望这会有所帮助...

【讨论】:

【参考方案4】:

怎么样

scope = _.extend(scope, ParentScope);

ParentScope 是一个注入服务?

【讨论】:

【参考方案5】:

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

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(..)'s 和$scope.complexClickAction = function() ..'s

作为实现数据解耦和视图封装的另一种方法,将视图与模型解耦真正有意义的唯一地方是使用指令。但即使在那里,也不要在controllerlink 函数中使用$watch 标量值。这不会节省时间或使代码更易于维护和阅读。它甚至不会使测试更容易,因为 Angular 中的健壮测试通常会测试生成的 DOM。相反,在指令中要求您的 data 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>

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

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

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

    关注点分离:组件通过 props 使用它的所有数据,并且几乎不依赖全局单例(例如 Angular 服务),并且对发生的事情一无所知在视图层次结构的上方

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

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

【讨论】:

More 我对 AngularJS 的了解越多。我相信 AngularJS 控制器应该尽可能简单和薄。通过在控制器中添加 $watches 会使逻辑复杂化。通过仅引用服务中的值,它更简单,并且似乎更像是 AngularJS 方式。 —— AngularJS 的“十诫”。它是声明性的。 @GilBirman 很好的答案。您介意为指令编写示例吗?为了说明您所说的“将视图与模型分离的唯一真正有意义的地方是使用指令。[...] 相反,在指令中要求您的数据 API 为对象形式,并倾向于仅使用 $由 ng-bind 创建的观察者。”【参考方案6】:

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

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

2) 您可以使用 angularjs 事件。无论何时,您都可以发送信号(来自 $rootScope)并在任何您想要的地方捕获它。您甚至可以发送关于该事件名称的数据。

也许这可以帮助你。 如果您需要更多示例,这里是链接

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

【讨论】:

【参考方案7】:

在上面的示例的基础上,我想我会采用一种透明地将控制器变量绑定到服务变量的方式。

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

在生产中,我们实际上是使用 this 绑定来更新服务上的 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>

服务/控制器:

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


【讨论】:

这是一个很棒的解决方案,谢谢,这对我有很大帮助,非常聪明的做法:)【参考方案8】:

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

原因如下:

<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上玩。

【讨论】:

【参考方案9】:

我认为这个问题有上下文成分。

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

此外,角度的性能基于两件事。第一个是页面上有多少绑定。第二个是 getter 函数有多昂贵。 Misko 谈到了这个here

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

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

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

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

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

【讨论】:

小心这个建议,不是 javascript 专家的人可以尝试使用字符串属性来做到这一点。在这种情况下,javascript 不会引用该对象,而是原始复制字符串。 (这很糟糕,因为它不会更新,如果它发生变化) 我没有用我的“警告”来说明你应该始终使用点(意思是不要把它挂在 $scope 上,而是挂在 $scope.model 上)。如果您有 $scope.model.someStringProperty,并且您在视图中引用了 model.someStringProperty,它将被更新,因为内部观察者将在对象上,而不是在道具上。【参考方案10】:

在我看来,$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 语句)时遵循相同的推理。

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

【讨论】:

你能详细说明一下吗?您是否更喜欢 $watches 因为视图与服务的耦合度较低?即,lastUpdatedtimerData.lastUpdated @BarDev,要在 $watch 中使用 Timer.dataTimer 必须在 $scope 上定义,因为传递给 $watch 的字符串表达式是根据范围计算的。这是一个plunker,它展示了如何实现这一目标。 objectEquality 参数记录在 here - 第三个参数 - 但没有真正解释得太清楚。 性能方面,$watch 效率很低。请参阅***.com/a/17558885/932632 和***.com/questions/12576798/… 的答案 @Kyrm 在某些情况下这可能是正确的,但在处理性能时,我们需要寻找“临床上显着”的性能增强,而不仅仅是统计上显着的、概括的增强。如果现有应用程序中存在性能问题,则应予以解决。否则,这只是过早优化的一种情况,会导致难以阅读、容易出错的代码不遵循最佳实践并且没有任何明显的好处。 假设观察者使用一个函数来调用getter,那么是的。它运作良好。我也是服务返回集合中实例的支持者,其中 es5 的 getter 和 setter 功能相当好。

以上是关于AngularJS:绑定到服务属性的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

在 AngularJS 服务中处理数据绑定

angularjs 绑定多个属性到下拉框

AngularJS数据双向绑定

Angular 7 无法绑定到“routerlink”,因为它不是“a”的已知属性

Angular视频教程汇总

AngularJS 1.5 绑定和编译 html 到模态