JavaScript
Angular
18 September 2014

Смелый стайлгайд по AngularJS для командной разработки [2/2]

Original author: Todd Motto
Translation
Первая часть перевода тут.

После прочтения Google's AngularJS Guidelines, у меня создалось впечатление о его незавершённости, а ещё в нём часто намекали на профит от использования библиотеки Closure. Ещё они заявили, «Мы не думаем, что эти рекомендации одинаково хорошо применимы для всех проектов, использующих AngularJS. Мы будем рады видеть инициативу от сообщества за более общий стайлгайд, применимый как для небольших так и крупных проектов».

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

Директивы


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

Манипуляции с DOM


Манипуляциям с DOM следует находиться в методе link директивы.

Плохо:

// не используем контроллер
function MainCtrl (SomeService) {

  this.makeActive = function (elem) {
    elem.addClass('test');
  };

}
angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

Хорошо:

// используем директиву
function SomeDirective (SomeService) {
  return {
    restrict: 'EA',
    template: [
      '<a href="" class="myawesomebutton" ng-transclude>',
        '<i class="icon-ok-sign"></i>',
      '</a>'
    ].join(''),
    link: function ($scope, $element, $attrs) {
      // DOM manipulation/events here!
      $element.on('click', function () {
        $(this).addClass('test');
      });
    }
  };
}
angular
  .module('app')
  .directive('SomeDirective', SomeDirective);

Соглашение об именовании


Пользовательские директивы не должны иметь префикса ng-* в названии, во избежании возможного переназначения кода будущими релизами Angular. Наверняка, на момент появления ng-focus, было написано много директив, с таким-же названием, чья работа в приложениях была парализована только из-за использования такого же названия. Также, использование этого префикса запутывает, и внешне из кода представления не понятно, какие из директив написаны пользователем, а какие пришли с библиотекой.

Плохо:

function ngFocus (SomeService) {
  return {};
}
angular
  .module('app')
  .directive('ngFocus', ngFocus);

Хорошо:

function focusFire (SomeService) {
  return {};
}
angular
  .module('app')
  .directive('focusFire', focusFire);

Для именования директив используется camelCase. Первая буква в названии директивы при этом строчная. Стоит также заметить, что в коде представления (view) мы орудуем уже названием директивы, написанным через дефис. Так, для использования директивы focusFire в представлении мы обращаемся через <input focus-fire>.

Ограничения в использовании


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

Плохо:

Это очень путает.

<!-- directive: my-directive -->
<div class="my-directive"></div>

Хорошо:

Декларативные пользовательские директивы выглядят более выразительно.

<my-directive></my-directive>
<div my-directive></div>

Разрешение promise в роутере, а defer в контроллере


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

Благодаря angular-route.js (либо аналогичным сторонним дополнениям, таким как ui-router.js), мы получаем возможность использовать свойство resolve, для разрешения promise'ов следующего представления до момента отображения нам готовой страницы. Это означает, что контроллер для данного представления будет создан сразу после получения всех данных, а это в свою очередь значит, что до этого момента не будет вызовов функций.

Плохо:

function MainCtrl (SomeService) {

  var self = this;

  // не будет разрешён
  self.something;

  // будет разрешён асинхронно
  SomeService.doSomething().then(function (response) {
    self.something = response;
  });

}
angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

Хорошо:

function config ($routeProvider) {
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html',
    resolve: {
      doSomething: function (SomeService) {
        return SomeService.doSomething();
      }
    }
  });
}
angular
  .module('app')
  .config(config);

На данном этапе внутри нашего сервиса произойдёт привязка promise к отдельному объекту, который в свою очередь, может быть передан «отложенному» контроллеру:

Хорошо:

function MainCtrl (SomeService) {
  // будет разрешён!
  this.something = SomeService.something;
}
angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

Но здесь есть ещё кое-что, что можно улучшить. Можно перенести свойство resolve прямо в код контроллера, что позволит избежать наличия какой-либо логики в коде маршрутизатора.

Отлично:

// конфигурация, где resolve ссылается на метод в соответствующем контроллере
function config ($routeProvider) {
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html',
    controller: 'MainCtrl',
    controllerAs: 'main',
    resolve: MainCtrl.resolve
  });
}
// собственно, контроллер
function MainCtrl (SomeService) {
  // будет разрешён!
  this.something = SomeService.something;
}
// описываем свойство resolve прямо в контроллере
MainCtrl.resolve = {
  doSomething: function (SomeService) {
    return SomeService.doSomething();
  }
};

angular
  .module('app')
  .controller('MainCtrl', MainCtrl)
  .config(config);

Изменение маршрута и спиннер


В процессе разрешения нового маршрута, наверняка нам захочется показать что-нибудь для индикации прогресса. По обыкновению Angular создаёт событие $routeChangeStart, когда мы покидаем текущую страницу. В этот самый момент можно показать спиннер. А убрать его можно в момент возникновения события $routeChangeSuccess (подробнее здесь).

Избегайте $scope.$watch


Используйте $scope.$watch только тогда, когда нет возможности обойтись без него. Стоит помнить, что по производительности он значительно уступает решениям ng-change.

Плохо:

<input ng-model="myModel">
<script>
$scope.$watch('myModel', callback);
</script>

Хорошо:

<input ng-model="myModel" ng-change="callback">
<!--
  $scope.callback = function () {
    // go
  };
-->

Структура проекта


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

|-- app.js
|-- controllers.js
|-- filters.js
|-- services.js
|-- directives.js

Хорошо:

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

|-- app.js
|-- controllers/
|   |-- MainCtrl.js
|   |-- AnotherCtrl.js
|-- filters/
|   |-- MainFilter.js
|   |-- AnotherFilter.js
|-- services/
|   |-- MainService.js
|   |-- AnotherService.js
|-- directives/
|   |-- MainDirective.js
|   |-- AnotherDirective.js

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

Хорошо:

|-- app.js
|-- dashboard/
|   |-- DashboardService.js
|   |-- DashboardCtrl.js
|-- login/
|   |-- LoginService.js
|   |-- LoginCtrl.js
|-- inbox/
|   |-- InboxService.js
|   |-- InboxCtrl.js

Соглашения об именовании и конфликты


В Angular есть множество объектов, чьё название начинается со знака $, например $scope или $rootScope. Этот символ как бы намекает нам на то, что тот или иной объект является публичным и с ним можно взаимодействовать из разных мест. Мы также знакомы с такими вещами как $$listeners, которые также доступны в коде, но считаются приватными.

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

Плохо:

Здесь мы задаём $$SomeService в качестве определения, название функции при этом оставляем без префиксов.

function SomeService () {

}
angular
  .module('app')
  .factory('$$SomeService', SomeService);

Хорошо:

Здесь мы задаём SomeService в качестве определения и названия самой функции для более выразительного stack trace.

function SomeService () {

}
angular
  .module('app')
  .factory('SomeService', SomeService);

Минификация и аннотация


Порядок аннотации


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

Плохо:

// зависимости указаны беспорядочно
function SomeCtrl (MyService, $scope, AnotherService, $rootScope) {

}

Хорошо:

// сначала провайдеры Angular -> свои
function SomeCtrl ($scope, $rootScope, MyService, AnotherService) {

}

Автоматизируйте минификацию


Используйте ng-annotate для автоматической аннотации зависимостей, ведь ng-min устарел и больше не поддерживается. Что касается ng-annotate, так подробнее о нём здесь.

В нашем случае, когда мы описываем код модуля в отдельной функции, для корректной минификации необходимо будет использовать комментарий @ngInject перед теми функциями с зависимостями. Этот комментарий является инструкцией ng-annotate для автоматического описания зависимостей того или иного модуля.

Плохо:

function SomeService ($scope) {

}
// ручное описание зависимостей – это пустая трата времени
SomeService.$inject = ['$scope'];
angular
  .module('app')
  .factory('SomeService', SomeService);

Хорошо:

/**
 * @ngInject
 */
function SomeService ($scope) {

}
angular
  .module('app')
  .factory('SomeService', SomeService);

В итоге это превратится в следующее:

/**
 * @ngInject
 */
function SomeService ($scope) {

}
// следующую строчку ng-annotate создаст автоматически
SomeService.$inject = ['$scope'];
angular
  .module('app')
  .factory('SomeService', SomeService);

Данный стайлгайд находится в процессе доработки. Всегда актуальные рекомендации Вы найдёте на Github.

+18
22.2k 225
Comments 29