Любая физическая система стремится к состоянию с наименьшей потенциальной энергией. И программисты не исключение. Поэтому речь пойдет о том, как упростить себе жизнь при разработке на angular.js, используя при этом сервисы, которые сейчас в тренде. Главным образом, я буду ненавязчиво пиарить свое архитектурное решение angular-boilerplate, а на закуску предложу поделиться своим опытом и идеями в комментариях.
Мотивация
Свести рутину к минимуму, создать интуитивно понятную архитектуру и собрать вместе то, что называется best practices.
Сразу оговорюсь, что angular-boilerplate — результат создания с нуля нескольких проектов разной степени сложности, в процессе работы над которыми я всячески старался избавиться от ненужного усложнения, в первую очередь, собственного рабочего процесса. Поэтому в чем-то мои решения вызваны субъективным взглядом на разработку, но тем не менее все они прошли проверку временем и показали практическую пользу.
Особенности
Grunt
Начнем с того, что нам понадобится grunt.js. Grunt нужен будет, в первую очередь, для того, чтобы собирать и сжимать файлы кода и стилей. В проекте уже есть две задачи в Gruntfile.js: grunt install и grunt build — первая позволяет выполнять различные операции при установке и в начальном варианте производит установку необходимых библиотек через bower, вторая, как я уже писал выше собирает и минифицирует файлы с помощью require.js.
Bower
Чтобы лишний раз не бегать на гитхаб, библиотеки проще подгружать через bower. Здесь я отмечу файл .bowerrc, в котором просто можно определить путь и имя папки модулей.
Require.js
Практика, когда библиотеки и код подлючаются в html странице, лично у меня вызывает когнитивный диссонанс. И я всегда хотел иметь в javascipt привычную для других языков возможность програмно подключать код и определять зависимости. Поэтому я с большим энтузиазмом отношусь к Require.js и применению его в качестве каркаса для приложения. В этом месте я, пожалуй, опущу детальное введение в Require.js, а просто укажу на то, как он используется в проекте.
Итак, главное, что нам дает require.js, помимо модульности — возможность свести всё приложение в один файл и затем сжать. Но, разумеется, при разработке хотелось бы иметь дело с нормальной версией файлов, поэтому в индексе есть два варианта подключения логики и стилей:
<!--Development-->
<link rel="stylesheet" href="app/styles/styles.css">
<!--Production-->
<link rel="stylesheet" href="app/styles/styles.min.css">
<!--Development-->
<script data-main="app/js/app" src="app/lib/requirejs/require.js"></script>
<!--Production-->
<script src="app/js/app.min.js"></script>
Которые позволяют переключаться между development и production.
Все библиотеки, модули и прочее при этом подключаются при инициализации приложения, и их настройки определяются в файле app.js:
require.config({
baseUrl: 'app',
paths: {
'jquery': 'lib/jquery/dist/jquery',
'angular': 'lib/angular/angular',
'text': 'lib/requirejs-text/text'
},
shim: {
'jquery': {
exports: 'jQuery'
},
'angular': {
exports: 'angular',
'deps': ['jquery']
},
'lib/angular-route/angular-route': {
'deps': ['angular']
}
},
config: {
'js/services': {
apiUrl: 'http://localhost/api'
}
}
});
Далее пройдемся по файлам.
Файлы
/app
/js
app.js
app.min.js
controllers.js
directives.js
filters.js
services.js
/styles
styles.js
styles.min.js
/templates
someone-template.html
index.html
bower.json
package.json
.bowerrc
Gruntfile.js
Общая структура проекта не должна вызывать особых вопросов. Сервисы, директивы, контроллеры, фильтры задаются в соответсвующих файлах, которые потом автоматически подключаются при загрузке приложения:
var angular = require('angular'),
controllers = require('js/controllers'),
services = require('js/services'),
directives = require('js/directives'),
filters = require('js/filters');
...
angular.forEach(services, function (service, name) {
$provide.factory(name, service);
});
...
angular.forEach(directives, function (directive, name) {
app.directive(name, directive);
});
angular.forEach(filters, function (filter, name) {
app.filter(name, filter);
});
angular.forEach(controllers, function (controller, name) {
app.controller(name, controller);
});
Я думаю, что многие, да и я сам по началу задавались вопросом, как правильно оформлять логику, которая не привязана к конкретному отображению, а выполняется глобально. Часто ответом на этот вопрос служит контроллер, который вызывается где-то в index.html с помощью ng-controller. Конечно это не смертельно, но я лично считаю такой вариант более правильным:
app.run(controllers.GlobalCtrl);
То есть данный контроллер вызывается единожды при запуске приложения и формально не связан ни с одним шаблоном.
Одним из приятных дополнений requirejs является модуль text, который позволяет подгружать через require() не только AMD модули, но и обычные текстовые файлы, к примеру, html, которые потом могут быть включены при компиляции. Это позволяет нам добавлять в общий билд файлы шаблонов.
...
.when('/', {
controller: controllers.MainCtrl,
template: require('text!templates/main.html')
});
...
И напоследок, что касается:
$httpProvider.interceptors.push(['$q', '$error', function ($q, $error) {
return {
'request': function (config) {
return config;
},
'response': function (response) {
return response;
},
'responseError': function (rejection) {
$error('Connection error!');
return $q.reject(rejection);
}
}
}]);
На мой взгляд посредник при обработке запросов — неотъемлемая часть клиент-серверного взаимодействия, которая позволяет выполнять форматирование ответа или, в конкретном случае, реализовать централизованную обработку всех ошибок.
Не хочу говорить больше, чем нужно, поэтому на этом все. Как и обещал, приглашаю в комментарии, делиться своей практикой, мыслями и предложениями.
Автор: durovchpoknet