Смелый стайлгайд по 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.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 29

    0
    В директивах:
       $(this).addClass('test');
    

    Ипользуется jquery. Это вообще «хорошо»?
      0
      Эм, а почему нет?
        0
        ну малоли.

        ng-class же православнее.
          0
          А. Понял, я думал, тут речь про jQuery вообще.

          Если говорить про пример с классами, иногда бывает такое, что, с одной стороны, нужно добавить какой-то класс к элементу, но при этом мы знаем, что это будет происходить только один раз и больше манипуляции с классами не предвидится. В таком случае выводить логику в скоуп действительно не имеет смысла.
            0
            Ну мне и про вообще не очень понятно
            toster.ru/q/132115
      –1
      «После создания сервисов, наверняка мне захочется воспользоваться ими в контроллерах», которые к роутерам никак не прикручены.
      По моим безопытным представлениям, таких контроллеров будет большинство, а через роутер будет 1-2 контроллера.
      И тогда толку от этого совета никакого.
      Или я чегото недопонимаю?

        0
        На мой взгляд, с архитектурой вашего приложения что-то не так.
        По моему опыту, контроллеров, «не прикрученных» к роутеру, как раз 1-2 — те контроллеры, которые навешиваются на подгружаемые partial-view с компиляцией на лету (диалоговые окна и т.п.). Все остальные (основные) контроллеры «прикручены» к роутеру.
          0
          Конкретно в голове интерфейс такой:
          Страница содержит несколько вкладок (табов) по N типов объектов.
          Каждая вкладка содержит: тулбар действий, фильтр, таблица обнаруженных объектов, превью выбранного объекта.
          Открытие объекта создаёт новую вкладку: тулбар действий, просмотр/редактирование объекта, иногда — таблица связанных/вложенных объектов + тулбар действий с ними.

          На рутере у меня, как я понимаю, 2*N контроллеров вкладок (N для «каталогов» и N для отдельных объектов)
          А весь остальной ворох — это какраз partial-view, и они могут использовать разные сервисы.
            0
            P.S.
            ¿Или это у меня dojo головного мозга и такие вкладки принято засовывать в один контроллер?
        0
        Хорошо, но жаль что не много.
        Хочу добавить что ко всем не стандартным аттрибутам лучше добавлять «data-...» или «x-...» дабы не пугать HTML5 валидатор.
        Например data-ng-model=..., data-ng-controller=… data-ng-cloak=…
          0
          Я для этого при сборке использую плагин для gulp'a под названием gulp-angular-htmlify.
          И самому удобнее писать имена директив без data-префикса и валидатор ругаться не будет потом)
            0
            А что это даст?
              0
              Не понял вашего вопроса.

              но может ответ тут есть
              stackoverflow.com/questions/18607437/should-i-care-about-w3c-validation
                0
                Нет-нет, я хотел узнать, проходить валидацию чтобы что? Что мне даст то, что валидатор не ругается? Не сарказм, просто действительно не улавливаю смысла этого.
                  0
                  тут много написано, но к сожаления на английском.
            0
            Скажите, а насколько вообще перспективен этот фреймворк AngularJS? Сейчас стою перед выбором — изучать его или что-то другое, например, Prototype.
              +4
              Перспективен на 19.
              Что конкретно вас интересует?
                0
                Хочу делать SPA, RIA. При этом в качестве серверной платформы планируется старый добрый Django. Не случится ли так, что вскоре декларативный подход, используемый в Angular, будет признан плохой практикой программирования и проект загнётся?
                  0
                  А какая разница, кем и как он будет признан? Вам шашечки или ехать?
                  Вопрос очень субъективный. Мне нравится, удобно. И я буду пользоваться, даже если его вдруг будут обзывать плохими словами. Если, конечно, не обнаружу что-то лучше. Такой риск есть у любого решения.
                    0
                    Главное чтобы гугл не забросил проект. А они могут, у них дарт есть, и большое желание вывести его в массы.
                    0
                    А если на старый добрый напялить django-rest-framework, то будет достаточно пофиг, что на фронтенде.
                    http/rest пока умирать точно не собираются.

                    Ну и ещё, наверное, djangular, чтобы жаваскрипты аккуратно раскладывались
                –1
                Вы вот упомянули controllerAs, а тему не раскрыли.
                А это, как раз, было бы очень интересно. Остальное уже, вроде как, де-факто.
                По поводу ngInject — документация к npm-пакету говорит, что «ng-annotate follows references» и такой код

                function MyCtrl($scope, $timeout) {
                }
                var MyCtrl2 = function($scope) {};
                
                angular.module("MyMod").controller("MyCtrl", MyCtrl);
                angular.module("MyMod").controller("MyCtrl", MyCtrl2);
                

                будет аннотирован автоматом.
                Больше всего понравился трюк с Controller.resolve — действительно, удобно
                  +1
                  Тема с controllerAs былаже раскрыта в предыдущей статье.
                    –1
                    В статье ссылки не было, поэтому я не в курсе. Дайте ссылку, плиз
                0
                Скрытый текст
                |-- app.js
                |-- controllers/
                |   |-- MainCtrl.js
                |   |-- AnotherCtrl.js
                |-- filters/
                |   |-- MainFilter.js
                |   |-- AnotherFilter.js
                |-- services/
                |   |-- MainService.js
                |   |-- AnotherService.js
                |-- directives/
                |   |-- MainDirective.js
                |   |-- AnotherDirective.js
                


                Не путайте людей. Данный пример (второй из раздела «Структура проекта») — это точно такое же «Плохо», как и предыдущий первый. Подход «структурирование по типу» нельзя использовать даже в проектах с небольшой кодовой базой. Во-первых, маленькие проекты имеют тенденцию вырастать в большие, и вам впоследствии придется делать очень болезненный рефакторинг; во-вторых, это уменьшает пользу от существования единого соглашения по структуризации кода в разных проектах.

                «Хорошим» примером является только третий. Причем я бы обязательно добавил, что структура должна быть рекурсивной, а не одноуровневой. Больше чтива по теме из блога разработчиков.
                  0
                  должна быть рекурсивной

                  я правильно вашу мысль понял?

                  |-- app.js
                  |-- dashboard/
                  |   |--controllers/
                  |   |   |-- DashboardCtrl.js
                  |   |--services/
                  |   |   |-- DashboardService.js
                  |-- login/
                  |   |--services/
                  |   |   |-- LoginService.js
                  |   |--controllers/
                  |   |   |-- LoginCtrl.js
                  |-- inbox/
                  |   |--services/
                  |   |   |-- InboxService.js
                  |   |--controllers/
                  |   |   |-- InboxCtrl.js
                  
                    0
                    Нет, элементом рекурсии является кусок функциональности проекта (feature), а не тип сущности. Подробнее об этом по ссылке, что я привел выше.

                  Only users with full accounts can post comments. Log in, please.