AngularJS: правильный способ привязки к свойствам сервиса


Я ищу лучшую практику привязки к свойству сервиса в AngularJS.

Я работал через несколько примеров, чтобы понять, как привязать к свойствам в сервисе, который создается с помощью AngularJS.

ниже у меня есть два примера, как привязать к свойствам в службе; они оба работают. В первом примере используются базовые привязки, а во втором - $scope.$watch для привязки к свойствам сервиса

являются ли эти пример предпочтителен при привязке к свойствам в службе или есть другой вариант, о котором я не знаю, что было бы рекомендовано?

предпосылка этих примеров заключается в том, что служба должна обновлять свои свойства "lastUpdated" и "вызовы" каждые 5 секунд. После обновления свойств сервиса представление должно отражать эти изменения. Оба эти примера работают успешно; интересно, есть ли лучший способ сделать это.

базовый Привязка

следующий код можно просмотреть и запустить здесь: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.$часы в контроллере.

$scope.$смотреть

следующий код можно просмотреть и запустить здесь: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.$ трансляция в сервисе и $корень.$on в контроллере, но в других примерах, которые я создал, которые используют $broadcast/$при первой трансляции не захватываются контроллером, но дополнительные вызовы, которые транслируются, запускаются в контроллере. Если вы знаете способ решения $rootscope.$проблема трансляции, пожалуйста, дайте ответ.

но чтобы повторить то, что я упоминал ранее, я хотел бы знать, как лучше всего привязать к сервису свойства.


обновление

этот вопрос был первоначально задан и дан ответ в апреле 2013 года. В мае 2014 года Гил Бирман предоставил новый ответ, который я изменил как правильный ответ. Поскольку ответ Гила Бирмана имеет очень мало голосов, меня беспокоит то, что люди, читающие этот вопрос, будут игнорировать его ответ в пользу других ответов с большим количеством голосов. Прежде чем вы примете решение о том, что является лучшим ответом, я настоятельно рекомендую Гил Бирман ответ.

10 159

10 ответов:

рассмотрим некоторые плюсы и минусы второго подхода:

  • 0{{lastUpdated}} вместо {{timerData.lastUpdated}}, который так же легко может быть {{timer.lastUpdated}}, который я мог бы утверждать, более читаем (но давайте не будем спорить... Я даю этому пункту нейтральную оценку, поэтому вы решаете сами)

  • +1 может быть удобно, что контроллер действует как своего рода API для разметки такой, что если каким-то образом структура изменений модели данных вы можете (теоретически) обновить контроллер сопоставления API не касаясь HTML частично.

  • -1 однако теория не всегда является практикой, и мне обычно приходится изменять разметку и логика контроллера, когда требуются изменения, в любом случае. Таким образом, дополнительные усилия по написанию API отрицают это преимущество.

  • -1 кроме того, этот подход не очень сухой.

  • -1 если вы хотите привязать данные к ng-model ваш код становится еще менее сухим, как вы должны повторно упаковать $scope.scalar_values в контроллере, чтобы сделать новый вызов REST.

  • -0.1 есть крошечный хит производительности, создающий дополнительный наблюдатель(ы). Кроме того, если свойства данных присоединены к модели, которая не нужно наблюдать в определенном контроллере, они создадут дополнительные накладные расходы для глубоких наблюдателей.

  • -1 что делать, если несколько контроллеров нуждаются в одних и тех же моделях данных? Это означает, что у вас есть несколько API для обновления при каждом изменении модели.

$scope.timerData = Timer.data; начинает звучать очень заманчиво прямо сейчас... Давайте погрузимся немного глубже в эту последнюю точку... О каких изменениях в модели мы говорили о чем? Модель на заднем конце (сервер)? Или модель, которая создается и живет только в интерфейсе? В любом случае, что по сути является Data mapping API принадлежит в уровень обслуживания переднего плана, (угловая фабрика или обслуживание). (Обратите внимание, что ваш первый пример-мои предпочтения - не имеет такого API в слой сервиса, что хорошо, потому что это достаточно просто, он не нуждается в этом.)

в заключение, все не должно быть разъединено. И поскольку разметка полностью отделена от модели данных, недостатки перевешивают преимущества.


контроллеры, в общем не должны быть завалены $scope = injectable.data.scalar's. скорее всего, они должны быть посыпаны $scope = injectable.data ' s,promise.then(..)и $scope.complexClickAction = function() {..} ' s

в качестве альтернативного подхода для достижения развязки данных и, следовательно, инкапсуляции представления, единственное место, которое это действительно имеет смысл отделить представление от модели и с директивой. Но даже там, не $watch скалярные значения в controller или link функции. Это не сэкономит время и не сделает код более удобным для обслуживания и чтения. Это даже не сделает тестирование проще, так как робастные испытания в угловом обычно испытывают приводя DOM так или иначе. Скорее, в директиве требуют вашего API данных в форме объекта, и пользу, используя только $watchers, созданные 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>

обновление: я, наконец, вернулся к этому вопросу, чтобы добавить, что я не думаю, что любой подход "неправильный". Первоначально я написал, что ответ Джоша Дэвида Миллера был неправильным, но в ретроспективе его точки зрения полностью справедливы, особенно его точка зрения о разделении проблем.

разделение сторону (но косвенное отношение), есть еще одна причина для защитное копирование это я не учел. Этот вопрос в основном касается чтения данных непосредственно из службы. Но что, если разработчик в вашей команде решит, что контроллер должен преобразовать данные каким-то тривиальным способом, прежде чем представление отобразит его? (Должны ли контроллеры вообще преобразовывать данные-это еще одно обсуждение.) Если она сначала не сделает копию объекта, она может невольно вызвать регрессии в другой компонент представления, который потребляет те же данные.

что этот вопрос действительно подчеркивает, так это архитектурные недостатки типичного углового приложения (и действительно любого приложения JavaScript): тесная связь проблем и изменчивость объекта. Я недавно влюбился в архитектуру приложения с React и неизменяемые структуры данных. Это решает следующие две проблемы чудесно:

  1. разделение: компонент потребляет все свои данные через реквизит и практически не зависит от глобальных синглетов (таких как угловые сервисы) и ничего не знает о том, что произошло выше это в иерархии представлений.

  2. переменчивости: все реквизиты являются неизменными, что исключает риск невольной мутации данных.

Угловое 2.0 теперь находится на пути к тому, чтобы сильно заимствовать у React для достижения двух пунктов выше.

С моей точки зрения, $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 заявления).

чтобы быть справедливым, здесь, вероятно, есть несколько точек зрения, и я с нетерпением жду других ответов.

поздно на вечеринку, но для будущих гуглеров - не используйте предоставленный ответ.

JavaScript имеет механизм передачи объектов по ссылке, в то время как он передает только мелкую копию для значений "числа, строки и т. д.".

в приведенном выше примере, вместо привязки атрибутов сервиса,почему бы нам не предоставить услугу в область?

$scope.hello = HelloService;

этот простой подход сделает угловой способный сделать двухстороннюю вязку и все волшебные вещи тебе нужно. Не взламывайте контроллер с помощью наблюдателей или ненужной разметки.

и если вы беспокоитесь о том, что ваше представление случайно перезаписывает атрибуты службы, используйте defineProperty чтобы сделать его читаемым, перечислимым, настраиваемым или определить геттеры и сеттеры. Вы можете получить много контроля, сделав свой сервис более твердым.

последний совет: если вы тратите свое время на работу с контроллером больше, чем ваши услуги, то вы делаете это неправильно :(.

в этом конкретный демо-код, который вы предоставили, я бы рекомендовал вам сделать:

 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, а также задать новое значение в базу данных как-то!

или мы можем принять любой подход, мы хотеть.

относятся к документация MDN на defineProperty.

Я думаю, что этот вопрос имеет контекстуальный компонент.

Если вы просто вытаскиваете данные из службы и излучаете эту информацию в ее представление, я думаю, что привязка непосредственно к свойству службы просто прекрасна. Я не хочу писать много boilerplate кода чтобы просто сопоставить свойства сервиса для моделирования свойств для использования в моем представлении.

кроме того, производительность в angular основана на двух вещах. Во-первых, сколько Привязок на странице. Второй насколько дороги функции геттера. Миско говорит об этом здесь

Если вам нужно выполнить конкретную логику экземпляра для данных службы (в отличие от массажа данных, применяемого в самой службе), и результат этого влияет на модель данных, подверженную представлению, тогда я бы сказал, что $watcher подходит, если функция не очень дорогая. В случае дорогостоящей функции я бы предложил кэшировать результаты в локальном (контроллере) переменная, выполняющая ваши сложные операции за пределами функции $watcher, а затем привязывающая вашу область к результату этого.

как предостережение, вы не должны висеть любой свойства непосредственно с вашего $ scope. Элемент $scope переменная-это не ваша модель. Он имеет ссылки на вашу модель.

на мой взгляд, "лучшая практика" для простого излучения информации из службы вниз для просмотра:

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

и тогда ваш взгляд будет содержать {{model.timerData.lastupdated}}.

основываясь на приведенных выше примерах, я подумал, что я бы бросил способ прозрачной привязки переменной контроллера к служебной переменной.

в приведенном ниже примере изменения контроллера $scope.count переменная будет автоматически отражена в сервисе count переменной.

в производстве мы фактически используем эту привязку для обновления идентификатора на службе, которая затем асинхронно извлекает данные и обновляет свои сервисные vars. Дальнейшее связывание, что означает, что контроллеры автоматически обновляться, когда служба обновляет себя.

код ниже можно увидеть работает на 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);
    };

}

Я думаю, что это лучший способ, чтобы привязка к самому сервису вместо атрибутов на нем.

вот почему:

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

вы можете играть в нее на этот plunker.

Я бы предпочел, чтобы мои наблюдатели были как можно меньше. Мой разум основан на моем опыте, и можно было бы утверждать это теоретически.
Проблема с использованием наблюдателей заключается в том, что вы можете использовать любое свойство в области для вызова любого из методов в любом компоненте или службе, которые вам нравятся.
В реальном мире проекта, довольно скоро вы будете в конечном итоге с не tracable (лучше сказать, трудно проследить) цепочка вызываемых методов и изменяемых значений, что специально делает посадку на борт процесс трагический.

для привязки любых данных, которые отправляет сервис не очень хорошая идея (архитектура),но если вам это нужно больше, я предлагаю вам 2 способа сделать это

1) Вы можете получить данные не внутри вас службы.Вы можете получить данные внутри вашего контроллера / директивы, и у вас не будет проблем, чтобы привязать его в любом месте

2) Вы можете использовать события angularjs.Когда вы хотите, вы можете отправить сигнал(от $rootScope) и поймать его, где вы хотите.Вы даже можете отправить данные об этом имя события.

может быть, это может помочь вам. Если вам нужно больше примеров,вот ссылка

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

а как же

scope = _.extend(scope, ParentScope);

где ParentScope является внедренной службой?

Самые Элегантные Решения...

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

кроме того, я пишу EDAs (Event-Driven Architectures), поэтому я склонен делать что-то вроде следующего [упрощенная версия]:

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

затем я помещаю слушателя в свой контроллер на нужный канал и просто обновляю свою локальную область таким образом.

В заключение, там не так много "лучшей практики" - скорее, его в основном предпочтение -- покуда вы держите вещи твердым и используя слабую связь. Причина, по которой я бы защищал последний код, заключается в том, что EDAs имеют самое низкое соединение возможно по своей природе. И если вы не слишком обеспокоены этим фактом, давайте не будем работать над одним и тем же проектом вместе.

надеюсь, что это помогает...