Руководство по стилю программирования и оформления приложений на AngularJS

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

Отличное сообщество, которому следует отдать должное

Никогда не работайте в одиночку. Лично я считаю, что сообщество AngularJS является отличным источником полезной информации. Хочу отметить, что над этим руководством также работал специалист по AngularJS Todd Motto. Во многих пунктах мы сходились во мнении, хотя были и разногласия. В его статье вы увидите его подход к данному вопросу.

Большинство моих правил по написанию кода были выработаны после работы с Ward Bell. Конечно не во всем мы соглашались, но все же он оказал значительное влияние на формирование моего стиля.

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

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

Принцип единой ответственности

  • Указывайте только один компонент в пределах одной строки.

В следующем примере мы создаем модуль app с зависимостями, задаем контролер, фабрику и все это делается на одной строке.

/* избегайте */
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

  • IIFE - преобразует компоненты AngularJS в немедленно выполняемые функции.

Зачем? - Таким образом мы избавляемся от глобальных переменных. Все переменные и функции находятся в памяти только на время своего выполнения, что так же помогает избежать проблем с одноименными переменными.

Зачем? - При сокращении вашего кода для размещения его на рабочем сервере вы можете столкнуться с большим количеством глобальных переменных, чьи имена могут совпадать. IIFE избавляет вас от таких проблем, путем ограничения видимости переменных.

/* избегайте */
// 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() { }
})();
  • Для краткости во всех остальных примерах IIFE синтаксис может быть опущен.
  • Такой синтаксис не позволяет юнит тестам получить доступ к частным переменным (например, к регулярным выражениям или вспомогательным функциям), что усложняет процесс тестирования. Тем не менее можно либо осуществить тесты через доступные переменные, либо открыть доступ к необходимым переменным путем создания отдельных компонентов. Например, разместить регулярные выражения, вспомогательные функции или константы в отдельную фабрику.

Модули

  • используйте только уникальные имена и разделители для подмодулей.

Зачем? При таком подходе вы гарантированно защищаете себя от проблем, связанных с повторными именами. А разделители помогут легко вам ориентироваться в иерархии модулей. Например, app - корневой модуль, app.dashboard и app.users могут быть модулями, которые используются в app в качестве зависимостей.

Сеттеры

  • объявляйте модули без переменной используя синтаксис сеттеров.

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

/* избегайте */
var app = angular.module('app', [
   'ngAnimate',
   'ngRoute',
   'app.shared',
   'app.dashboard'
]);

Лучше сделать следующим образом:

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

Геттеры

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

Зачем? Таким образом вы пишите более читаемый код и избегаете конфликта имен переменных.

/* избегайте */

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’);, чтобы получить к нему доступ.

Именованные / Анонимные функции

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

Зачем? Упрощается чтение кода и отладка, а также сокращает вложенность кода.

/* избегайте */
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() { }

Контролеры

controllerAs синтаксис в шаблонах

  • используйте controllerAs синтаксис вместо классического синтаксиса со $scope.

Зачем? Контролеры создаются, обновляются и используются в качества одного экземпляра, а синтаксис controllerAs ближе к JavaScript конструкторам, чем классический синтаксис со $scope.

Зачем? Способствует к использованию точечной нотации объекта в шаблоне (то есть customer.name вместо name), что выглядит более понятно и помогает избежать путаницы с именами.

Зачем? Помогает избежать вызовов $parent в шаблоне с вложенными контролерами.

<!-- избегайте -->
<div ng-controller="Customer">
   {{ name }}
</div>

<!-- рекомендовано -->
<div ng-controller="Customer as customer">
  {{ customer.name }}
</div>

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

  • Используйте controllerAs синтаксис вместо классического стиля со $scope синтаксисом.
  • При таком синтаксисе вы будет использовать this внутри контроллёра, что помогает ограничить доступ к внутренним переменным.

Зачем? ControllerAs синтаксис более удобен в использовании. Вы также можете привязаться к шаблону и использовать методы из $scope.

Зачем? Помогает избегать использования $scope методов в контролере в те моменты, когда их лучше вообще избежать или перенести в фабрику. Используйте $scope в фабрике или контролере только при необходимости. Например, при использовании подписчиков событий - $emit, $broadcast или $on, лучше размещайте их в фабрике и вызывайте из контролера.

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

controllerAs с vm

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

Зачем? this - переменная контекста, которая может изменяться в зависимости от места вызова. Такой подход помогает избежать подобных проблем.

/* избегайте */
function Customer() {
   this.name = {};
   this.sendMessage = function() { };
}
/* рекомендовано */
function Customer() {
   var vm = this;
   vm.name = {};
   vm.sendMessage = function() { };
}
  • Сообщений jshint можно избежать разместив следующий комментарий над строкой кода:
/* jshint validthis: true */
var vm = this;
  • При создании точек останова при изменении данных с помощью синтаксиса controllerAs, используйте vm.* со следующим синтаксисом (создавайте такие точки аккуратно, так как они ресурсоемкие):
$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() {
       /** 
         * lines 
         * of
         * code
         * affects
         * readability
         */
   };
   vm.search = search;
   vm.sessions = [];
   vm.title = 'Sessions';
/* рекомендовано */
function Sessions(dataservice) {
   var vm = this;

   vm.gotoSession = gotoSession;
   vm.refresh = dataservice.refresh; // 1 liner is OK
   vm.search = search;
   vm.sessions = [];
   vm.title = 'Sessions';

Объявляйте функции для скрытия деталей логики

  • размещайте связанные переменные вверху файла. Если вам требуется привязать функцию в контролере, то указывайте на объявление функции сделанное ниже в файле.

Зачем? Расположение связанных переменных вверху помогает легко определить какие переменные можно использовать в шаблоне.

Зачем? Размещение основной логики ниже, помогает сначала увидеть опорные точки кода.

Зачем? Так как объявления функций подняты вверх, можно не беспокоится за то, что вызов функции произойдет до её объявления.

Зачем? Вам не придется задумываться не сломается ли ваш код, если вы объявляете функцию, которая переносит var a до var b, так как a зависит от b.

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

/** 
 * избегайте 
 * Using 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();
}
  • Обратите внимания на то, что предыдущем примере, все важные моменты кода разбросаны.
  • В примере ниже мы переместили их наверх, а именно, vm.avengers, vm.title. А все остальное разместили ниже, так просто удобнее читать код.
/*
 * рекомендовано
 * Using function declarations
 * and bindable members up top.
 */
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) {
   var vm = this;
   vm.checkCredit = checkCredit;
   vm.total = 0;

   function checkCredit() { 
       var orderTotal = vm.total;
       return $http.get('api/creditcheck').then(function(data) {
           var remaining = data.remaining;
           return $q.when(!!(remaining > orderTotal));
       });
   };
}
/* рекомендовано */
function Order(creditService) {
   var vm = this;
   vm.checkCredit = checkCredit;
   vm.total = 0;

   function checkCredit() { 
      return creditService.check();
   };
}

Разделяйте контролеры

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

Зачем? Использование одного контролера в нескольких шаблонах приводит к нестабильности и усложняет процесс тестирования приложения.

Назначение контролера

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

Зачем? объединение контролера в маршруте позволяет разным маршрутам вызывать разные пары контролеров и шаблонов. Если контролер задан при помощи ng-controller, то этот шаблон навсегда привязан к одному маршруту.

/* избегайте - when using with a route and dynamic pairing is desired */

// 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>

Службы

Одиночки (Singleton)

  • создание служб происходит при помощи ключевого слова new, а this используется для доступа к открытым методам и свойствам. Так как такой подход очень похож на использование фабрик, то почему бы не использовать их.
  • Все службы AngularJS - одиночки. Т.е. создается только один экземпляр службы.
// 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) {
         /* */
       }
  };
}

Фабрики

  • фабрики должны отвечать за одну функцию. Как только вам требуется новый функционал, лучше создать новую фабрику.
  • фабрики - singleton, возвращающие объект, который содержит свойства и методы службы.
  • Все службы AngularJS - одиночки
  • располагайте публичные свойства и методы службы вверху файла.

Зачем? При таком расположении вы сразу видите какие методы службы следует вызывать и использовать при тестировании.

Зачем? Так же это очень удобно, особенно, когда файл становится достаточно большим, так как не приходится прокручивать его до конца.

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

/* избегайте */
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() { 
       /* */
   };
}
  • Таким образом, привязки отображаются в объекте, а примитивные значения не могут обновляться одновременно при использовании паттерна раскрывающийся модуль.

Объявление функций для скрытия деталей реализации

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

Зачем? Такое размещение свойств и методов фабрики упрощает чтение кода и вы сразу же видите, какие функции следует использовать.

Зачем? Вся сложная логика переносится вниз, оставляя вверху только основные моменты.

Зачем? Объявления функций подняты вверх, поэтому вы можете не переживать за то, что вызовите функцию до её описания.

Зачем? Вам не придется задумываться не сломается ли ваш код, если вы объявляете функцию, которая переносит var a до var b, так как a зависит от b.

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

/**
 * избегайте
 * Использование функции выражения
 */
function dataservice($http, $location, $q, exception, logger) {
   var isPrimed = false;
   var primePromise;

   var getAvengers = function() {
      // implementation details go here
   };

   var getAvengerCount = function() {
       // implementation details go here
   };

   var getAvengersCast = function() {
      // implementation details go here
   };

   var prime = function() {
      // implementation details go here
   };

   var ready = function(nextPromises) {
       // implementation details go here
   };

   var service = {
       getAvengersCast: getAvengersCast,
       getAvengerCount: getAvengerCount,
       getAvengers: getAvengers,
       ready: ready
   };

   return service;
}
/**
 * рекомендовано
 * Использование описания функций
 * и доступные члены сверху.
 */
function dataservice($http, $location, $q, exception, logger) {
   var isPrimed = false;
   var primePromise;

   var service = {
       getAvengersCast: getAvengersCast,
       getAvengerCount: getAvengerCount,
       getAvengers: getAvengers,
       ready: ready
   };

   return service;

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

   function getAvengers() {
      // реализация функции
   }

   function getAvengerCount() {
       // реализация функции
   }

   function getAvengersCast() {
      // реализация функции
   }

   function prime() {
       // реализация функции
   }

   function ready(nextPromises) {
       // реализация функции
   }
}

Службы данных

Разделяйте обращения к данным

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

Зачем? Контролер отвечает за сбор информации и обработку шаблона. В нем не должно быть логики обработки данных. Размещения операций обработки данных в службе значительное упрощает контролер.

Зачем? При таком подходе тестирование кода значительно упрощается.

Зачем? Служба данных может содержать в себе специфическую логику обработки данных.

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

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

// dataservice factory
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);
       }
   }
}
  • Вызов службы данных происходит от потребителя, например, контролера, тем самым скрывая саму логику от него.
/* рекомендовано */

// controller calling the dataservice factory
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 из вызывающей функции.

Зачем? Вы можете выстраивать цепочки из объектов promise, после чего выполнить какую-то необходимую операцию.

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

activate();

function activate() {
   /**
     * Step 1
     * Ask the getAvengers function for the
     * avenger data and wait for the promise
     */
   return getAvengers().then(function() {
       /**
         * Step 4
         * Perform an action on resolve of final promise
         */
       logger.info('Activated Avengers View');
   });
}

function getAvengers() {
     /**
       * Step 2
       * Ask the data service for the data and wait
       * for the promise
       */
     return dataservice.getAvengers()
         .then(function(data) {
             /**
               * Step 3
               * set the data and resolve the promise
               */
             vm.avengers = data;
             return vm.avengers;
     });
}

Директивы

  • создавайте одну директиву в пределах одного файла. Называйте файл в соответствии с именем директивы.

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

Зачем? При таком подходе упрощается отладка.

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

angular
   .module('app.widgets')

   /* order directive that is specific to the order module */
   .directive('orderCalendarRange', orderCalendarRange)

   /* sales directive that can be used anywhere across the sales app */
   .directive('salesCustomerInfo', salesCustomerInfo)

   /* spinner directive that can be used anywhere across apps */
   .directive('sharedSpinner', sharedSpinner);

function orderCalendarRange() {
   /* implementation details */
}

function salesCustomerInfo() {
   /* implementation details */
}

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

/**
 * @desc order directive that is specific to the order module at a company named Acme
 * @example <div acme-order-calendar-range></div>
 */
angular
   .module('sales.order')
   .directive('acmeOrderCalendarRange', orderCalendarRange);

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

/**
 * @desc spinner directive that can be used anywhere across the sales app at a company named Acme
 * @example <div acme-sales-customer-info></div>
 */    
angular
   .module('sales.widgets')
   .directive('acmeSalesCustomerInfo', salesCustomerInfo);

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

/**
 * @desc spinner directive that can be used anywhere across apps at a company named Acme
 * @example <div acme-shared-spinner></div>
 */
angular
   .module('shared.widgets')
   .directive('acmeSharedSpinner', sharedSpinner);

function sharedSpinner() {
   /* implementation details */
}
  • Существует довольно много варианов именования директив, особенно, если брать в рассмотрение то, что они могут быть использованы как при узкой, так и широкой области видимости. Используйте ту, которая четко отображает иерархию. Я приведу несколько примеров ниже, а так же советую обратиться к разделу про имена для наглядности.

Сокращайте изменения DOM

  • при прямом изменении объектов DOM используйте директивы. Если есть возможность использовать другие методы изменения, например, CSS, службы анимации, шаблонизатор AngularJS (ngShow, ngHide), используйте их. К примеру, если директива просто прячет и показывает объект, то лучше воспользуйтесь ngShow/ngHide.

Зачем? Изменения в DOM довольно сложно тестировать, отлаживать, и, как правило, существуют более удобные методы изменения. (css, анимации, шаблоны).

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

  • используйте короткую, уникальную и понятную приставку, как, например, acmeSalesCustomer, которая объявлена в html следующим образом - acme-sales-customer-info.

Зачем? Такой подход четко задает отделяет и задает происхождение директивы. Например, приставка cc- может указывать на то, что директива является частью приложения CodeCamper, в то время как, acme- может быть директивой компании Acme.

  • избегайте использования ng-, так как это зарезервированное слово AngularJS. Так же всегда проверяйте не нарушаете ли вы других имен, например, ion- - Ionic Framework.

Ограничивайте до элементов и атрибутов

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

Зачем? Так просто логичнее.

Зачем? Хотя мы можем использовать директиву в качестве класса, если директива выполняет роль элемента, то её лучше использовать как элемент или, хотя бы, как атрибут.

  • EA устанавливаются по-умолчанию в AngularJS 1.3+
!-- избегайте -->
<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

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

Зачем? Это логично и легко реализуется.

  • Следующая директива показывает один из способов использования области видимости внутри ссылки и контроллерной директивы, при использовании controllerAs.
<div my-example max="77"></div>
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;

   ExampleController.$inject = ['$scope'];
   function ExampleController($scope) {
       // Injecting $scope just for comparison
       /* jshint validthis:true */
       var vm = this;

       vm.min = 3; 
       vm.max = $scope.max; 
       console.log('CTRL: $scope.max = %i', $scope.max);
       console.log('CTRL: vm.min = %i', vm.min);
       console.log('CTRL: vm.max = %i', vm.max);
   }

   function linkFunc(scope, el, attr, ctrl) {
       console.log('LINK: scope.max = %i', scope.max);
       console.log('LINK: scope.vm.min = %i', scope.vm.min);
       console.log('LINK: scope.vm.max = %i', scope.vm.max);
   }
}
<!-- example.directive.html -->
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="vm.max"/></div>
<div>min={{vm.min}}<input ng-model="vm.min"/></div>

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

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;
       });
   }
}

Promise разрешения маршрута

  • если контроллер зависит от какого-либо promise, то запускайте эти зависимости в $routeProvider до запуска основной логики контроллера. Если вам необходимо, в зависимости от какого либо условия, отменить маршрут до запуска контролера, используйте разрешение маршрутов.

Зачем? Иногда данные требуются до загрузки контролера. Они могут быть получены при помощи promise из клиентской фабрики или $http. Использование расшифровщика маршрутов позволяет определение promise до запуска логики контролера, который, в свою очередь может строить свою логику в зависимости от полученных данных.

/* избегайте */
angular
   .module('app')
   .controller('Avengers', Avengers);

function Avengers(movieService) {
   var vm = this;
   // unresolved
   vm.movies;
   // resolved asynchronously
   movieService.getMovies().then(function(response) {
       vm.movies = response.movies;
   });
}
/* рекомендовано */

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

function config($routeProvider) {
   $routeProvider
       .when('/avengers', {
           templateUrl: 'avengers.html',
           controller: 'Avengers',
           controllerAs: 'vm',
           resolve: {
               moviesPrepService: function(movieService) {
                   return movieService.getMovies();
               }
           }
       });
}

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

Avengers.$inject = ['moviesPrepService'];
function Avengers(moviesPrepService) {
     /* jshint validthis:true */
     var vm = this;
     vm.movies = moviesPrepService.movies;
}
  • Пример зависимости от movieService не является безопасным для минификации кода. Для подробностей обратите внимания на раздел “Внедрение зависимостей” и “Минификация кода и аннотации”.

Аннотация внедрения зависимостей

Обезопасьте себя от минификации кода

  • избегайте использования сокращенного синтаксиса объявления зависимостей без учета минификации кода.

Зачем? Параметры компонента (контролер, фабрика и т.д.) будут конвертированы в некорректные переменные. Например, common и dataservice, могут стать a или b и AngularJS не сможет их определить.

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

function Dashboard(common, dataservice) {
}
  • Из такого кода могут получиться некорректные переменные, которые вызовут ошибки.
/* избегайте - не безопасно для минификации */
angular.module('app').controller('Dashboard', d);function d(a, b) { }

Объявляйте зависимости вручную

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

Зачем? Такой подход полностью соответствует технологии использованной в ng-annotate, что поможет обезопасить вас от проблем с минификацией кода. Если ng-annotate видит, что зависимость уже была подключена, то она игнорирует повторные её подключения.

Зачем? Таким образом вы избегаете опасностей связанных с безопасностью при минификации кода и изменении имен переменных. Например, common и dataservice, могут стать a или b и AngularJS не сможет их определить.

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

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

/* avoid */
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) {
}
  • Если функция располагается ниже return, то $inject может быть недоступен (такое обычно случается в директивах). Вы можете либо передвинуть $inject выше return или использовать альтернативный синтаксис внедрения массива.
  • 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.

Зачем? Такой подход убирает анонимную функцию преобразователя маршрутов, что упрощает чтение кода.

Зачем? Выражение $inject может находится перед преобразователя, чтобы обезопасить вас от проблем связанных с минификацией кода.

/* рекомендовано */
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();
}

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

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

Зачем? Это обезопасит вас от проблем с минификацией кода тех зависимостей, которые не приняли во внимание все аспекты минификации.

Зачем? ng-min является устаревшим выражением.

  • Я предпочитаю использовать Gulp, так как его проще писать, читать и отлаживать.
  • Следующий код используют технику безопасной минификации.
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'];
  • Если ng-annotate определит, что подключение зависимости уже было произведено (т.е. присутствует @ngInject), то $inject код не будет повторен.
  • При использовании расшифровщика маршрутов, можно добавлять /* @ngInject */ перед его функцией. В результате вы получите правильно проставленные аннотации и избежите проблем с минификацией.
// Using @ngInject annotations
function config($routeProvider) {
   $routeProvider
       .when('/avengers', {
           templateUrl: 'avengers.html',
           controller: 'Avengers',
           controllerAs: 'vm',
           resolve: { /* @ngInject */
               moviesPrepService: function(movieService) {
                   return movieService.getMovies();
               }
           }
       });
}
  • Начиная с AngularJS 1.3 используйте параметр ngStrictDi директивы ngApp. При его использовании все зависимости будут созданы в режиме strict-di, тем самым вызывая ошибку при запуске функций без аннотаций (так как это небезопасно при минификации).Отладочная информация отобразиться в консоли, где будет указано на проблемный участок кода. <body ng-app="APP" ng-strict-di>

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

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

Зачем? ng-annotate отслеживает большинство зависимостей, но иногда полезно указывать /* @ngInject */.

  • Следующий пример демонстрирует задачу Gulp и использованием ngAnnotate
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));
});

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

Декораторы

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

Зачем? Таким образом вы задаете постоянный способ отслеживания исключений, которые не обрабатывает AngularJS.

  • Так же можно переопределить службу, а не использовать декоратор. Такой вариант вполне подходит, но если вы хотите сохранить поведение AngularJS по-умолчанию, то лучше использовать декоратор.
/* рекомендовано */
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 
       };
       /**
         * Could add the error to a service's collection,
         * add errors to $rootScope, log errors to remote web server,
         * or log locally. Or throw hard. It is entirely up to you.
         * throw exception;
         */
       toastr.error(exception.msg, errorData);
   };
}

Отслеживание исключений

  • создайте фабрику которая задаёт интерфейс для отслеживания и обработки исключений.

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

  • Такой обработчик особенно хорош для отслеживания и обработки специфических исключений, которые могут возникнуть в критических местах ваших функций. Например, при вызове XHR для получения данных от удаленной веб службы, в которой вы хотели бы отследить возможные исключения и отреагировать на них.
/* рекомендовано */
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() {
   /**
     * Route cancellation:
     * On routing error, go to the dashboard.
     * Provide an exit clause if it tries to do it twice.
     */
   $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 || '');
           /**
             * Optionally log using a custom service or $log.
             * (Don't forget to inject custom service)
             */
           logger.warning(msg, [current]);
       }
   );
}

Присваивание имен

Правила присваивания имен

  • используйте логику в присваивании имен для своих компонентов. Придерживайтесь таких имен, которые бы давали представление о функционале компонента и, возможно, его типе. Я рекомендую использовать шаблон функицонал.тип.js. Существует два имени для большинства ресурсов:
    • имя файла (avengers.controller.js)
    • зарегистрированное имя компонента Angular (AvengersController)

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

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

Имена функциональных файлов

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

Зачем? Обеспечивает возможность быстро понять назначение файла. Зачем? Обеспечивает соответствие шаблону при автоматизации задач.

/**
 * common options 
 */

// Controllers
avengers.js
avengers.controller.js
avengersController.js

// Services/Factories
logger.js
logger.service.js
loggerService.js

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

// controllers
avengers.controller.js
avengers.controller.spec.js

// services/factories
logger.service.js
logger.service.spec.js

// constants
constants.js

// module definition
avengers.module.js

// routes
avengers.routes.js
avengers.routes.spec.js

// configuration
avengers.config.js

// directives
avenger-profile.directive.js
avenger-profile.directive.spec.js

Другой подход

  • именование файлов контролера без слова controller, то есть avengers.js вместо avengers.controller.js. Все остальные правила придерживаются использования суффикса, отвечающего за тип компонента. Так как контролер это самый часто используемый объект, то вы его узнаете всегда, так что таким образом мы экономим немного времени. Я советую, определить для себя набор правил и следовать им.
/**
 * рекомендовано
 */
// Controllers
avengers.js
avengers.spec.js

Имена для файлов тестов

  • называйте их так же, как и компонент, который они тестируют, добавив суффикс spec.

Зачем? Обеспечивает возможность быстро понять назначение компонента. Зачем? Соответствует шаблону для работы с karma и другими обработчиками тестов.

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

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

  • используйте постоянную политику имен, описывающих функционал контролера. Используйте ГорбатыйСтильРегистра так как ваши контролеры создаются при помощи контрукторов.

Зачем? Обеспечивает возможность быстро понять назначение компонента. Зачем? ГорбатыйСтильСБольшойБуквы - принятый подход называть объекты, которые могут быть созданы при помощи конструктора.

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

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

function HeroAvengers(){ }

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

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

Зачем? Обычно суффикс все такие используется, так как это более наглядно. Зачем? Пропуск суффикса сокращает название контролера и ничуть не ухудшает восприятия имени.

/**
 * рекомендовано: Option 1
 */

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

function Avengers(){ }

/**
 * рекомендовано: Option 2
 */

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

function AvengersController(){ }

Имена фабрик

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

Зачем? обеспечивает быстрое восприятие фабрик.

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

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

function logger(){ }

Имена директив компонента

  • используйте постоянные правила для всех имен директив. Используйте короткую приставку для описания области действия директивы. (приставка с именем компании или проекта).

Зачем? помогает быстро находить и обращаться к компонентам.

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

// 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.js.

Зачем? Приложение с одним модулем может быть названо app.js. Так как это и есть приложение.

Зачем? Обеспечивает постоянность для приложений с несколькими модулями, а также упрощает процесс расширения приложения.

Зачем? Помогает создавать автоматизированный процесс загрузки определения модулей до загрузки остальных файлов angular.

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

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

Зачем? Отделение настроек модуля от основного кода. Зачем? Задает строгое место для хранения настроек.

Маршруты

  • Выносите настройку маршрутов в отдельный файл. Например, app.routes.js для главного модуля и admin.route.js для модуля admin. Я предпочитаю такое разделение даже в небольших приложениях. Как вариант, можно использовать более длинные имена, например, admin.config.route.js.

Структура приложения, принцип LIFT

  • структурируйте свое приложение таким образом, чтобы легко можно было найти нужный участок кода (L - locate), определить назначение файла (I - identify), не усложняйте структуру (F - flat), старайтесь не повторять себя (Try to stay DRY). Следуют соблюдать 4 базовых правила.

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

Когда начинаю сомневаться в своей структуре, я обращаюсь к четырем базовым принципам LIFT:

  1. Возможность легко найти нужный код
  2. Понять назначение файла
  3. Поддерживать наиболее простую структуру проекта
  4. Не повторять свой код

Место расположения

  • находите код интуитивно

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

/bower_components
/client
 /app
   /avengers
   /blocks
     /exception
     /logger
   /core
   /dashboard
   /data
   /layout
   /widgets
 /content
 index.html
.bower.json

Определение назначения файла

  • из названия файла вы должны сразу понимать его назначение.

Зачем? Вы тратите гораздо меньше времени на разбирание кода и работаете намного быстрее. И ничего страшного даже если для этого вам потребуются более длинные имена файлов.

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

Простая структура

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

Зачем? Никому не хочется проходить 7 каталогов вглубь, чтобы добраться до одного файла. Подумайте о меню на сайтах… Стоит несколько раз подумать прежде чем разместить что-то глубже второго уровня. Не существует жесткого правила по отношению к каталогам, но стоит задуматься, если в вашей директории находится больше 7-10 файлов. Делайте так, как вам удобно. Не усложняйте структуру проекта без причины.

Не повторяйте свой код

  • никогда не повторяйте свой код, до тех пор пока это не ухудшает читаемость кода.

Зачем? Такой подход немаловажен, но не стоит быть фанатичным и нарушать другие принципы LIFT. Я не хотел бы набирать session-view.html для вида, так как и так понятно что это вид. Если бы это не было ясно из принятых правил, то тогда есть смысл употреблять такое имя.

Структура приложения

Общие правила

  • всегда держите в уме не только то, что надо реализовать сейчас, но и смотрите вперед.

Другими словами, начинайте с малого, но помните куда ваше приложение движется. Весь код приложения хранится в каталоге app. Все содержимое - 1 функционал на файл. Каждый контроллер, служба, модуль и шаблон должны находится в своих файлах. Все сторонние библиотеки должны располагаться в другом каталоге, не в app. Не я их писал, и я не хочу чтобы они находились внутри моего приложения (bower_components, scripts, lib).

  • Более подробно о структуре проекта вы можете в оригинальной статье о структуре.

Макет страницы

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

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

Каталог на один функционал

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

Зачем? Разработчик легко найдет нужный код, определит его назначение при виде файла, проще структуры быть не может, а также отсутствуют какие-либо повторения. Зачем? Принципы LIFT полностью соблюдены. Зачем? Предотвращает засорение проекта и придерживает проект в рамках LIFT. Зачем? Когда файлов больше 10 в одном каталоге, то их проще находить по подкаталогам, чем в одной директории.

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

app/
   app.module.js
   app.config.js
   app.routes.js
   components/       
       calendar.directive.js  
       calendar.directive.html  
       user-profile.directive.js  
       user-profile.directive.html  
   layout/
       shell.html      
       shell.controller.js
       topnav.html      
       topnav.controller.js       
   people/
       attendees.html
       attendees.controller.js  
       speakers.html
       speakers.controller.js
       speaker-detail.html
       speaker-detail.controller.js
   services/       
       data.service.js  
       localstorage.service.js
       logger.service.js   
       spinner.service.js
   sessions/
       sessions.html      
       sessions.controller.js
       session-detail.html
       session-detail.controller.js 

  • Не используйте структурирование по принципу каталог-по-типу. Таким образом вам придется часто переходить из каталога в каталог при работе над одним функционалом. А когда ваше приложение разрастается до 5-10-25 шаблонов и контроллеров, то это вообще приводит к невозможности что-либо быстро найти.
/* 
* избегайте, "каталог по типу".
* Я рекомендую "каталог по назначению".
*/

app/
   app.module.js
   app.config.js
   app.routes.js
   controllers/
       attendees.js            
       session-detail.js       
       sessions.js             
       shell.js                
       speakers.js             
       speaker-detail.js       
       topnav.js               
   directives/       
       calendar.directive.js  
       calendar.directive.html  
       user-profile.directive.js  
       user-profile.directive.html  
   services/       
       dataservice.js  
       localstorage.js
       logger.js   
       spinner.js
   views/
       attendees.html     
       session-detail.html
       sessions.html      
       shell.html         
       speakers.html      
       speaker-detail.html
       topnav.html   

Модульность

Много небольших независимых модулей

  • создавайте небольшие модули отвечающие за одну функцию.

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

Создавайте модуль App

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

Зачем? AngularJS выступает за модульность и разделение проблем. Создание одного корневого модуля, чья роль подключить все остальные модули, позволяет очень легко добавлять и исключать модули из приложения.

Не загромождайте модуль App

  • вкладывайте в него только сбор других модулей, не размещайте там никакой другой логики.

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

Область функционала и модули

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

Зачем? Независимые модули легко добавляются в проект. Зачем? Спринты или итерации могут уделить внимание на области функционала и включать их по окончанию своей работы.

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

Повторно используемые блоки - модули

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

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

Зависимости модуля

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

Зачем? Главный модуль содержит в себе полное описание необходимых модулей. Зачем? Функционал легко распространять между приложениями. Такой функционал, как правило, зависит от основных модулей, которые одинаковы для всех приложений и соединены в один модуль (например, app.core).

Зачем? Функционал, используемый в разных приложениях, например, открытые службы данных, легко обнаружить и открыть в пределах app.core (можете назвать этот модуль как вам больше нравится).

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

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

Служба врапперы $ в angular

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

Зачем? Эти службы обрабатываются Angular и тестировать их гораздо проще, чем document и window.

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

Зачем? Эти службы обрабатываются Angular, гораздо проще поддаются тестированию и сохраняют синхронизацию данных.

Тестирование

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

Пишите тесты для сценариев

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

Зачем? Написание описаний теста помогает четко понять назначение теста и как оценить его результат.

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 для юнит тестирования.

Зачем? Обе библиотеки широко распространены среди сообщества AngularJS, довольно стабильны, хорошо поддерживаемые и предоставляют широкий спектр возможностей. При использовании Mocha, рассмотрите дополнительную библиотеку Chai.

Запуск тестов

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

Зачем? Легко настроить для одноразового запуска или для запуска каждый раз при изменении кода.

Зачем? Она легко встраивается в ваш процесс интеграции своими средствами или при помощи Grunt или Gulp.

Зачем? Некоторые IDE предлагают встроенную поддержку Karma - WebStorm, Visual Studio. Зачем? Она отлично работает с лидерами автоматизации задач -Grunt, Gulp.

Отслеживание работы кода

  • используйте Sinon для слежения за кодом.

Зачем? Отлично работает в связке с Jasmine и Mocha и расширяет их возможности. Зачем? Позволяет легко переключаться между Jasmine и Mocha при желании попробовать оба.

Браузер без заголовков

  • используйте PhantomJS для запуска тестов на сервере.

Зачем? Это беззаголовочный браузер, который позволяет исполнять тесты без использования “визуального” браузера. То есть вам не потребуется устанавливать Chrome, Safari, IE или другие браузеры на свой сервер.

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

Анализ кода

  • используйте JSHint.

Зачем? Тестируйте свой код. Это библиотека поможет вам выделить узкие места вашего кода.

  • Дайте доступ к глобальным переменным для тестирования при помощи JSHint: облегчите условия работы тесты и дайте ему доступ к таким переменным, как describe и expect.

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

/* global sinon, describe, it, afterEach, beforeEach, expect, inject */

Анимации

  • используйте легковесную анимацию AngularJS для построения переходов между состояниями для видов и основных визуальных эффектов. Подключите модуль ngAnimate. Анимация должна быть легковесной, гладкой и ненавязчивой.

Зачем? Такая анимация улучшает восприятие пользователем при корректном применении.

  • используйте краткосрочную анимацию. Я, обычно, начинаю с 300 мс и настраиваю этот параметр по необходимости.

Зачем? Длительная анимацию ухудшает восприятие её пользователем и создает эффект низкой производительности.

animate.css

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

Зачем? Анимации, предлагаемые animate.css быстрые, гладкие и легко применяемые в приложении.

Зачем? Добавляет постоянность вашим анимациям. Зачем? Очень распространенная библиотека.

Комментарии

  • если вы планируете генерировать документацию, используйте синтаксис 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;
     };
 }
})();

JSHint

  • используйте JS Hint для облегчения работы с JavaScript. Не забудьте отредактировать файл настроек JS Hint и включить его в систему контроля версий.

В документации по JS Hint вы найдете описание всех опций.

Зачем? Обеспечивает наличие предупреждений перед тем как отправить коммит в вашу систему контроля версий.

Зачем? Обеспечивает стабильность в пределах вашей команды.

{
   "bitwise": true,
   "camelcase": true,
   "curly": true,
   "eqeqeq": true,
   "es3": false,
   "forin": true,
   "freeze": true,
   "immed": true,
   "indent": 4,
   "latedef": "nofunc",
   "newcap": true,
   "noarg": true,
   "noempty": true,
   "nonbsp": true,
   "nonew": true,
   "plusplus": false,
   "quotmark": "single",
   "undef": true,
   "unused": false,
   "strict": false,
   "maxparams": 10,
   "maxdepth": 5,
   "maxstatements": 40,
   "maxcomplexity": 8,
   "maxlen": 120,

   "asi": false,
   "boss": false,
   "debug": false,
   "eqnull": true,
   "esnext": false,
   "evil": false,
   "expr": false,
   "funcscope": false,
   "globalstrict": false,
   "iterator": false,
   "lastsemic": false,
   "laxbreak": false,
   "laxcomma": false,
   "loopfunc": true,
   "maxerr": false,
   "moz": false,
   "multistr": false,
   "notypeof": false,
   "proto": false,
   "scripturl": false,
   "shadow": false,
   "sub": true,
   "supernew": false,
   "validthis": false,
   "noyield": false,

   "browser": true,
   "node": true,

   "globals": {
       "angular": false,
       "$": false
   }
}

Константы

Глобальные переменные сторонних продуктов

  • создайте Angular JS Contstant для всех переменных от сторонних библиотек в вашем проекте.

Зачем? Позволяет внедрить сторонние библиотеки, которые можно использовать в любом месте вашего проекта. Облегчает тестирование, так как вы точно знаете какие зависимости используют ваши компоненты (избегаете незаметных подключений зависимостей). Так же такой подход позволяет легко повторить такой набор зависимостей по необходимости.

// constants.js

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

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

Шаблоны файлов и сниппеты

Используйте шаблоны файлов или сниппеты для поддержания единого стиля в коде. Я приложу несколько шаблонов и/или сниппетов для некоторых распространенных IDE.

  • Sublime Text: AngularJS сниппеты с соблюдением всех стилей и правил.
    • Загрузите сниппеты
    • Разместите их в каталоге пакетов
    • Перезапустите Sublime
    • В JavaScript файле введите следующие команды, разделив их табуляцией.
  • Visual Studio: AngularJS сниппеты с соблюдением всех стилей и правил можно скачать с SideWaffle
    • Загрузите файл расширений Visual Studio (vsix)
    • Запустите vsix файл
    • Перезапустите Visual Studio
  • WebStorm: AngularJS сниппеты с соблюдением всех стилей и правил, которые вы можете импортировать в настройки WebStorm
    • Скачайте шаблоны
    • Откройте WebStorm и перейдите в меню Файл
    • Выберите Импортировать Настройки
    • Укажите нужный файл и нажмите ОК
    • В типа файла JavaScript введите следующие команды, разделив их табуляцией
      • ng-c
      • ng-f
      • ng-m

Комментарии

4
Дмитрий Матросов, 1 год назад
1

Всё чётко! Спасибо большое за статью, это самая замечательная статья на русском языке, которую я прочитал!

devacademy, 1 год назад
0

заходи ещё, самые лучшие статьи впереди

Denis, 11 месяцев назад
1

Большое спасибо, очень кратко и по делу!!! Отлично. Только начинаю изучать и уже смотрю на свой код, и вижу нечитабельную кашу ))) Статья как раз во время.

devacademy, 11 месяцев назад
0

Будет ещё много статей, заходи