Статьи

Приведите в порядок ваши угловые контроллеры с заводами и сервисами

Около пяти или шести лет назад было время, когда jQuery доминировал на стороне клиента в Интернете. Он читался как обычный английский, его было легко установить, а кривая обучения была достаточно ровной, чтобы малыши могли кататься на нем на своих трехколесных велосипедах. Однако с такой простотой доступа возникло множество проблем. С помощью jQuery было легко взломать что-то, что «работало», но за счет лучших практик, удобства обслуживания и масштабируемости.

Затем начались войны за фреймворки, и вскоре все стали настаивать на том, чтобы попробовать новейшую и лучшую фреймворк, который принесет обещанную структуру и масштабируемость в их приложение. Одним из таких фреймворков является AngularJS. Теперь кривая обучения Angular значительно круче, чем у jQuery, но я думаю, что она достигла точки, когда многие разработчики вполне уверенно могут установить базовое приложение. При этом использование фреймворка не решает автоматически основную проблему дизайна приложения. Все еще возможно создавать приложения в таких средах, как AngularJS, EmberJS или React, которые не подлежат обслуживанию и не масштабируются — на самом деле новички и даже пользователи среднего уровня довольно часто допускают эту ошибку.

Как вещи выходят из-под контроля так легко?

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

Давайте создадим простое приложение

Приложение, которое мы собираемся создать, представляет собой приложение для скоринга игроков Dribbble . Мы сможем ввести имя пользователя Dribbble и добавить его на табло.

Спойлер — Вы можете увидеть рабочую реализацию конечного продукта здесь .

Для начала создайте файл index.html со следующим содержимым:

 <!DOCTYPE html> <html> <head> <title>Angular Refactoring</title> <link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular.min.js"></script> </head> <body> <div> <div class="panel panel-default"> <div class="panel-heading">Dribbble Player Scores</div> <div class="panel-body"> <p>Add Dribbble players to see how they rank:</p> <div class="form-inline"> <input class="form-control" type="text" /> <button class="btn btn-default">Add</button> </div> </div> <ul class="list-group"> ... </ul> </div> </div> </body> </html> 

Создайте наше приложение AngularJS

Если вы уже написали приложение для Angular, следующие несколько шагов должны быть вам знакомы. Прежде всего, мы создадим файл app.js котором будем создавать экземпляр нашего приложения AngularJS:

 var app = angular.module("dribbbleScorer", []); 

Теперь мы включим это в наш файл index.html . Мы также добавим атрибут ng-app="dribbbleScorer" в наш <html> чтобы загрузить приложение Angular.

 <html ng-app="dribbbleScorer"> <head> <title>Angular Refactoring</title> <link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular.min.js"></script> <script src="app.js"></script> </head> ... 

Теперь, когда наше приложение настроено и загружено, мы можем приступить к обработке бизнес-логики нашего приложения.

Заставить это работать

Пришло время реально реализовать наше приложение. Помните, что мы подходим к этому «давайте работать», потому что это часто реальность, с которой мы сталкиваемся. Точно так же, как можно было спешить добавить обработчик щелчков с помощью jQuery, пользователи Angular часто ищут самый быстрый путь к работающему приложению: ng-controller . Давайте посмотрим, как это может работать.

В app.js мы определим контроллер и некоторые фиктивные данные игрока:

 var app = angular.module("dribbbleScorer", []); app.controller("DribbbleController", function($scope) { $scope.players = ["Tom", "Dick", "Harry"]; }); 

В index.html мы вставим наш контроллер, используя ng-controller , и отредактируем наш список ul чтобы перебрать игроков и отобразить каждого из них в виде li :

 <body> <!-- Add our DribbbleController --> <div ng-controller="DribbbleController"> ... <ul class="list-group"> <!-- Loop over players using ng-repeat --> <li class="list-group-item" ng-repeat="player in players"> {{player}} </li> </ul> ... </div> </body> 

Если вы сохраните оба файла и откроете index.html в браузере, вы увидите список из трех имен Том, Дик и Гарри. Довольно просто и пока чисто.

Реализация формы

Далее, давайте запустим нашу форму. Нам понадобится переменная для использования в качестве ng-model для поля ввода, и нам понадобится обработчик щелчка для кнопки. Обработчик кликов должен добавить наш вход в текущий список игроков.

В index.html добавьте модель и щелкните обработчик в нашей форме:

 <div ng-controller="DribbbleController"> ... <div class="form-inline"> <input class="form-control" type="text" ng-model="newPlayer" /> <button class="btn btn-default" ng-click="addPlayer(newPlayer)">Add</button> </div> ... </div> 

Далее мы реализуем эти две вещи в app.js :

 app.controller("DribbbleController", function($scope) { $scope.newPlayer = null; // Our model value is null by default $scope.players = ["Tom", "Dick", "Harry"]; // Adds a player to the list of players $scope.addPlayer = function(player) { $scope.players.push(player); } }); 

Проверьте это в браузере. Введите имя, нажмите кнопку Добавить, и оно должно появиться в списке. С контроллерами AngularJS довольно легко получить что-то действительно быстрое.

Извлечение данных из Dribbble

Теперь, вместо того, чтобы просто использовать фиктивные имена игроков, давайте на самом деле получим информацию об игроке из Dribbble. Мы обновим нашу addPlayer() чтобы отправить имя игрока в API Dribbble, и вместо этого поместим результат в список:

 app.controller("DribbbleController", function($scope, $http) { $scope.newPlayer = null; // Our model value is null by default $scope.players = ["Tom", "Dick", "Harry"]; // Fetches a Dribbble player and adds them to the list $scope.addPlayer = function(player) { $http.jsonp( 'http://api.dribbble.com/players/' + player + '?callback=JSON_CALLBACK' ).success(function(dribbble_player){ $scope.players.push(dribbble_player.name); }).error(function(){ // handle errors }); } }); 

Не забудьте сначала ввести службу $http в ваш контроллер. Dribbble API основан на JSONP, поэтому нам нужно использовать метод $http.jsonp() и добавить ?callback=JSON_CALLBACK к URL, чтобы Angular автоматически обрабатывал ответ для нас. Остальное довольно просто. В нашем успешном обратном вызове мы помещаем имя игрока в список. Попробуйте и попробуйте это в браузере.

Удаление игрока

Давайте добавим кнопку удаления в ряды наших игроков. Сначала внесите следующие изменения в index.html .

 <ul class="list-group"> <!-- Loop over players using ng-repeat --> <li class="list-group-item" ng-repeat="player in players"> {{player}} <a href="" ng-click="removePlayer(player)"> <i class="glyphicon glyphicon-remove pull-right"></i> </a> </li> </ul> 

Затем внесите эти изменения в app.js :

 app.controller("DribbbleController", function($scope, $http) { ... $scope.removePlayer = function(player) { $scope.players.splice($scope.players.indexOf(player), 1); }; }); 

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

Использование player Объект

Пришло время создать последний кусочек нашего приложения, прежде чем мы начнем рефакторинг. Мы собираемся создать произвольную «оценку за комментарий» и «оценку за» для наших игроков. Но сначала нам нужно превратить строки нашего игрока в объекты, чтобы они могли иметь свойства, которые мы затем можем отображать в DOM. Давайте обновим app.js чтобы использовать фактические объекты игрока, возвращенные из Dribbble:

 app.controller("DribbbleController", function($scope, $http) { $scope.newPlayer = null; // Our model value is null by default $scope.players = []; // We'll start with an empty list // Fetches a Dribbble player and adds them to the list $scope.addPlayer = function(player) { $http.jsonp( 'http://api.dribbble.com/players/' + player + '?callback=JSON_CALLBACK' ).success(function(dribbble_player){ $scope.players.push(dribbble_player); // Here we add the dribbble_player object to the list }).error(function(){ // handle errors }); }; }); 

Далее, давайте обновим DOM, чтобы использовать свойства плеера:

 <ul class="list-group"> <!-- Loop over players using ng-repeat --> <li class="list-group-item" ng-repeat="player in players"> <!-- We use player.name here instead of just player --> {{player.name}} <a href="" ng-click="removePlayer(player)"> <i class="glyphicon glyphicon-remove pull-right"></i> </a> </li> </ul> 

На этом этапе приложение все еще должно работать как обычно.

Подсчет баллов

Давайте добавим информацию о счете в DOM, а затем реализуем ее в нашем файле JavaScript:

 <ul class="list-group"> <li class="list-group-item" ng-repeat="player in players"> {{player.name}} L: {{likeScore(player)}} C:{{commentScore(player)}} <a href="" ng-click="removePlayer(player)"> <i class="glyphicon glyphicon-remove pull-right"></i> </a> </li> </ul> 

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

 app.controller("DribbbleController", function($scope, $http){ ... $scope.likeScore = function(player) { return player.likes_received_count - player.likes_count; }; $scope.commentScore = function(player) { return player.comments_received_count - player.comments_count; }; }); 

Перезагрузите страницу, добавьте несколько игроков, и вы должны увидеть оценку «Нравится» (L) и «Комментарий» (C) для каждого игрока.

Посмотрите на этот контроллер!

Теперь все хорошо, что наше приложение работает, но просто посмотрите на размер и сложность контроллера, который мы создали! В идеальном мире контроллер должен заниматься только этим: контролировать связь между различными частями вашего приложения. Здесь наш контроллер отвечает за абсолютно все.

 app.controller("DribbbleController", function($scope, $http) { $scope.newPlayer = null; // Our model value is null by default $scope.players = []; // We'll start with an empty list // Fetches a Dribbble player and adds them to the list $scope.addPlayer = function(player) { $http.jsonp( 'http://api.dribbble.com/players/' + player + '?callback=JSON_CALLBACK' ).success(function(dribbble_player) { $scope.players.push(dribbble_player); // Here we add the dribbble_player object to the list }).error(function() { // handle errors }); }; $scope.removePlayer = function(player) { $scope.players.splice($scope.players.indexOf(player), 1); }; $scope.likeScore = function(player) { return player.likes_received_count - player.likes_count; }; $scope.commentScore = function(player) { return player.comments_received_count - player.comments_count; }; }); 

Мы можем сделать лучше, чем это.

Использование угловой фабрики для абстрагирования наших проблем

Добавление и удаление игрока — это две концепции, которые принадлежат контроллеру. Дело не в том, что контроллер предоставляет эти функции, а в том, что он также отвечает за их реализацию. Разве не было бы лучше, если бы функция контроллера addPlayer() просто передавала этот запрос другой части приложения, которая могла бы обрабатывать все входы и выходы при фактическом добавлении проигрывателя. Ну, вот тут-то и появляются фабрики AngularJS.

Создание нашей фабрики

Если мы думаем в объектно-ориентированных терминах, мы имеем дело с объектом игрока Dribbble. Итак, давайте создадим фабрику, которая сможет производить плееры Dribbble. Мы просто реализуем это в том же файле app.js для простоты:

 app.controller("DribbbleController", function($scope, $http) { ... }); app.factory("DribbblePlayer", function() { // Define the DribbblePlayer function var DribbblePlayer = function(player) { }; // Return a reference to the function return (DribbblePlayer); }); 

Вы заметите, что мы определили DribbblePlayer с заглавным синтаксисом. Это потому, что это функция конструктора. Также обратите внимание, что функция конструктора принимает параметр player. Когда мы добавим эту фабрику в наш контроллер, мы сможем вызвать new DribbblePlayer(player) и вернуть его new DribbblePlayer(player) экземпляр, сконфигурированный для этого игрока.

Давайте добавим функцию инициализации в конструктор DribbblePlayer чтобы установить некоторые свойства по умолчанию:

 // We need to inject the $http service in to our factory app.factory("DribbblePlayer",function($http) { // Define the DribbblePlayer function var DribbblePlayer = function(player) { // Define the initialize function this.initialize = function() { // Fetch the player from Dribbble var url = 'http://api.dribbble.com/players/' + player + '?callback=JSON_CALLBACK'; var playerData = $http.jsonp(url); var self = this; // When our $http promise resolves // Use angular.extend to extend 'this' // with the properties of the response playerData.then(function(response) { angular.extend(self, response.data); }); }; // Call the initialize function for every new instance this.initialize(); }; // Return a reference to the function return (DribbblePlayer); }); 

Здесь есть несколько вещей, на которые стоит обратить внимание:

Мы определяем переменную self как ссылку на this в этом контексте это DribbblePlayer экземпляр DribbblePlayer . Мы делаем это так, чтобы экземпляр был доступен для расширения в обратном вызове then() обещания.

Мы также используем angular.extend() чтобы добавить все свойства игроков Dribbble, которые мы получили от API, в наш экземпляр DribbblePlayer . Это эквивалентно выполнению:

 playerData.then(function(response) { self.name = response.data.name; self.likes_count = response.data.likes_count; // etc }); 

Мы вызываем this.initialize() сразу после его определения. Это делается для имитации нормального поведения ООП, когда определение конструктора или метода initialize() приведет к выполнению этого метода при создании нового экземпляра этого класса.

Используя Фабрику

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

 ... // Inject DribbblePlayer into your controller and remove the $http service app.controller("DribbbleController", function($scope, DribbblePlayer) { $scope.newPlayer = null; $scope.players = []; $scope.addPlayer = function(player) { // We can push a new DribbblePlayer instance into the list $scope.players.push(new DribbblePlayer(player)); $scope.newPlayer = null; }; ... }); 

Перезагрузите приложение в браузере, и оно должно работать так же, как и раньше. Разве это не круто?

Что именно здесь происходит?

Напомним, что мы DribbblePlayer нашу фабрику DribbblePlayer в наш контроллер. Фабрика позволяет нам создавать новые экземпляры DribbblePlayer конструктора DribbblePlayer . Метод initialize() использует параметр имени игрока, чтобы получить сведения об игроке из Dribbble и установить их в качестве свойств экземпляра. И наконец, именно этот экземпляр мы запихиваем в наш список.

Нам вообще не нужно менять DOM, потому что он ожидает объекты, которые имеют name и like_count , и это именно то, что мы ему даем.

Это действительно того стоило?

Абсолютно! Мы не только упростили наш контроллер, мы разделили наши проблемы. Наш контроллер больше не занимается реализацией добавления плеера. Мы могли бы заменить new DribbblePlayer() на new BaseballSuperstar() , и нам нужно было бы изменить только одну строку кода. Более того, теперь мы можем абстрагировать и другие части контроллера, используя более читаемый и масштабируемый подход ООП.

Давайте перенесем likeScore() и commentScore() в нашу фабрику и установим их как методы для каждого экземпляра игрока, а не функции, которые принимают параметр player:

 ... this.initialize = function(argument) { ... }; this.likeScore = function() { return this.likes_received_count - this.likes_count; }; this.commentScore = function() { return this.comments_received_count - this.comments_count; }; } 

Теперь каждый раз, когда мы вызываем new DribbblePlayer(player) возвращаемый объект будет иметь метод commentScore() метод commentScore() . Они должны оставаться как функции, а не свойства, чтобы в каждом из циклов $digest Angular они генерировали новые значения, представляющие любые потенциальные изменения в модели DribbblePlayer .

Нам нужно обновить нашу DOM, чтобы отразить эти изменения:

 <ul class="list-group"> <li class="list-group-item" ng-repeat="player in players"> <!-- We can now use player.likeScore instead of likeScore(player) --> {{player.name}} L: {{player.likeScore()}} C:{{player.commentScore()}} <a href="" ng-click="removePlayer(player)"> <i class="glyphicon glyphicon-remove pull-right"></i> </a> </li> </ul> 

Завершение

Я попытался продемонстрировать, насколько легко для нас написать код, который просто «заставляет его работать», и чтобы этот код очень быстро вышел из-под контроля. Мы закончили с грязным контроллером, полным функций и обязанностей. Однако после некоторого рефакторинга наш файл контроллера теперь выглядит так:

 app.controller("DribbbleController", function($scope, DribbblePlayer) { $scope.newPlayer = null; $scope.players = []; $scope.addPlayer = function(player) { $scope.players.push(new DribbblePlayer(player)); }; $scope.removePlayer = function(player) { $scope.players.splice($scope.players.indexOf(player), 1); }; }); 

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

Код из этого урока доступен на GitHub !

Дополнительный кредит

Мы, конечно, улучшили addPlayer() , но зачем останавливаться на достигнутом? Вот несколько других улучшений, которые мы могли бы сделать:

  • Абстрагируйте вызов $http в ресурс Angular, чтобы отделить постоянство / выделение ресурсов. Затем вы можете добавить ресурс на свою фабрику, чтобы использовать его.
  • Создайте фабрику PlayerList для управления списком, включая добавление, удаление и сортировку. Таким образом, вы можете абстрагировать методы push() и splice() стоящие за PlayerList.add() и PlayerList.remove() чтобы вы не зависели от этой реализации непосредственно внутри вашего контроллера.