Документация AngularJS отлично подходит для начала работы и ковыряния в API. Однако, она не объясняет как организовать и управлять приложением, когда оно разрастется до десятков или сотен тысяч строк кода. Я собрал здесь некоторые из моих наблюдений и передового опыта по управлению расползающимися приложениями. Сначала взглянем на организацию, затем перейдем к некоторым советам по улучшению производительности и закончим краткой сводкой по инструментам, серверам и процессу сборки. Этот пост будет сосредоточен на больших приложениях, в частности, есть отличная статья по лучшим практикам AngularJS с декабрьской встречи, на которую также стоит взглянуть.
Не пишите огромные приложения
Лучший совет по огромным приложениям не делать их. Пишите небольшие сфокусированные модульные части и постепенно объединяйте их в более крупные вещи, чтобы собрать приложение. (Этот совет дал хакер node.js, крутой, во всех отношениях, чувак @substack).
Организация
Вероятно, самый большой вопрос по большими приложениям, куда же поместить весь код. В набор вещей, требующих организации, попадают файлы, каталоги, модули, сервисы и контроллеры. Для быстрого обзора хорошей структуры проекта, посмотрите шаблонный проект AngularJS на Github. Тем не менее хотел бы копнуть глубже и предложить некоторые дополнительные рекомендации по структуре проекта. Давайте начнем с каталогов и будем двигаться вниз по списку.
Каталоги
Типичная рекомендуемая мной структура папок:
root-app-folder ├── index.html ├── scripts │ ├── controllers │ │ └── main.js │ │ └── ... │ ├── directives │ │ └── myDirective.js │ │ └── ... │ ├── filters │ │ └── myFilter.js │ │ └── ... │ ├── services │ │ └── myService.js │ │ └── ... │ ├── vendor │ │ ├── angular.js │ │ ├── angular.min.js │ │ ├── es5-shim.min.js │ │ └── json3.min.js │ └── app.js ├── styles │ └── ... └── views ├── main.html └── ...
По мере добавления новых файлов, возможно, имеет смысл создавать подкаталоги для дальнейшей организации контроллеров и услуг. Например, часто ловлю себя на том, что делаю каталог models
внутри services
. Как правило так же сортирую файлы по каталогам, если существует некоторая рациональная иерархия с помощью которой можно организовать хранение файлов.
На тему организации кода читайте так же Организация кода в больших AngularJS и JavaScript приложениях (прим. переводчика)
Файлы
Каждый файл должен содержать одну «сущность», где «сущность» представляет собой контроллер, директиву, фильтр или сервис. Это позволяет, как минимум, сфокусировать файлы. Также помогает создать лакмусовую бумажку для тестирования API. Если обнаружили, что листаете файлы вперед и назад слишком часто, это признак того, что ваши API-интерфейсы слишком сложные. Необходимо переосмыслить, реорганизовать и упростить их.
Хотел бы сделать исключение для тесно связанных директив. Например, если имеется директива <pane>
, обращающаяся к <panel>
как к родителю, то они должны быть в одном файле.
Модули
Определяйте и настраивайте все модули в app.js
:
angular.module('yourAppName', ['yourAppDep']);
angular.module('yourAppDep');
Определяйте контроллеры, сервисы и т.д. в модулях следующим образом:
angular.module('yourAppDep').controller('MyCtrl', function () {
// ...
});
Хотя мы (команда Ангуляра) обсуждали возможность ленивой загрузки модульной структуры, она еще не входит в планы следующей версии Ангуляра. Было хорошее обсуждение на Google+ об использовании составных высокоуровневых приложений приложения для достижения эффекта ленивой загрузки. Не пробовал так делать, но если отчаянно нуждаетесь в уменьшении размера полезной нагрузки, что, безусловно, там показан один из способов.
Единственный оставшийся вопрос, каким образом разделить контроллеры, директивы, сервисы и фильтры на модули. Шаблонный проект Ангуляра выносит фильтры, сервисы и директивы в отдельные модули, но мне это кажется немного глупым. В зависимости от приложения, был бы более склонен организовать модули по страницам/маршрутам. С точки зрения производительности, не имеет значения, как вы организуете модули, так что выберите сами, какой метод лучше всего подходит для вашего проекта.
Зависимости
В целом, сервисы, контроллеры, директивы и т.д., должны иметь настолько мало зависимостей насколько это возможно. Это хорошая практика разработки программного обеспечения в целом, поэтому стоит её упомянуть. Такой подход так же поможет в тестировании.
API-интерфейсы должны быть многоуровневыми. Не следует разносить контроллеры по разным уровням абстракции.
Директивы
Используйте уникальный префикс для директив в приложении. Это позволит избежать пересечений со сторонними компонентами. Что касается сторонних компонентов, есть растущее сообщество на сайте под названием ngmodules, которое выглядит многообещающим.
Например, если ваше приложение называется «The Best Todo List App Ever», можете начинать директивы с префикса «btla».
angular.module('yourAppDep').directive('btlaControlPanel', function () {
// ...
});
Можно обеспокоиться тем, что имена станут слишком длинными, но я не видел, чтобы это стало проблемой. Благодаря GZIP-сжатию производительность не упадет из-за длинных названий директив.
Сервисы
angular.module('yourAppDep').service('MyCtrl', function () {
// ...
});
Модели
Ангуляр выделяется среди яваскипт-фреймворков тем, что дает полный контроль над уровнем модели. Думаю, что это одна из самых сильных сторон Ангуляра, потому что в основе приложения лежат данные, и данные резко изменяются от приложения к приложению. Самая лучшая рекомендация принимать во внимание то, как данные будут использоваться и храниться.
Если используете NoSQL хранилища данных, такие как CouchDB или MongoDB, можно будет использовать в качестве контента чистые JavaScript-объекты (POJO) и функциональных помощников. Если используете реляционную базу данных, как MySQL, можете использовать псевдо-классы с методами мутации, сериализации и десериализации данных. Если сервер предоставляет RESTful- интерфейс, сервис $resource станет хорошей площадкой для начала. Это всего лишь предложения, любые из этих подходов могут быть полезны вне ситуаций, которые описал. Вариантов так много, что иногда сложно принять решение. Но раздумья об организации данных окупятся.
В основном, можно найти много отличных инструменты для работы с моделями в Underscore.js, библиотеке, которая также используется в Backbone.js.
Контроллеры
По соглашению, имена контроллеров должны начинаться с заглавной буквы и заканчиваться на «Ctrl».
angular.module('yourAppDep').controller('MyCtrl', function () {
// ...
});
Помните, что контроллеры могут использоваться повторно. Это кажется очевидным, но я поймал себя на повторной реализации функций в разных контроллерах. Рассмотрим, например, пользовательскую панель управления, которая позволяет пользователю изменять параметры программы и диалоговое окно, предлагающее пользователю менять настройку. Оба могли бы использовать общий контроллер.
Производительность
Ангуляровские приложения, как правило, очень и очень быстрые. Большинство приложений действительно не требуют никакой специальной оптимизации, так что если не испытываете проблем с производительностью, лучше потратить время на улучшение приложения в других отношениях. В исключительных случаях Ангуляр обеспечивает отличные способы решения проблем с производительностью. Во-первых, важно определить причину снижения производительности. Для этого очень рекомендую расширение Batarang для Хрома или встроенное в Хром профилирование процессора.
Оптимизация цикла переваривания (digest)
Ангуляр использует грязную проверку в цикле «переваривания». Непосвященные могут прочитать больше о цикле переваривания в официальной документации и в этом ответе на StackOverflow.
Иногда хочется избежать цикла переваривания. Одной из распространенных ситуации в приложениях реального времени, использующих веб-сокеты, является то, что при получении сообщений не всегда хочется запускать переваривание. Рассмотрим игру в режиме реального времени, в которой сообщения отправляются с сервера более 30 раз в секунду.
app.factory('socket', function ($rootScope) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () { // может происходить много раз в секунду
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
}
// ...
};
});
Один из отличных способов справиться с этим, это «задушить» запросы, чтобы запускать переваривание лишь несколько раз в секунду. Underscore.js предоставляет такую функцию, но её реализация миниатюрна, так что воспроизвел ее внутри сервиса socket
ниже:
app.factory('socket', function ($rootScope) {
// Underscore.js 1.4.3
// http://underscorejs.org
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore may be freely distributed under the MIT license.
// _.throttle
// https://github.com/documentcloud/underscore/blob/master/underscore.js#L626
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
var throttle = function (func, wait) {
var context, args, timeout, result;
var previous = 0;
var later = function() {
previous = new Date();
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date();
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, throttle(function () { // ограничиваем до одного раза в 500 мс
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
}, 500));
}
// ...
};
});
Так же известно, что входящие изменения влияют только на определенные области и можно провести грязную проверку только для них. В подобных случаях можно вызвать $scope.$digest вместо $scope.$apply
. $digest
сработает только для области видимости, в которой он вызван и для всех дочерних областей видимости.
Наконец, чтобы поддерживать циклы переваривания короткими, выражения в $scope.$watch должны быть как можно более быстрыми. По возможности, избегайте глубоких сравнений. Помните, что необходимо всего лишь сравнивать вещи, влияющие на вид.
Фильтры
Фильтры вызываются, по крайней мере, дважды в течение каждого цикла переваривания. По этой причине, лучше, если они легкие.
В случаях, когда данные загружаются и отображаются, но не изменяется, может быть лучше вынести логику преобразования из фильтра в место получения данных. На самом деле это довольно просто, так как фильтры Ангуляра можно использовать в программе с помощью сервиса $filter.
Например, загружается список имен в нижнем регистре в котором необходимо изменить первые буквы на заглавные, для чего используется фильтр, делающий такое преобразование:
{{someModel.name | titlecase}}
Довольно легко переместить его в контроллер и заменять первые буквы имен на заглавные при загрузке.
angular.module('myApp').controller('MyCtrl', function ($scope, $http, $filter) {
$http.get('/someModel')
.success(function (data) {
$scope.someModel = data;
// применяем тот же самый фильтр «titlecase» внутри контроллера после загрузки данных
$scope.someModel.name = $filter('titlecase')($scope.someModel.name);
});
});
В случаях, когда невозможно преобразовать данные во время выборки, запоминание будет лучшим способом ускорения дорогостоящих фильтров без особого труда. Адди Османи написал довольно большую статью о запоминании в JavaScript, которую стоит прочесть. Что касается реализации, Underscore.js предоставляет отличные запоминающие функции, которые можно использовать. Однако, этим методом не следует пользоваться вслепую. Запоминание помогает только при частых вызовах одного и того же фильтра без изменения модели. Для быстро меняющихся моделей, значения в которых слишком разнятся во время выполнения приложения, лучше не использовать запоминающие фильтры для таких данных.
Так же пару советов по оптимизации, можно прочитать здесь (прим. переводчика)
Тестирование
Тестирование чрезвычайно важно для больших проектов. Тесты позволяют уверенно проводить рефакторинг, который имеет важное значение для сохранения чистоты кода в большом проекте. В больших приложениях должны проводится как модульные, так и системные (E2E) тесты. Модульные тесты помогают выявить проблемы, а системные тесты — убедиться, что всё приложение работает как ожидалось. Каждый контроллер, сервис, фильтр и директива должны иметь набор модульных тестов. Каждая особенность приложения должна иметь системный тест.
Это еще одна тема, заслуживающая большого внимания. К счастью, документация Ангуляра может многое сказать как о модульных, так и о сисемных тестах. Мой коллега Войта Джин также рассказывал недавно о тестировании директив. Безусловно, стоит посмотреть его видео.
Инструментарий
Я проделал кучу работы по Yeoman, чтобы попытаться выделить лучшие практики и хорошую структуру проекта, и сделать автоматическую генерацию небольших шаблонных библиотек Ангуляра. Настоятельно рекомендую воспользоваться этим.
Batarang еще один из моих проектов, который подходит для отладки и поиска узких мест в производительности.
Сервер
Как известно, с Ангуляром можно использовать любой сервер, какой захотите. Это строго клиентская библиотека. Моей рекомендацией и предпочтительной установкой является использование Node.js вместе с Nginx. Использую Nginx как сервер статических файлов, и Node.js для создания RESTful API и/или сокет приложения. Node.js это золотая середина между простотой использования и скоростью. Например, относительно легко порождать рабочие процессы или создать веб-сервер, который может использовать все ядра воображаемого сервера.
Что касается облачных сервисов, с большим успехом использовал и Nodejitsu и Linode. Nodejitsu предпочтителен, если строго придерживаетесь Node.js. Он облегчает развертывание приложения, и вам не придется беспокоиться о серверной среде. Можно порождать дополнительные процессы Node.js, необходимые для расширения и чтобы выдерживать большую нагрузку. Если необходим больший контроль над серверной средой, Linode предоставляет доступ к корню парка виртуальных машин. Linode также обеспечивает хороший API для управления виртуальными машинами. Существует множество других отличных поставщиков облачных сервисов, которые еще не успел испытать на себе.
Настройка и масштабирование серверной части достойна отдельной статьи, и нет недостатка хороших советов в других местах.
Процесс сборки
По общему признанию, это та вещь, в которой нуждается Ангуляр, чтобы стать лучше, и одна из моих огромных целей на 2013 год помочь на этом фронте. Я выпустил ngmin, инструмент, который, надеюсь, в конечном счете, решит задачу минимизации AngularJS приложений на стадии выпуска.
На данный момент, думаю, что сперва лучше всего объединить яваскрипт-файлы с app.js
, затем с помощью ngmin
закомментировать функции внедрения зависимости, и, наконец, минифицировать с помощью Closure Compiler с флагом --compilation_level SIMPLE_OPTIMIZATIONS
. Можете посмотреть пример в работе в процессе сборки angular.js.
Не рекомендую использовать RequireJS с AngularJS. Хотя это, конечно, возможно, я не видел ни одного случая, когда применение RequireJS было бы выгодно на практике.
Заключение
Ангуляр является одним из наиболее подходящих JS фреймворков для написания больших приложений. Замечательный, очень быстрый и очень помогает структурировать приложение. Надеемся, что эти советы полезны для расширения границ познания, о возможностях Ангуляра.
У вас есть несколько советов по масштабированию ангуляр-приложений? Пишите в твиттер, на электронную почту, или отправьте пулл-реквест на Гитхабе.
Автор: tamtakoe