AngularJS: от прототипирования до функционального кода. Часть 3
AngularJS

AngularJS: от прототипирования до функционального кода. Часть 3

Оригинал: AngularJS: From Prototyping to Functional Code, Fernando Villalobos

Третья часть перевода статьи Фернандо Виллалобоса посвящена доработкам прототипа до полноценного приложения на AngularJS.

AngularJS: от прототипирования до функционального кода. Часть 1
AngularJS: от прототипирования до функционального кода. Часть 2

8. Рефакторинг

Наш клиент остался доволен демо: он/она дал зеленый свет. За работу!

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

8.1. Подключение JSON бэкенда

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

8.2. Рефакторинг: фабрика для управления клиентами

Наиболее правильным будет заключить весь код, взаимодействующий с сервером, в одном месте. Это защитит фронтенд от изменений API. Мы должны будем внести все изменения в одном месте, а контроллер даже не заметит изменений. Мы будем использовать фабрику, которяа будет инкапсулировать все взаимодействия с бэкендом. Внутри фабрика будет использовать сервис $http, который включен в AngularJS из коробки. Наша реализация будет использовать методы $http.get, $http.post, $http.put и $http.delete для взаимодействия с удаленными данными.


// scripts/factories/clients.coffee 
angular.module('clientsApp').factory('Clients', function($http) { 
    var BASE_URL = '/clients'; 
    return { 
        all: function() { 
            return $http.get(BASE_URL); 
        }, 
        create: function(client) { 
            return $http.post(BASE_URL, client); 
        }, 
        update: function(client) { 
            return $http.put(BASE_URL + '/' + client.id, client); 
        }, 
        delete: function(id) { 
            return $http.delete(BASE_URL + '/' + id); 
        } 
    }; 
});

Чтобы использовать эту фабрику, необходимо передать ее в контоллер ClientsCtrl:


angular.module('clientsApp').controller('ClientsCtrl', function($scope, Clients) {
  ...
});

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


Clients.all().success(function(data) {
   $scope.clients = data;
});

Таким образом объект clients, расположенный внутри объекта $scope, по прежнему будет содержать массив, разница лишь в том, что теперь это будут реальные данные. Остальные же части приложения даже не заметят этих изменений.

Теперь мы должны использовать методы create, update и delete фабрики Clients, но в то же время, они должны поддерживать массив $scope.clients (это наша «локальная» копия данных). Благодаря фабрике Clients эта зада будет не слишком сложной:


$scope.create = function() { 
    Clients.create($scope.newClient).success(function(data) { 
        $scope.clients.push(data); 
        $scope.newClient = null; 
    }); 
}; 
$scope.delete = function(client) { 
    return Clients.delete(client.id).success(function(data) { 
        var index = $scope.clients.indexOf(client); 
        $scope.clients.splice(index, 1); 
    }); 
}; 
$scope.update = function(client) { 
    Clients.update(client).success(function(data) { 
        $scope.activeClient = null; 
    }); 
};

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

8.3. Директива для валидации целых чисел

Ранее мы говорили, что наша форма можен производить более конкретные проверки. Например, поле возраст можно ограничить только целыми числами. Мы можем создать новую директиву валидации. Основным преимуществом этого решения является то, что оно не зависит от формы и может использоваться других местах.

Для простоты (и потому что это отвечает нашим потребностям), мы будем использовать реализацию директивы прямо из документации к AngularJS, из раздела custom validation.


// scripts/directives/integer.coffee 
var INTEGER_REGEXP = /^\-?\d+$/; 
    angular.module('clientsApp').directive('integer', function() { 
        return { 
            require: 'ngModel', 
            link: function(scope, elm, attrs, ctrl) { 
            ctrl.$validators.integer = function(modelValue, viewValue) { 
                if (ctrl.$isEmpty(modelValue)) return true; 
                if (INTEGER_REGEXP.test(viewValue)) return true; 
                return false; 
            }; 
        } 
    }; 
});

Теперь нам нужно только добавить директиву integer к полю age на форме и сообщение, связанное с этой ошибкой.


<form name='clientForm' novalidate role='form'> 
    ... 
    <input class='form-control' ng-model='newClient.age' name='age' type='number' required max='100' min='1' integer> 
    <span class='help-block' ng-show='clientForm.age.$error.integer'>Age should be an integer</span> 
    ... 
</form>
Кастомные валидаторы AngularJS

8.4. Фильтр для корректного отображения процентов

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

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

Во-первых, мы должны избавиться от метода percentageOf в контроллере и реализовать эту логику в фильтре:


// scripts/filters/percentage.coffee 
angular.module('clientsApp').filter('percentage', function() { 
    return function(value) { 
        return value * 100 + ' %'; 
    }; 
});

Во-вторых, мы должны доработать представление; вместо вызова метода percentageOf преобразуем значение с помощью фильтра percentage.


<!-- before --> 
<td>{{ percentageOf(client.percentage) }}</td> 

<!-- after --> 
<td>{{ client.percentage | percentage }}</td>
Фильтры AngularJS

Работает! Такое решение не требует слишком много дополнительного кода, но у нас есть преимущество повторного использования кода в других представлениях, контроллерах и даже других фильтрах.

8.5. Директива для режима редактирования

Самую «зрелищную» часть мы оставили на конец; фишка редактирования элемента «в строке» уже работает правильно, но мы можем ее улучшить. Для прототипа схема с объектом activeClient работала нормально, но в финальной версии мы можем добавить свойство isEditing, что позволит сделать компонент более гибким. К тому же, если мы сможем удалить этот функционал из контроллера и поместить где-нибудь еще, мы сможем легко добавить возможность отменить редактирование и восстановить начальные данные. Давайте напишем директиву, которая реализует обе эти возможности.

Во-первых, мы должны перенести код представления туда, где директива сможет получить к нему доступ — блок script с типом text/ng-template может хранить этот шаблон:


<script id='client.html' type='text/ng-template'>
  <td ng-if-start='client != activeClient'>{{ client.id }}</td>
  <td>{{ client.name }}</td>
  <td>{{ client.age }}</td>
  <td>{{ client.percentage | percentage }}</td>
  <td ng-if-end>
    <a class='btn btn-primary' href='' ng-click='edit(client)'>
      Edit
    </a>
    <a class='btn btn-danger' href='' ng-click='delete(client)'>
      Delete
    </a>
  </td>

  <td ng-if-start='client == activeClient'>{{ client.id }}</td>
  <td>
    <input class='form-control' ng-model='client.name' type='text'>
  </td>
  <td>
    <input class='form-control' ng-model='client.age' type='text'>
  </td>
  <td>
    <input class='form-control' ng-model='client.percentage' type='text'>
  </td>
  <td ng-if-end>
    <a class='btn btn-primary' href='' ng-click='update(client)'>
      Update
    </a>
  </td>
</script>

Таким способом, директива может ссылаться на шаблон по id. Мы использовали разновидность директивы ng-if: ng-if-start и ng-if-end; в таком варианте мы можем отобразить содержимое в зависимости от условия без необходимости оборачивать его в родительский тег, просто указав, где начинается условие, а где заканчивается.

Пришло время реализовать саму директиву; в контроллере есть методы create, update и delete и объект activeClient, наша директива может содержать методы, которые будут переключать логическое значение. Назовем их editClient, updateClient, deleteClient и isEditing:


// scripts/directives/clientRow.coffee
angular.module('clientsApp').directive('clientRow', function() {
  return {
    restrict: 'A',
    templateUrl: 'client.html',
    link: function(scope, element, attrs) {
      scope.isEditing = false;
      scope.editClient = function() {
        scope.isEditing = true;
      };
      scope.updateClient = function() {
        scope.isEditing = false;
      };
      scope.deleteClient = function() {};
    }
  };
});

Также, поскольку мы не определили область видимости директивы, в методе updateClient можно вызвать метод update из родительской области видимости:


angular.module('clientsApp').directive('clientRow', function() {
  return {
    ...
    link: function(scope, element, attrs) {
      ...
      scope.updateClient = function() {
        scope.update(scope.client);
        scope.isEditing = false;
      };
    }
  };
});

Точно так же в методе deleteClient можно вызвать метод delete:


angular.module('clientsApp').directive('clientRow', function() {
  return {
    ...
    link: function(scope, element, attrs) {
      ...
      scope.deleteClient = function() {
        scope.delete(scope.client);
      };
    }
  };
});

Теперь наш шаблон вместо методов edit, update и delete будет использовать методы editClient, updateClient и deleteClient, а нужные строки мы будем отображать в зависимости от значения переменной isEditing:


<script id='client.html' type='text/ng-template'>
  <td ng-if-start='!isEditing'>{{ client.id }}</td>
  ...
  <td ng-if-end>
    <a class='btn btn-primary' href='' ng-click='editClient()'>
      Edit
    </a>
    <a class='btn btn-danger' href='' ng-click='deleteClient()'>
      Delete
    </a>
  </td>

  <td ng-if-start='isEditing'>{{ client.id }}</td>
  ...
  <td ng-if-end>
    <a class='btn btn-primary' href='' ng-click='updateClient()'>
      Update
    </a>
  </td>
</script>

Также мы можем добавить возможность отменить редактирование и восстановить начальные данные. Во первых, метод editClient будет хранить копию начального объекта, чтобы иметь возможность восстановить его:


angular.module('clientsApp').directive('clientRow', function() {
  return {
    ...
    link: function(scope, element, attrs) {
      ...
      scope.editClient = function() {
        scope.original = angular.copy(scope.client);
        scope.isEditing = true;
      };
    }
  };
});

Во-вторых, мы добавим новый метод cancelEdit в нашу директиву; этот метод будет восстанавливать объект до начального состояния и переключать режим редактирования помощью свойвства isEditing.


angular.module('clientsApp').directive('clientRow', function() {
  return {
    ...
    link: function(scope, element, attrs) {
      ...
      scope.cancelEdit = function() {
        scope.isEditing = false;
        angular.copy(scope.original, scope.client);
      };
    }
  };
});

И наконец, в каждую строку таблицы добавим кнопку Cancel, щелчок по которой будет выполнять только что добавленный метод cancelEdit.


<td ng-if-start='isEditing'>{{ client.id }}</td>
...
<td ng-if-end>
  <a class='btn btn-primary' href='' ng-click='updateClient()'>
    Update
  </a>
  <a class='btn btn-danger' href='' ng-click='cancelEdit()'>
    Cancel
  </a>
</td>

Шаблон теперь будет выглядеть следующим образом:


<script id='client.html' type='text/ng-template'>
  <td ng-if-start='!isEditing'>{{ client.id }}</td>
  <td>{{ client.name }}</td>
  <td>{{ client.age }}</td>
  <td>{{ client.percentage | percentage }}</td>
  <td ng-if-end>
    <a class='btn btn-primary' href='' ng-click='editClient()'>
      Edit
    </a>
    <a class='btn btn-danger' href='' ng-click='deleteClient()'>
      Delete
    </a>
  </td>

  <td ng-if-start='isEditing'>{{ client.id }}</td>
  <td>
    <input class='form-control' ng-model='client.name' type='text'>
  </td>
  <td>
    <input class='form-control' ng-model='client.age' type='text'>
  </td>
  <td>
    <input class='form-control' ng-model='client.percentage' type='text'>
  </td>
  <td ng-if-end>
    <a class='btn btn-primary' href='' ng-click='updateClient()'>
      Update
    </a>
    <a class='btn btn-danger' href='' ng-click='cancelEdit()'>
      Cancel
    </a>
  </td>
</script>

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

Директивы AngularJS
Примечание: такого же результата мы могли достичь, создав еще один контроллер и добавив директиву ng-controller к некоторому тегу, который содердит директиву ng-repeat, но использование директивы дает большую гибкость для будущих изменений и не создает дополнительных областей видимости.

Заключение

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

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

Такой рабочий процесс довольно гибкий, но мы должны установить границы; из-за нашей способности производить изменения в прототипе, клиент может требовать внесения изменений снова и снова, и мы будем прототипировать день за днем. Не поймите меня неправильно, я не говорю, что нужно быть негибкими, просто важно помнить, что всякие изменения необходимо согласовывать, так как без этого мы можем в конечном итоге заниматься только построением прототипов, не написав ни строчки функционального кода.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться