Статьи

Управление клиентским состоянием только в AngularJS

Модели представления в платформах JavaScript, таких как AngularJS, могут отличаться от моделей домена на сервере — модель представления даже не должна существовать на сервере. Из этого следует, что модели представлений могут иметь только клиентское состояние, например, «анимация начата» и «анимация закончена» или «перетаскиваться» и «отброшено». В этом посте речь пойдет об изменениях состояния при создании и сохранении моделей представлений с использованием службы $resource Angular.

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

 angular.module('clientOnlyState.controllers') .controller('ArticleCtrl', function($scope, $resource, ArticleStates /* simple lookup */) { var Article = $resource('/article/:articleId', { articleId: '@id' }); var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); article.state = ArticleStates.NONE; // "NONE" $scope.article = article; $scope.save = function() { article.state = ArticleStates.SAVING; // "SAVING" article.$save(function success() { article.state = ArticleStates.SAVED; // "SAVED" }); }; }); 

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

$resource Сервисы

Давайте начнем с извлечения нашего ресурса Article в сервис для инъекций. Давайте также добавим самый простой параметр состояния в NONE при первом создании Article .

 angular.module('clientOnlyState.services') .factory('Article', function($resource, ArticleStates) { var Article = $resource('/article/:articleId', { articleId: '@id' }); // Consumers will think they're getting an Article instance, and eventually they are... return function(data) { var article = new Article(data); article.state = ArticleStates.NONE; return article; } }); 

Как насчет поиска и сохранения? Мы хотим, чтобы Article казался потребителям сервисом $resource , поэтому он должен постоянно работать как один. Техника, которую я изучил в превосходной книге Джона Ресига «Секреты ниндзя JavaScript», очень полезна здесь — перенос функций. Вот его реализация, прямо поднятая в инъекционный сервис Angular.

 angular.module('clientOnlyState.services') .factory('wrapMethod', function() { return function(object, method, wrapper) { var fn = object[method]; return object[method] = function() { return wrapper.apply(this, [fn.bind(this)].concat( Array.prototype.slice.call(arguments)) ); }; } }); 

Это позволяет нам обернуть методы save и get Article и сделать что-то другое / дополнительное до и после:

 angular.module('clientOnlyState.services') .factory('Article', function($resource, ArticleStates, wrapMethod) { var Article = $resource('/article/:articleId', { articleId: '@id' }); wrapMethod(Article, 'get', function(original, params) { var article = original(params); article.$promise.then(function(article) { article.state = ArticleStates.NONE; }); return article; }); // Consumers will actually call $save with optional params, success and error arguments // $save consolidates arguments and then calls our wrapper, additionally passing the Resource instance wrapMethod(Article, 'save', function(original, params, article, success, error) { article.state = ArticleStates.SAVING; return original.call(this, params, article, function (article) { article.state = ArticleStates.SAVED; success && success(article); }, function(article) { article.state = ArticleStates.ERROR; error && error(article); }); }); // $resource(...) returns a function that also has methods // As such we reference Article's own properties via extend // Which in the case of get and save are already wrapped functions return angular.extend(function(data) { var article = new Article(data); article.state = ArticleStates.NONE; return article; }, Article); }); 

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

 angular.module('clientOnlyState.controllers') .controller('ArticleCtrl', function($scope, Article) { var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); console.log(article.state); // "NONE" $scope.article = article; $scope.save = function() { article.$save({}, function success() { console.log(article.state); // "SAVED" }, function error() { console.log(article.state); // "ERROR" }); }; }); 

Преимущества инкапсуляции

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

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

 angular.module('clientOnlyState.controllers') .controller('ArticleCtrl', function($scope, Article, ArticleStates) { var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); var translations = {}; translations[ArticleStates.SAVED] = 'Saved, oh yeah!'; translations['default'] = ''; $scope.article = article; $scope.save = function() { article.$save({}); }; $scope.$watch('article.state', function(newState, oldState) { if (newState == ArticleStates.SAVED && oldState == ArticleStates.SAVING) { $scope.message = translations[newState]; } else { $scope.message = translations['default']; } }); }); 

Предположим на мгновение, что $scope s, директивы и фильтры образуют API приложения. HTML-представления используют этот API. Чем больше компоновка API, тем больше вероятность его повторного использования. Могут ли фильтры улучшить компоновку по сравнению с просмотром новых или старых?

Составление через фильтры, панацея?

Что-то вроде следующего — вот что я имею в виду. Каждая часть выражения становится многоразовой.

 <p>{{article.state | limitToTransition:"SAVING":"SAVED" | translate}}</p> 

Начиная с Angular 1.3, фильтры могут использовать свойство $stateful , но его использование настоятельно не рекомендуется, поскольку Angular не может кэшировать результат вызова фильтра на основе значения входных параметров. Таким образом, мы передадим параметры с limitToTransition состояния в limitToTransition (предыдущее состояние) и translate (доступные переводы).

 angular.module('clientOnlyState.filters') .filter('limitToTransition', function() { return function(state, prevState, from, to) { if(prevState == from && state == to) return to; return ''; }; }) .filter('translate', function() { return function(text, translations) { return translations[text] || translations['default'] || ''; }; }); 

Из-за этого нам нужна небольшая поправка к Article :

 function updateState(article, newState) { article.prevState = article.state; article.state = newState; }; wrapMethod(Article, 'get', function(original, params) { var article = original(params); article.$promise.then(function(article) { updateState(article, ArticleStates.NONE); }); return article; }); 

Конечный результат не так хорош, но все же очень мощный:

 <p>{{article.state | limitToTransition : article.prevState : states.SAVING : states.SAVED | translate : translations}}</p> 

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

 angular.module('clientOnlyState.controllers') .controller('ArticleCtrl', function($scope, Article, ArticleStates) { var article = new Article({ id: 1, title: 'A title', author: 'M Godfrey' }); // Could be injected in... var translations = {}; translations[ArticleStates.SAVED] = 'Saved, oh yeah!'; translations['default'] = ''; $scope.article = article; $scope.states = ArticleStates; $scope.translations = translations; $scope.save = function() { article.$save({}); }; }); 

Вывод

Извлечение моделей представлений в инъекционные сервисы помогает нам масштабировать приложения. Пример, приведенный в этом посте, намеренно прост. Рассмотрим приложение, которое позволяет торговать валютными парами (например, GBP к USD, EUR к GBP и т. Д.). Каждая валютная пара представляет продукт. В таком приложении могут быть сотни продуктов, каждый из которых получает обновления цен в режиме реального времени. Обновление цены может быть выше или ниже текущей цены. Одна часть приложения может заботиться о ценах, которые повысились в два раза подряд, в то время как другая часть может заботиться о ценах, которые только что понизились. Возможность наблюдать за этими состояниями изменения цены значительно упрощает различные части приложения.

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

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

Образцы кода, созданные во время исследования этого поста, также доступны на GitHub .