首页 文章

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

提问于
浏览
160

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

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

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

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

这些示例的前提是服务应每5秒更新其属性“lastUpdated”和“calls” . 更新服务属性后,视图应反映这些更改 . 这两个例子都成功地运作了我想知道是否有更好的方法 .

Basic Binding

可以在此处查看和运行以下代码:http://plnkr.co/edit/d3c16z

<html>
<body ng-app="ServiceNotification" >

    <div ng-controller="TimerCtrl1" style="border-style:dotted"> 
        TimerCtrl1 
Last Updated: {{timerData.lastUpdated}}
Last Updated: {{timerData.calls}}
</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
Last Updated: {{lastUpdated}}
Last Updated: {{calls}}
</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 . $ $和在控制器中使用$ root . $ on,但在我创建的其他使用$ broadcast / $的示例中,第一次广播不会被控制器,但在控制器中触发广播的其他呼叫 . 如果你知道一种方法来解决$ rootscope . $ broadcast问题,请提供答案 .

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


Update

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

10 回答

  • -2

    考虑一些 pros and cons of the second approach

    • 0 {{lastUpdated}} 而不是 {{timerData.lastUpdated}} ,这可能很容易 {{timer.lastUpdated}} ,我可能认为它更具可读性(但让's not argue... I' m给予这一点中性评级,所以你自己决定)

    • +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,这很好,因为它需要它 . )

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


    Controllers, in general 不应该被 $scope = injectable.data.scalar 弄乱 . 相反,他们应该撒上 $scope = injectable.datapromise.then(..)$scope.complexClickAction = function() {..}

    作为实现数据解耦并因此实现视图封装的替代方法, it really makes sense to decouple the view from the model 的唯一位置是 with a directive . 但即使在那里,也不要 controllerlink 函数中的标量值 . 这甚至可以赢得胜利测试更容易,因为角度的稳健测试通常会测试生成的DOM . 相反,在指令中以对象形式要求您的数据API,并且仅使用 ng-bind 创建的 $watch .


    示例http://plnkr.co/edit/MVeU1GKRTN4bqA3h9Yio

    <body ng-app="ServiceNotification">
        <div style="border-style:dotted" ng-controller="TimerCtrl1">
            TimerCtrl1
    Bad:
    Last Updated: {{lastUpdated}}
    Last Updated: {{calls}}
    Good:
    Last Updated: {{data.lastUpdated}}
    Last Updated: {{data.calls}}
    </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>

    UPDATE :我认为这两种方法都是"wrong" . 最初我写过Josh David Miller的答案是不正确的,但回想起来,他的观点完全有效,尤其是关于分离关注点的观点 .

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

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

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

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

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

  • 98

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

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

    function TimerCtrl1($scope, Timer) {
      $scope.$watch( function () { return Timer.data; }, function (data) {
        $scope.lastUpdated = data.lastUpdated;
        $scope.calls = data.calls;
      }, true);
    }
    

    这就是你所需要的 .

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


    为了提供一点上下文,我希望这种方法将服务值直接放在范围上的原因是为了确保关注点的正确分离 . 您的观点不应该认为它的工作就是将服务直接传递给视图 . 否则,控制器甚至在那里做什么?当AngularJS开发人员选择不在模板中包含任何"logic"时(例如 if 语句),他们遵循相同的推理 .

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

  • 12

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

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

    在上面的示例中,而不是绑定服务的属性, why don't we expose the service to the scope?

    $scope.hello = HelloService;
    

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

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

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

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

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

    Edit:

    如上所述,您可以使用 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 .

  • 1

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

    如果您查看,我认为直接绑定到服务属性就好了 . 我不想编写a lot of boilerplate code来简单地将服务属性映射到我的视图中要使用的模型属性 .

    此外,角度的性能基于两件事 . 首先是a上有多少绑定页 . 第二个是吸气功能有多贵 . Misko谈到这个here

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

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

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

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

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

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

    服务/控制器:

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

    我认为这是更好的方式 bind on the service itself 而不是它的属性 .

    原因如下:

    <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>
        
    ArrService.arrTwo: <span ng-repeat="v in ArrService.arrTwo">{{v}}</span>

    <!-- This is empty since $scope.arrOne never changes --> arrOne: <span ng-repeat="v in arrOne">{{v}}</span>
    <!-- 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上播放它 .

  • -1

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

  • 0

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

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

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

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

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

  • 19

    关于什么

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

    然后,我在我的控制器中将一个监听器放在所需的通道上,并以这种方式保持我的本地范围是最新的 .

    总而言之,只要你保持SOLID并使用弱耦合,就没有太多的东西 - 而是它的主要偏好 . 我提倡后一种代码的原因是因为EDA具有本质上可行的最低耦合 . 如果你不太关心这个事实,那就让我们一起避免同一个项目 .

    希望这可以帮助...

相关问题