Angular Style Guide

Одобрена Angular Team

Agenda

  • Single Responsibility (Единственная обязанность)
  • IIFE
  • Modules (Модули)
  • Controllers (Контроллеры)
  • Services (Сервисы)
  • Factories (Фабрики)
  • Data Services (Сервисы данных)
  • Directives (Директивы)
  • Resolving Promises for a Controller (Работа с Объектами Promise в Контроллерe)
  • Manual Annotating for Dependency Injection (Аннотация для Внедренной Зависимости)
  • Minification and Annotation (Минификация и аннотация)
  • Exception Handling (Обработка Исключений)
  • Naming (Именования)
  • Startup Logic (Логика Запуска Приложения)
  • Angular $ Wrapper Services (Angular и Интерфейсные Сервисы)
  • Testing (Тестирование)
  • Animations (Анимации)
  • Comments (Комментарии)
  • Constants (Константы)
  • Routing (Маршрутизация)
  • Useful (Полезное)

John Papa

Эксперт разработчик Googleрегиональный директор 

Microsoft и MVP, автор более 100 статей и 10 книг, а также бывший евангелист технологии для клиент команд Microsoft.

Single Responsibility

Единственная обязанность

Правило №1

Определяйте 1 компонент в одном файле.

В одном и том же файле определяется модуль(module) app вместе с его зависимостями, определяется контроллер(controller), а также сервис(factory).

angular
    .module('app', ['ngRoute'])
    .controller('SomeController', SomeController)
    .factory('someFactory', someFactory);

function SomeController() { }

function someFactory() { }

Каждый компонент разнесен в свой отдельный файл.

/* рекомендовано */

// app.module.js
angular
    .module('app', ['ngRoute']);
/* рекомендовано */

// someController.js
angular
    .module('app')
    .controller('SomeController', SomeController);

function SomeController() { }
/* рекомендовано */

// someFactory.js
angular
    .module('app')
    .factory('someFactory', someFactory);

function someFactory() { }

IIFE

Immediately-Invoked Function Expression

Немедленно Исполняемые Функции

Оборачивайте компоненты Angular в Немедленно Исполняемые Функции(IIFE - Immediately Invoked Function Expression).

(function() {
    'use strict';
    ...

})();
// logger.js
angular
    .module('app')
    .factory('logger', logger);

// функция logger добавлена как глобальная переменная
function logger() { }
// storage.js
angular
    .module('app')
    .factory('storage', storage);

// функция storage добавлена как глобальная переменная
function storage() { }
// logger.js
(function() {
    'use strict';

    angular
        .module('app')
        .factory('logger', logger);

    function logger() { }
})();
// storage.js
(function() {
    'use strict';

    angular
        .module('app')
        .factory('storage', storage);

    function storage() { }
})();

Больше нет глобальных переменных

Modules

Модули

Избегайте коллизий имен

Используйте конвенцию уникальных имен с разделителями для подмодулей

Почему?

Уникальные имена помогают избежать коллизий в именах модулей. Разделители определяет сам модуль и его подмодульную иерархию. Например, app может быть вашим корневым модулем, а модули app.dashboard и app.users могут использоваться как имена модулей, зависимые от app.

Сеттеры (Setters)

Объявляйте модули без переменных, используйте сеттеры (setters).

/* избегайте этого */
var app = angular.module('app', [
    'ngAnimate',
    'ngRoute',
    'app.shared',
    'app.dashboard'
]);
/* рекомендовано */
angular
    .module('app', [
        'ngAnimate',
        'ngRoute',
        'app.shared',
        'app.dashboard'
    ]);

Используйте простой синтаксис сеттера.

Геттеры (Getters)

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

/* избегайте этого */
var app = angular.module('app');
app.controller('SomeController', SomeController);

function SomeController() { }
/* рекомендовано */
angular
    .module('app')
    .controller('SomeController', SomeController);

function SomeController() { }

Определение и получение модулей

Определите модуль один раз и получайте его во всех других сущностях.

- Используйте `angular.module('app', []);` для определения модуля.
- Используйте `angular.module('app');` чтобы получить модуль.

Именованные или Анонимные Функции

Используйте именованные функции, не передавайте анонимные функции обратного вызова в качестве параметров.

/* избегайте этого */
angular
    .module('app')
    .controller('Dashboard', function() { })
    .factory('logger', function() { });
// dashboard.js
angular
    .module('app')
    .controller('Dashboard', Dashboard);

function Dashboard() { }
// logger.js
angular
    .module('app')
    .factory('logger', logger);

function logger() { }

Контроллеры

Controllers

Синтаксис controllerAs в представлении

Используйте синтаксис controllerAs, который работает поверх синтаксиса классический контроллер со $scope

<!-- избегайте этого -->
<div ng-controller="Customer">
    {{ name }}
</div>
<!-- рекомендовано -->
<div ng-controller="Customer as customer">
    {{ customer.name }}
</div>

Синтаксис controllerAs в контроллере

Используйте синтаксис controllerAs поверх синтаксиса классический контроллер со $scope.

Синтаксис controllerAs использует внутри контроллеров ключевую переменную this, которая привязывается к $scope.

/* избегайте этого */
function Customer($scope) {
    $scope.name = {};
    $scope.sendMessage = function() { };
}
/* рекомендовано - 
но изучите следующую секцию */
function Customer() {
    this.name = {};
    this.sendMessage = function() { };
}

Синтаксис controllerAs с переменной vm

Сохраните this в переменную, когда используете синтаксис controllerAs. Выберите постоянное имя для переменной, такое как vm, что будет значить ViewModel.

/* избегайте этого */
function Customer() {
    this.name = {};
    this.sendMessage = function() { };
}
/* рекомендовано */
function Customer() {
    var vm = this;
    vm.name = {};
    vm.sendMessage = function() { };
}

Замечание:

Вы можете избежать любые jshint предупреждения, если разместите над строкой кода комментарий, как в приведенном ниже примере. Это не требуется, если функция названа с помощью ВерхнегоРегистра(UpperCasing), так как согласно этой конвенциии, это означает, что функция является конструктором контроллера Angular.

/* jshint validthis: true */
var vm = this;
<input ng-model="vm.title"/>
function SomeController($scope, $log) {
    var vm = this;
    vm.title = 'Some Title';

    $scope.$watch('vm.title', function(current, original) {
        $log.info('vm.title was %s', original);
        $log.info('vm.title is now %s', current);
    });
}

Привязываемые Члены Сверху

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

/* избегайте этого */
function Sessions() {
    var vm = this;

    vm.gotoSession = function() {
      /* ... */
    };
    vm.refresh = function() {
      /* ... */
    };
    vm.search = function() {
      /* ... */
    };
    vm.sessions = [];
    vm.title = 'Sessions';
}
/* рекомендовано */
function Sessions() {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = refresh;
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';

    ////////////

    function gotoSession() {
      /* */
    }

    function refresh() {
      /* */
    }

    function search() {
      /* */
    }
}
/* избегайте этого */
function Sessions(data) {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = function() {
        /**
         * эти
         * строки
         * кода
         * ухудшают
         * читабельность
         */
    };
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';
}
/* рекомендовано */
function Sessions(dataservice) {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = dataservice.refresh; // одна строка
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';
}

Определения Функций Для Скрытия Деталей Реализации

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

/**
* избегайте этого
* Использование выражений функций (function expressions).
*/
function Avengers(dataservice, logger) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    var activate = function() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    var getAvengers = function() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }

    vm.getAvengers = getAvengers;

    activate();
}
/*
* рекомендовано
* Объявления функций и
* привязанные члены смещены вверх.
*/
function Avengers(dataservice, logger) {
    var vm = this;
    vm.avengers = [];
    vm.getAvengers = getAvengers;
    vm.title = 'Avengers';

    activate();

    function activate() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    function getAvengers() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }
}

Логика Контроллера Отдельно

Переносите логику контроллера в сервисы и фабрики.

/* избегайте этого */
function Order($http, $q, config, userInfo) {
    var vm = this;
    vm.checkCredit = checkCredit;
    vm.isCreditOk;
    vm.total = 0;

    function checkCredit() {
        var settings = {};
        // Get the credit service base URL from config
        // Set credit service required headers
        // Prepare URL query string or data object with request data
        // Add user-identifying info so service gets the right credit limit for this user.
        // Use JSONP for this browser if it doesn't support CORS
        return $http.get(settings)
            .then(function(data) {
             // Unpack JSON data in the response object
               // to find maxRemainingAmount
               vm.isCreditOk = vm.total <= maxRemainingAmount
            })
            .catch(function(error) {
               // Interpret error
               // Cope w/ timeout? retry? try alternate service?
               // Re-reject with appropriate error for a user to see
            });
    };
}
/* рекомендовано */
function Order(creditService) {
    var vm = this;
    vm.checkCredit = checkCredit;
    vm.isCreditOk;
    vm.total = 0;

    function checkCredit() {
       return creditService.isOrderTotalOk(vm.total)
    .then(function(isOk) { vm.isCreditOk = isOk; })
          .catch(showServiceError);
    };
}

Один Контроллер - Одно Представление

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

Получение Контроллеров

Когда контроллер и его представление уже создано, и если нужно что-то повторно использовать (контроллер и представление), определяйте экземпляр контроллера вместе с его маршрутом (route).

Замечание: 

Если представление загружается не через маршрут, тогда используйте синтаксис ng-controller="Avengers as vm".

/* избегайте этого - когда используется маршрут
и необходимо динамическое назначение 
контроллера и представления */

// route-config.js
angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
          templateUrl: 'avengers.html'
        });
}
<!-- avengers.html -->
<div ng-controller="Avengers as vm">
</div>
/* рекомендовано */

// route-config.js
angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm'
        });
}
<!-- avengers.html -->
<div>
</div>

Сервисы (Services)

Синглтоны

Сервисы создаются с помощью ключевого слова new. Используйте this для публичных методов и переменных. Так как они очень похожи на фабрики, то используйте фабрики для согласованности.

Замечание:

Все Angular сервисы являются синглтонами. Это значит, что создается только один экземпляр сервиса на один инжектор.

// service
angular
    .module('app')
    .service('logger', logger);

function logger() {
  this.logError = function(msg) {
    /* */
  };
}
// factory
angular
    .module('app')
    .factory('logger', logger);

function logger() {
    return {
        logError: function(msg) {
          /* */
        }
   };
}

Фабрики (Factories)

Единственная Обязанность (Single Responsibility)

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

Синглтон

Фабрики это синглтоны, которые возвращают объект, содержащий свойства и методы сервиса.

Замечание:

Все Angular сервисы являются синглтонами.

Доступные Члены Наверх

Помещайте вызываемые члены сервиса (интерфейс) наверху, используя технику Revealing Module Pattern.

/* избегайте этого */
function dataService() {
  var someValue = '';
  function save() {
    /* */
  };
  function validate() {
    /* */
  };

  return {
      save: save,
      someValue: someValue,
      validate: validate
  };
}
/* рекомендовано */
function dataService() {
    var someValue = '';
    var service = {
        save: save,
        someValue: someValue,
        validate: validate
    };
    return service;

    ////////////

    function save() {
        /* */
    };

    function validate() {
        /* */
    };
}

Сервисы данных

(Data Services)

Отделите вызовы данных

Делайте рефакторинг логики работы с данными и взаимодействия этих данных с фабрикой. Создавайте сервисы данных, ответственных за вызовы XHR, локальное хранилище(local storage), работу с памятью или за любые другие операции с данными.

/* рекомендовано */

// фабрика сервиса данных
angular
    .module('app.core')
    .factory('dataservice', dataservice);

dataservice.$inject = ['$http', 'logger'];

function dataservice($http, logger) {
    return {
        getAvengers: getAvengers
    };

    function getAvengers() {
        return $http.get('/api/maa')
            .then(getAvengersComplete)
            .catch(getAvengersFailed);

        function getAvengersComplete(response) {
            return response.data.results;
        }

        function getAvengersFailed(error) {
            logger.error('XHR Failed for getAvengers.' + error.data);
        }
    }
}
/* рекомендовано */

// контроллер вызывает фабрику сервиса данных
angular
    .module('app.avengers')
    .controller('Avengers', Avengers);

Avengers.$inject = ['dataservice', 'logger'];

function Avengers(dataservice, logger) {
    var vm = this;
    vm.avengers = [];

    activate();

    function activate() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    function getAvengers() {
        return dataservice.getAvengers()
            .then(function(data) {
                vm.avengers = data;
                return vm.avengers;
            });
    }
}

Возвращение Объекта Promise для Контроллера

Если сервис данных возвращает promise типа $http, то в вызывающей функции возвращайте promise тоже.

/* рекомендовано */

activate();

function activate() {
    /**
     * Шаг 1
     * Запрашиваем функцию getAvengers function
     * для получения данных и ждем promise
     */
    return getAvengers().then(function() {
        /**
         * Шаг 4
         * Выполняем действие принятия финального объекта promise
         */
        logger.info('Activated Avengers View');
    });
}

function getAvengers() {
      /**
       * Шаг 2
       * Запрашиваем сервис для данных
       * и ждем promise
       */
      return dataservice.getAvengers()
          .then(function(data) {
              /**
               * Шаг 3
               * инициализируем данные и принимаем promise
               */
              vm.avengers = data;
              return vm.avengers;
      });
}

Директивы (Directives)

Одна Директива - Один Файл

Создавайте только одну директиву в файле. Называйте файл именем директивы.

/* избегайте этого */
/* directives.js */

angular
    .module('app.widgets')

    /* директива для заказа, которая специфична для модуля заказов */
    .directive('orderCalendarRange', orderCalendarRange)

    /* директива продажи, которая может быть использована везде в модуле продаж */
    .directive('salesCustomerInfo', salesCustomerInfo)

    /* директива крутилки (spinner), которая может быть использована во всех модулях */
    .directive('sharedSpinner', sharedSpinner);

function orderCalendarRange() {
    /* детали реализации */
}

function salesCustomerInfo() {
    /* детали реализации */
}

function sharedSpinner() {
    /* детали реализации */
}
/* рекомендовано */
/* calendarRange.directive.js */

/**
* @desc директива заказа, которая специфична модулю заказов в компании Acme
* @example <div acme-order-calendar-range></div>
*/
angular
    .module('sales.order')
    .directive('acmeOrderCalendarRange', orderCalendarRange);

function orderCalendarRange() {
    /* детали реализации */
}
/* рекомендовано */
/* customerInfo.directive.js */

/**
* @desc директива продажи, которая может быть использована везде в модуле продаж компании Acme
* @example <div acme-sales-customer-info></div>
*/
angular
    .module('sales.widgets')
    .directive('acmeSalesCustomerInfo', salesCustomerInfo);

function salesCustomerInfo() {
    /* implementation details */
}
/* рекомендовано */
/* spinner.directive.js */

/**
* @desc директива крутилки (spinner), которая может быть использована во всех модулях компании Acme
* @example <div acme-shared-spinner></div>
*/
angular
    .module('shared.widgets')
    .directive('acmeSharedSpinner', sharedSpinner);

function sharedSpinner() {
    /* детали реализации */
}

Манипулирование Элементами DOM в Директиве

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

Добавляйте Директивам Уникальный Префикс

Добавляйте директивам короткий, уникальный, пояснительный префикс, такой как 

acmeSalesCustomerInfo, директива будет объявлена в HTML как acme-sales-customer-info

Ограничивайте Элементы и Атрибуты

При создании директивы, которая планируется как самостоятельный элемент, применяйте ограничение E(разработано, как элемент) или по необходимости ограничение A (разработано, как атрибут). В основном, если директива разрабатывается как элемент, ограничения E вполне достаточно. Хотя Angular позволяет использовать EA, но все же лучше определится как реализовывать директиву, либо как самостоятельный отдельный элемент, либо как атрибут для улучшения функциональности существующего DOM-элемента.

<!-- избегайте этого -->
<div class="my-calendar-range"></div>
/* избегайте этого */
angular
    .module('app.widgets')
    .directive('myCalendarRange', myCalendarRange);

function myCalendarRange() {
    var directive = {
        link: link,
        templateUrl: '/template/is/located/here.html',
        restrict: 'C'
    };
    return directive;

    function link(scope, element, attrs) {
      /* */
    }
}
<!-- рекомендовано -->
<my-calendar-range></my-calendar-range>
<div my-calendar-range></div>
/* рекомендовано */
angular
    .module('app.widgets')
    .directive('myCalendarRange', myCalendarRange);

function myCalendarRange() {
    var directive = {
        link: link,
        templateUrl: '/template/is/located/here.html',
        restrict: 'EA'
    };
    return directive;

    function link(scope, element, attrs) {
      /* */
    }
}

Директивы и ControllerAs

Используйте синтактисcontroller as в директиве, чтобы директива была согласована с использованием синтаксиса controller as в паре контроллера и представлении.

angular
    .module('app')
    .directive('myExample', myExample);

function myExample() {
    var directive = {
        restrict: 'EA',
        templateUrl: 'app/feature/example.directive.html',
        scope: {
            max: '='
        },
        link: linkFunc,
        controller: ExampleController,
        controllerAs: 'vm'
    };

    return directive;

    function linkFunc(scope, el, attr, ctrl) {
        ...
    }
}

Resolving Promises for a Controller

(Работа с Объектами Promise в Контроллерe)

Активация объектов promise в контроллере

Размещайте стартовую начальную логику для контроллера в функции activate.

/* избегайте этого */
function Avengers(dataservice) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    dataservice.getAvengers().then(function(data) {
        vm.avengers = data;
        return vm.avengers;
    });
}
/* рекомендовано */
function Avengers(dataservice) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    activate();

    ////////////

    function activate() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }
}

Manual Annotating for Dependency Injection (Аннотация для Внедренной Зависимости)

Уязвимости от Минификации

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

/* избегайте этого - не безопасно для минификации */
angular
    .module('app')
    .controller('Dashboard', Dashboard);

function Dashboard(common, dataservice) {
}

После минификации будут производится усеченные переменные и это будет вызывать ошибки выполнения.

/* избегайте этого- не безопасно для минификации */
angular.module('app').controller('Dashboard', d);function d(a, b) { }

Определяйте Зависимости Вручную

Используйте $inject для ручного определения ваших зависимостей для AngularJS.

/* избегайте этого */
angular
    .module('app')
    .controller('Dashboard',
        ['$location', '$routeParams', 'common', 'dataservice',
            function Dashboard($location, $routeParams, common, dataservice) {}
        ]);
/* избегайте этого */
angular
  .module('app')
  .controller('Dashboard',
      ['$location', '$routeParams', 'common', 'dataservice', Dashboard]);

function Dashboard($location, $routeParams, common, dataservice) {
}
/* рекомендовано */
angular
    .module('app')
    .controller('Dashboard', Dashboard);

Dashboard.$inject = ['$location', '$routeParams', 'common', 'dataservice'];

function Dashboard($location, $routeParams, common, dataservice) {
}

Замечание:

Если функция снизу является возвращаемым значением, то $inject может быть недостижим (это может случится в директиве). Это можно решить перемещением $inject выше, чем возращаемое значение, либо использовать альтернативный синтаксис массива вставок.

Замечание:

ng-annotate 0.10.0 ввело возможность, когда $inject переносится туда, где оно доступно.

// внутри определения директивы
function outer() {
    return {
        controller: DashboardPanel,
    };

    DashboardPanel.$inject = ['logger']; // Недоступный код
    function DashboardPanel(logger) {
    }
}
// внутри определения директивы
function outer() {
    DashboardPanel.$inject = ['logger']; // Доступный код
    return {
        controller: DashboardPanel,
    };

    function DashboardPanel(logger) {
    }
}

Определяйте Маршрутные Обработчики Зависимостей Вручную

Используйте $inject, чтобы вручную определить ваш маршрутный обработчик зависимостей для компонентов AngularJS.

/* рекомендовано */
function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: moviePrepService
            }
        });
}

moviePrepService.$inject = ['movieService'];
function moviePrepService(movieService) {
    return movieService.getMovies();
}

Minification and Annotation

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

Используйте ng-annotate для Gulp или Grunt и комментируйте функции, которые нуждаются в автоматической вставке зависимостей, используйте /* @ngInject */.

angular
    .module('app')
    .controller('Avengers', Avengers);

/* @ngInject */
function Avengers(storageService, avengerService) {
    var vm = this;
    vm.heroSearch = '';
    vm.storeHero = storeHero;

    function storeHero() {
        var hero = avengerService.find(vm.heroSearch);
        storageService.save(hero.name, hero);
    }
}

Если код выше запустить через ng-annotate, то будет произведен код с аннотацией $inject, и код станет устойчив к минификации.

angular
    .module('app')
    .controller('Avengers', Avengers);

/* @ngInject */
function Avengers(storageService, avengerService) {
    var vm = this;
    vm.heroSearch = '';
    vm.storeHero = storeHero;

    function storeHero() {
        var hero = avengerService.find(vm.heroSearch);
        storageService.save(hero.name, hero);
    }
}

Avengers.$inject = ['storageService', 'avengerService'];

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

/* @ngInject */ и это будет производить корректный аннотационный код, делающий каждую вставленную зависимость безопасной для минификации.

// Используем @ngInject аннотацию
function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm',
            resolve: { /* @ngInject */
                moviesPrepService: function(movieService) {
                    return movieService.getMovies();
                }
            }
        });
}

Начиная с Angular 1.3 используйте ngApp 

директивный параметр ngStrictDi. При наличии инжектора будет создан режим "strict-di", который не даст приложению работать, если обнаружит функции, которые не используют явные аннотации (например, для защиты от минификации). Отладочная информация будет отображаться в консоли, чтобы помочь разработчику выявить код, ломающий приложение.

<body ng-app="APP" ng-strict-di>

Используйте Gulp или Grunt для ng-annotate

Используйте gulp-ng-annotate или grunt-ng-annotate для автоматических билдов. Вставляйте /* @ngInject */перед любой функцией, которая имеет зависимости.

gulp.task('js', ['jshint'], function() {
    var source = pkg.paths.js;
    return gulp.src(source)
        .pipe(sourcemaps.init())
        .pipe(concat('all.min.js', {newLine: ';'}))
        // Annotate before uglify so the code get's min'd properly.
        .pipe(ngAnnotate({
            // true helps add where @ngInject is not used. It infers.
            // Doesn't work with resolve, so we must be explicit there
            add: true
        }))
        .pipe(bytediff.start())
        .pipe(uglify({mangle: true}))
        .pipe(bytediff.stop())
        .pipe(sourcemaps.write('./'))
        .pipe(gulp.dest(pkg.paths.dev));
});

Exception Handling (Обработка Исключений)

декораторы

Используйте decorator, во время конфигурации, применяя сервис $provide, пользовательские действия будут происходить в сервисе $exceptionHandler, если произойдут исключения.

/* рекомендовано */
angular
    .module('blocks.exception')
    .config(exceptionConfig);

exceptionConfig.$inject = ['$provide'];

function exceptionConfig($provide) {
    $provide.decorator('$exceptionHandler', extendExceptionHandler);
}

extendExceptionHandler.$inject = ['$delegate', 'toastr'];

function extendExceptionHandler($delegate, toastr) {
    return function(exception, cause) {
        $delegate(exception, cause);
        var errorData = {
            exception: exception,
            cause: cause
        };
        /**
         * Здесь можно добавить ошибку в сервисную коллекцию,
         * добавить ошибки в $rootScope, логировать ошибки на удаленный сервер
         * или записывать их локально. Или просто бросить ошибку дальше.
         * Это полностью зависит от вас.
         * throw exception;
         */
        toastr.error(exception.msg, errorData);
    };
}

Обработчики Исключений

Создайте фабрику, которая предоставит интерфейс для перехвата и изящной обработки исключений.

/* рекомендовано */
angular
    .module('blocks.exception')
    .factory('exception', exception);

exception.$inject = ['logger'];

function exception(logger) {
    var service = {
        catcher: catcher
    };
    return service;

    function catcher(message) {
        return function(reason) {
            logger.error(message, reason);
        };
    }
}

Маршрутные ошибки

Обрабатывайте и логгируйте все маршрутные ошибки используя $routeChangeError

/* рекомендовано */
function handleRoutingErrors() {
    /**
     * Отмена маршрута:
     * Во время маршрутной ошибки, мы переходим на информационную панель.
     * Не забудьте реализовать выход, если происходит попытка перехода дважды.
     */
    $rootScope.$on('$routeChangeError',
        function(event, current, previous, rejection) {
            var destination = (current && (current.title || current.name || 
                                current.loadedTemplateUrl)) || 'unknown target';
            var msg = 'Error routing to ' + destination + '. ' + (rejection.msg || '');
            /**
             * Опционально мы можем записать логи, используя пользовательский 
             * сервис или $log.
             * (Не забудьте инжектировать пользовательский сервис)
             */
            logger.warning(msg, [current]);
        }
    );
}

Naming (Именования)

Рекомендации для именований

Используйте согласованные имена для всех компонентов по шаблону, который описывает особенность(feature) компонента, а затем (опционально) его тип. Я рекомендую шаблон - feature.type.js. Существует два типа имен для большинства случаев:

  • имя файла (avengers.controller.js)
  • имя компонента, которое зарегистрировано Angular (AvengersController)
/**
 * рекомендовано
 */

// контроллеры
avengers.controller.js
avengers.controller.spec.js

// сервисы/фабрики
logger.service.js
logger.service.spec.js

// константы
constants.js

// определение модуля
avengers.module.js

// маршруты
avengers.routes.js
avengers.routes.spec.js

// конфигурация
avengers.config.js

// директивы
avenger-profile.directive.js
avenger-profile.directive.spec.js
/**
 * общие настройки
 */

// Контроллеры
avengers.js
avengers.controller.js
avengersController.js

// Сервисы/Фабрики
logger.js
logger.service.js
loggerService.js
  /**
   * рекомендовано
   */
  // Controllers
  avengers.js
  avengers.spec.js

Имена Тестовых Файлов

Имя тестовой спецификации подобно имени компонента, которая его тестит, только к ней еще добавляется суффикс spec

/**
 * рекомендовано
 */
avengers.controller.spec.js
logger.service.spec.js
avengers.routes.spec.js
avenger-profile.directive.spec.js

Имена Контроллеров

Используйте согласованные имена для всех контроллеров, именованных по их характерной особенности. Используйте UpperCamelCase (ВерхнийВерблюжийРегистр) для контроллеров, так как они являются конструкторами.

/**
 * рекомендовано
 */

// avengers.controller.js
angular
    .module
    .controller('HeroAvengers', HeroAvengers);

function HeroAvengers() { }

Суффикс Имени Контроллера

Добавляйте к имени контроллера суффикс Controller или не добавляйте. Выберите одно правило и придерживайтесь его везде.

/**
 * рекомендовано: Вариант 1
 */

// avengers.controller.js
angular
    .module
    .controller('Avengers', Avengers);

function Avengers() { }
/**
 * рекомендовано: Вариант 2
 */

// avengers.controller.js
angular
    .module
    .controller('AvengersController', AvengersController);

function AvengersController() { }

Имена Фабрик

Используйте согласованные имена для всех фабрик, именуйте их по характерной особенности. Используйте camel-casing для сервисов и фабрик.

/**
 * рекомендовано
 */

// logger.service.js
angular
    .module
    .factory('logger', logger);

function logger() { }

Имена Директивных Компонент

Используйте согласованные имена для всех директив, применяя camel-case. Добавляйте короткий префикс для описания области, которой эти директивы принадлежат (иногда это может быть префикс компании, иногда префикс проекта).

/**
 * рекомендовано
 */

// avenger-profile.directive.js
angular
    .module
    .directive('xxAvengerProfile', xxAvengerProfile);

// применять так <xx-avenger-profile> </xx-avenger-profile>

function xxAvengerProfile() { }

Модули

Если разрабатываются несколько модулей, файл главного модуля будет называться app.module.js, а другие модули получат свое название по своему назначению (то что они представляют). Например, модуль администрирования будет назван admin.module.js. Соответствующие зарегистрированные имена модулей будутapp и admin.

Конфигурация

Отделяйте конфигурационную информацию от модуля в отдельном файле, называйте такой файл по названию модульного файла. Конфигурационный файл для главного app модуля называем app.config.js (или простоconfig.js). Конфигурацию для модуля admin.module.js называем соответственно admin.config.js.

Маршруты

Выделяйте конфигурацию маршрута в свой собственный файл. Примеры могут быть такими: app.route.js для главного модуля и admin.route.js для модуля admin. Даже в маленьких приложениях я предпочитаю такое разделение от остальной конфигурации.

Startup Logic

(Логика Запуска Приложения)

Конфигурация

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

angular
    .module('app')
    .config(configure);

configure.$inject =
    ['routerHelperProvider', 'exceptionHandlerProvider', 'toastr'];

function configure (routerHelperProvider, exceptionHandlerProvider, toastr) {
    exceptionHandlerProvider.configure(config.appErrorPrefix);
    configureStateHelper();

    toastr.options.timeOut = 4000;
    toastr.options.positionClass = 'toast-bottom-right';

    ////////////////

    function configureStateHelper() {
        routerHelperProvider.configure({
            docTitle: 'NG-Modular: '
        });
    }
}

Блоки Run

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

angular
    .module('app')
    .run(runBlock);

  runBlock.$inject = ['authenticator', 'translator'];

  function runBlock(authenticator, translator) {
      authenticator.initialize();
      translator.initialize();
  }

Angular $ Wrapper Services (Angular и Интерфейсные Сервисы)

$document and $window

Используйте $document и $window вместо document и window.

$timeout and $interval

Используйте $timeout и $interval вместо setTimeout и setInterval .

Testing (Тестирование)

Пишите Тесты с Историями

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

it('should have Avengers controller', function() {
    // TODO
});

it('should find 1 Avenger when filtered by name', function() {
    // TODO
});

it('should have 10 Avengers', function() {
    // TODO (mock data?)
});

it('should return Avengers via XHR', function() {
    // TODO ($httpBackend?)
});

// and so on

Библиотеки для тестирования

Используйте Jasmine или Mocha для модульного тестирования.

Движок Запуска Тестов

Используйте Karma в качестве движка для запуска тестов.

Организация Тестов

Размещайте файлы модульных тестов (specs) рядом со своим клиентским кодом. А тестовые файлы (specs), которые покрывают интеграцию с сервером или тестируют сразу несколько компонентов, в отдельной папке tests.

Animations (Анимации)

Применение

Используйте subtle анимации AngularJS чтобы перемещать состояния представлений и первичные визуальные элементы. Подключите модуль ngAnimate. Есть три ключа - тонкий (subtle), плавный (smooth), цельный (seamless).

Длительность Анимаций

Используйте короткую длительность анимаций. Я в основном начинаю с 300 миллисекунд и регулирую до нужного состояния.

animate.css

Используйте animate.css для обычных анимаций.

Comments (Комментарии)

jsDoc

Если планируется производство документации, используйте синтаксис jsDoc для документирования имен функций, описаний, параметров и возвращаемых значений. Используйте @namespace и @memberOf для соответствия структуре приложения.

/**
 * Logger Factory
 * @namespace Factories
 */
(function() {
  angular
      .module('app')
      .factory('logger', logger);

  /**
   * @namespace Logger
   * @desc Application wide logger
   * @memberOf Factories
   */
  function logger($log) {
      var service = {
         logError: logError
      };
      return service;

      ////////////

      /**
       * @name logError
       * @desc Logs errors
       * @param {String} msg Message to log
       * @returns {String}
       * @memberOf Factories.Logger
       */
      function logError(msg) {
          var loggedMsg = 'Error: ' + msg;
          $log.error(loggedMsg);
          return loggedMsg;
      };
  }
})();

Constants (Константы)

Глобальные Переменные Сторонних Производителей (Vendors)

Создайте константы Angular для глобальных переменных из библиотек сторонних производителей.

// constants.js

/* global toastr:false, moment:false */
(function() {
    'use strict';

    angular
        .module('app.core')
        .constant('toastr', toastr)
        .constant('moment', moment);
})();

Используйте константы для значений, которые не изменяются и не приходят из другого сервиса. Если константы используются в модуле, который может быть использован в нескольких приложениях, то поместите константу в файле, названному по имени модуля. В противном случае держите константы в главном модуле в файле constants.js.

// Константы используются во всем приложении
angular
    .module('app.core')
    .constant('moment', moment);

// Константы используются в модуле продаж
angular
    .module('app.sales')
    .constant('events', {
        ORDER_CREATED: 'event_order_created',
        INVENTORY_DEPLETED: 'event_inventory_depleted'
    });

Routing (Маршрутизация)

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

 

Используйте AngularUI Router для клиентской маршрутизации.

 

Определяйте маршруты для всех представлений в модуле, где они есть. Каждый модуль должен содержать маршруты для всех своих представлений.

Task Automation (Автоматизация)

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

 

Используйте автоматизацию для сборки всех файлов с определениями модуля *.module.js перед всеми остальными JavaScript-файлами приложения.

var clientApp = './src/client/app/';

// Всегда собираем файлы модулей первыми
var files = [
  clientApp + '**/*.module.js',
  clientApp + '**/*.js'
];

Useful (Полезное)

Полезные штучки для этого руководство

Исходник на англ.

Исходник на русском

Angular Style Guide v1

By Shuhratbek Mamadaliyev

Angular Style Guide v1

  • 1,678