Лучшие способы использования Angular.js

Original author: Jeff Dickey
  • Translation

От переводчика:


Привет, Хабр! Мы мы продолжаем делится с сообществом полезными материалами о разработке и дизайне. В этот раз команда TrackDuck подготовили перевод статьи Jeff Dickey о Angular, которая нам очень понравилась и в свое время заставила пристальней присмотреться к Gulp. Эта статья будет полезна разработчиками, которые хотят сэкономить время на рутинных операциях и построить качественные процессы при разработке веб-приложений. Мы активно используем Angular для разработки собственного продукта для визуального комментирования веб-сайтов, поэтому готовы ответить в комментариях на интересующие вас вопросы!




Я использовал Angular в довольно большом количестве приложений и видел много способов структурирования приложений с использованием этого фрэймворка. Сейчас я пишу книгу о проектировании Angular приложений c использованием MEAN стека, и больше всего исследований я провел в этом направлении. В итоге я остановился на довольно оригинальной структуре приложения. Я считаю, что мой подход более простой чем тот, что предложил Burke Holland.
Прежде чем начать, я хотел бы рассказать о существующем подходе к реализации модульности в Angular.

Что такое модуль в JavaScript


JavaScript приложения по умолчанию не имеют возможности загрузки модулей. Сперва давайте разберемся, что такое “Модуль”, так как каждый может понимать это по-своему.
Модуль позволяет обеспечить разработчикам разделение приложения на логические части. Также в JavaScript разделение приложения на модули позволяет избежать конфликтов глобальных переменных.

Новички в JavaScript могут быть удивлены тем фактом, что модулям уделяется так много внимания. Хочу сразу отметить одну вещь- основным назначением модулей является НЕ организация отложенной (lazy-loading) загрузки компонентов приложения. Require.js позволяет реализовать это, и это не главное в модулях.

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

.noConflict()


Я бы хотел проиллюстрировать проблему: скажем, вы хотите включить jQuery в ваш проект. jQuery должен определить глобальную переменную ‘$’. Если в вашем коде уже используется переменная с таким именем, естественно, вы получите конфликт переменных. Обычно это проблема решается с помощью функции .noConflict(). Проще говоря, .noConflict() позволяет вам изменить название переменной, которую вы используете для библиотеки.

<script>
var $ = 'myobject that jquery will conflict with'
</script>
<script src='//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js'></script>
<script>
// $ is jQuery since we added that script tag
var jq = jQuery.noConflict();
// $ is back to 'myobject that jquery will conflict with'
// jq is now jQuery
</script>


Это довольно общее, но не самое удобное для большинства JavaScript библиотек решение. Оно не обеспечивает хорошего разделения кода — приходится объявлять модули перед их использованием, и по сути такой подход заставляет вас снова использовать метод, аналогичный .noConflict()

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

Существуют альтернативные решения проблемы, и я бы хотел рассказать про 4 основных направления:
  • Require.js (Implementation of AMD)
  • Browserify (Implementation of CommonJS)
  • Angular dependency injection
  • ES6 modules


Каждое из них имеет свою специфику, плюсы и минусы. Вы также можете использовать несколько из этих решений одновременно (например, Burke Holland использует два). Давайте рассмотрим каждое из решений.

Sample App


Давайте создадим небольшое приложение на Angular и попробуем разобраться на реальном примере.
Вот ссылка на GitHub репозиторий с готовым кодом. Для удобства начнем работать с JavaScript в одном файле app.js:
var app = angular.module('app', [])
 
app.factory('GithubSvc', function ($http) {
 return {
   fetchStories: function () {
     return $http.get('https://api.github.com/users')
   }
 }
})
 
app.controller('GithubCtrl', function ($scope, GithubSvc) {
 GithubSvc.fetchStories().success(function (users) {
   $scope.users = users
 })
})


Определим объект ‘app’ — это наш модуль. Далее мы определим сервис ‘GithubSvc’ с единственной функцией, которая поможет нам получить пользователей GitHub.
Наконец, определим контроллер, который с помощью сервиса будет загружать пользователей в массив, находящийся в $scope. Вот шаблон, с помощью которого мы будем отображать список.

Разделение на отдельные файлы


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

В нашем примере я предложил бы такую структуру:
  • src/module.js
  • src/github/github.svc.js
  • src/github/github.ctrl.js


Если ваше приложение будет небольшим, используйте альтернативный способ разделения файлов:
  • src/module.js
  • src/services/github.svc.js
  • src/controllers/github.ctrl.js


В любом случае, без использования модуля наподобие browserify или require.js мы должны подключить каждый из этих файлов, используя тег script. Важно понимать, что ваш код может легко вырасти до сотен файлов и управлять их подключением таким образом будет просто неудобно.

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

Нам нужен способ, который позволит разработчику подключать большое количество js файлов, при этом они не должны загружаться в браузер одновременно (без подключения через тег script)

Именно поэтому люди используют загрузчики модулей require.js или browserify. Angular позволяет логически разбить код, но не файлы. Я хочу показать более простое решение, но для начала давайте рассмотрим известные способы управления загрузкой модулей.

Require.js — очень сложно


Require.js был первой попыткой использовать модули в JavaScript. Решение позволяет определить зависимости непосредственно внутри js файла, запускается в браузере и способно загружать модули по мере необходимости.

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

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

Более подробно о require.js можно прочитать в этом обзоре.

Возможность require.js загружать модули по запросу не работает для Angular. Хотя в реальности я ни разу не работал с проектом, который имел эту необходимость. Кроме того, я бы хотел еще раз подчеркнуть, что этот функционал не является жизненно важным для разработчиков на Angular. Гораздо важнее уметь правильно разделить ваш код на логические части.

Browserify —хороший загрузчик модулей


В отличие от require.js, который использует браузер для загрузки модулей, browserify обрабатывает ваш код на сервере перед тем он как начнет исполняться в браузере. Вы не сможете взять файл browserify и запустить в браузере. Для начала вам потребуется создать ‘пакет’.

Пакеты используют схожий с модулями node.js формат (к тому же почти всегда совместимый). Выглядит это следующим образом:
var moduleA = require('my-module')
var moduleB = require('your-module')

moduleA.doSomething(moduleB)


Это, действительно, выглядит удобнее и читабельнее. Вы просто объявляете переменную и ‘require()’ для модуля, к которому она относится. Написать код, экспортирующий модуль, очень просто.

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

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

Angular Dependency Injection  - решает большинство наших проблем


Вернемся назад и взглянем на app.js. Я бы хотел отметить такой момент:
не имеет значения, в каком порядке мы создаем сервисы и контроллеры. Angular обработает все зависимости за нас с помощью внутренних механизмов.

При использовании этого метода, чтобы работать с объектом ‘app’, мы должны быть уверенны что модуль объявлен в начале программы. Это единственное место, где порядок деклараций в Angular имеет значение, так как я хочу объединить все JavaScript файлы.

Gulp Concat


Для объединения JS файлов я буду использовать Gulp. Не волнуйтесь о необходимости изучения нового инструмента, я буду использовать его очень простым способом и вы сможете легко портировать его в Grunt, Make или ту утилиту, к которой вы привыкли. Вам просто нужно что-то, что поможет вам автоматизировать объединение файлов.

Я попробовал все современные системы сборки и Gulp, несомненно — мой самый любимый. Когда дело доходит до работы с JS или CSS он работает великолепно.

Вы можете подумать что я просто хочу заменить один инструмент сборки (browserify) на другой (Gulp), и вы будете правы. Но, Gulp умеет решать более широкий круг задач. Вы можете сконфигурировать и использовать Gulp для таких задач как минификация, предварительная компиляция CoffeeScript, генерация sourcemaps, обработка изображений, компиляция scss или sass, запуск dev сервера на node.js. Это все обеспечит нам платформу для дальнейшего расширения нашего проекта.

Сначала я установлю Gulp и Gulp-concat
$ npm install --global gulp
$ npm install --save-dev gulp gulp-concat


Вам понядобится создать файл package.json в вашем проекте и установить node.js. Вот небольшой трюк, который поможет вам инициализировать новый проект:
$ echo '{}' > package.json


Далее, создадим файл gulpfile.js:
var gulp = require('gulp')
var concat = require('gulp-concat')
 
gulp.task('js', function () {
 gulp.src(['src/**/module.js', 'src/**/*.js'])
   .pipe(concat('app.js'))
   .pipe(gulp.dest('.'))
})


Это простая задача, которая объединит JavaScript файлы из директории /src в файл app.js. JS файлы перечислены в таком порядке не спроста — именно при такой записи файлы *. module.js будут включены первыми. Я расскажу об этом, когда мы подробнее рассмотрим минификацию.

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

Gulp Watch


Это очень просто и код говорит сам за себя:
var gulp = require('gulp')
var concat = require('gulp-concat')
 
gulp.task('js', function () {
 gulp.src(['src/**/module.js', 'src/**/*.js'])
   .pipe(concat('app.js'))
   .pipe(gulp.dest('.'))
})
 
gulp.task('watch', ['js'], function () {
 gulp.watch('src/**/*.js', ['js'])
})


Тут мы всего лишь добавляем новую задачу ‘gulp watch’ которая будет выполнять задачу ‘js’ каждый раз когда любой из JavaScript файлов в ‘src/**/*.js’ будет изменен. Вуа-ля, мы избавились от рутины!

Minification


Пришло время заняться минификацией нашего кода. В Gulp мы создаем поток для исходных файлов, которые должны быть обработаны с помощью нескольких модулей (Минимизация, конкатенации и т.д.) и, затем сохраняем их.

Давайте начнем с gulp-uglify. Для начала потребуется установить его c помощью npm:
npm install -D gulp-uglify


Теперь дополним наш Gulp файл:
var gulp = require('gulp')
var concat = require('gulp-concat')
var uglify = require('gulp-uglify')
 
gulp.task('js', function () {
 gulp.src(['src/**/module.js', 'src/**/*.js'])
   .pipe(concat('app.js'))
   .pipe(uglify())
   .pipe(gulp.dest('.'))
})


Но у нас есть проблема. Gulp-uglify распознав имена аргументов функции Angular должен внедрить зависимости. Теперь наше приложение не работает. Если вы не знакомы с этой проблемой, прочитайте. docs.angularjs.org/guide/di

Мы можем использовать громоздкий синтаксис массивов в коде или замечательный инструмент: ng-gulp-annotate

Установим его:
npm install -D gulp-ng-annotate


И дополним наш gulpfile:
var gulp = require('gulp')
var concat = require('gulp-concat')
var uglify = require('gulp-uglify')
var ngAnnotate = require('gulp-ng-annotate')
 
gulp.task('js', function () {
 gulp.src(['src/**/module.js', 'src/**/*.js'])
   .pipe(concat('app.js'))
   .pipe(ngAnnotate())
   .pipe(uglify())
   .pipe(gulp.dest('.'))
})

Я надеюсь, вы начинаете видеть ценность в Gulp!

Sourcemaps


Практически каждый использует отладку для диагностики проблем с кодом. Проблема в том, что минифицированный JavaScript отлаживать невозможно. Для этого используются карты кода (sourcemaps). Вот Gulp плагин, который поможет их сгенерировать:

Установим gulp-sourcemaps:
npm install -D gulp-sourcemaps


И добавим соответствующую нотацию в gulpfile:
var gulp = require('gulp')
var concat = require('gulp-concat')
var sourcemaps = require('gulp-sourcemaps')
var uglify = require('gulp-uglify')
var ngAnnotate = require('gulp-ng-annotate')
 
gulp.task('js', function () {
 gulp.src(['src/**/module.js', 'src/**/*.js'])
   .pipe(sourcemaps.init())
     .pipe(concat('app.js'))
     .pipe(ngAnnotate())
     .pipe(uglify())
   .pipe(sourcemaps.write())
   .pipe(gulp.dest('.'))
})


Почему конкатенация лучше


Потому что, в первую очередь это проще. Angular обрабатывает весь код загрузки за нас, мы просто должны помочь с файлами.
Он также отлично подходит, потому что при создании новых файлов их достаточно добавить в каталог с существующими. Не требуется дополнительного декларирования этих файлов где бы то ни было, как, к примеру, в browserify. Нет зависимостей, которые были в require.js
Собственно чем меньше мест где мы можем ошибиться — тем меньше будет ошибок.

Резюме


Вот окончательный код. Это может легко стать отправной точкой для создания вашего Angular приложения.
  • Он хорошо структурирован.
  • JS автоматически минифицируется.
  • В нем есть карты исходного кода.
  • Он не имеет Глобальных переменных.
  • Не используются избыточные теги script.
  • Настройка сборки очень простая.


Я попытался сделать описание как можно более универсальным, Как я упоминал ранее это реализуемо не только с помощью Gulp. Повторить все тоже самое с любым сборщиком который поддерживает конкатенацию JS.

Взгляд в будущее: Angular 2.0 и ES6


Ожидается, что в следующая версии JavaScript будет решена проблема модулей на нативном уровне. И когда ES6 получит повсеместную поддержку — разумеется эта статья станет архаизмом, но тем не менее, я надеюсь на какое-то время я помог вам.

Стоит отметить, что Angular 2.0 планируется поддержка модулей ES6, и это замечательно. Я думаю что это будет похоже на набор пакетов из которых вы легко сможете составить необходимый функционал для вашего приложения. Но пока версия 2.0, к сожалению тоже далека от выпуска.
Angular 2.0 будет использовать отдельную библиотеку di.js, которая будут отвечать за модули. Мы сможем легко использовать ее во всех приложениях на Angular. Проблемой является только то, что до выхода ES6 и A2.0 придется иметь дело с текущей реализацией модулей, с чем, я надеюсь вам поможет моя статья.
TrackDuck
Company
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 18

    +2
    Очень интересна практика размещения сущностей в очень большом проекте, как лучше использовать html и стили less:

    Так:
    src/module.js
    src/github/github.svc.js
    src/github/github.ctrl.js
    src/github/github.less
    src/github/github.html

    Или так:
    src/module.js
    src/services/github/github.svc.js
    src/controllers/github/github.ctrl.js
    src/styles/github/github.less
    src/views/github/github.html

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

    Было бы неплохо увидеть широко распространённые практики раскладки сущностей по папкам.
      +1
      Используйте второй вариант и напишите скрипт, который будет проходиться по папкам и заносить все файлы с заданным расширением в заголовочный less или js файл. Практика другая потому, что больше разработчиков работают или в студиях, где небольшие проекты на потоке, или с open source, или не хотят развиваться и брать на себя ответственность. В очень большом проекте работает только компонентная архитектура с минимальной связанностью между компонентами и отдельными моделями, видами, контроллерами и т.п у каждого компонента.
        +1
        Обратите внимание на уже готовые генераторы, например generator-cg-angular.
        +15
        Какая-то статья ни о чем.

        Начали про ангуляр, продолжили про жквери, закончили gulp-concat.

        И да, есть модуль gulp-browserify, который еще и сурсмапы делает.
          +1
          Мне кажется что автор хотел провести читателя от проблем которые возникают при попытках реализовать модули в клиентском js, jQuery noConflict упоминается лишь как частный пример, не очень с его точки зрения, удачной реализации. Собственно там и написано
          Я бы хотел проиллюстрировать проблему
          . Да в статье многовато воды, убирали что могли ) Но надеюсь что многим будет позлезно, узнать даже про достаточно тривиальные на ваш взгляд вещи.
            +1
            а есть такой для grunt?
          +7
          Читал эту статью в оригинале и, честно говоря, меня очень удивило несоответствие заголовка и содержания.

          1) Непосредственно про Angular здесь не так много
          2) Автор рассуждает о том, как «космические корабли бороздят просторы Большого Театра», а потом приходит к выводу, что DI в ангуляре для разруливания зависимостей вполне ок, а с загрузкой отдельных модулей по сети лучше не париться, объединяя все скрипты в один файл app.js и минифицируя его. Ну и предлагает для этого Gulp.js
            +2
            Поддержу, пожалуй по-поводу заголовка. В оригинале звучит как «Best Practices for Building Angular.js Apps». Сейчас подумаем как заменить на более точно отражающий суть материала. Может быть у Вас есть предложения?
              +2
              Да мне и оригинальный заголовок не очень :) Building — довольно общее слово, я до середины статьи надеялся, что речь пойдёт об архитектуре приложений и о «правильном» использовании компонентов фреймворка.

              Имхо, что-то подобное ближе к смыслу статьи:
              1) «Способы сборки Angular.js приложений» — отражает обзорный характер статьи
              2) «Сборка Angular.js приложений: RequireJS, Browserify, Gulp» — возможно поможет кому-нибудь потом найти эту статью в поисковике
              3) «Простой и удобный конфиг Gulp для сборки Angular.js приложений» — громоздко, но вроде бы наиболее полно отражает то, что автор статьи в итоге сделал
                0
                Имхо второй вариант ок.
            +1
            Возможность require.js загружать модули по запросу не работает для Angular. Хотя в реальности я ни разу не работал с проектом, который имел эту необходимость.

            Просто angular 1 не рассчитан на работу с AMD и требует прямой работы с $compile и перекомпиляции в случае получения каких-то модулей в этом режиме.
            И это один из существенных недостатков особенно если речь идет о более менее крупных приложениях, об этом говорят и разработчики самого Angular и будут исправлять это во версии 2.

            Куда более удачный пример Durandal где require формирует основу движка композиций и позволяет очень гибко через lazy догружать модули по требованию, в отличии от Angular, в котором первоначально разработчиками подразумевалась полная загрузка всех компонентов модуля в память браузера.
            По этим же причинам Angular 2 будет представлять из себя Angular 1 + Durandal + ES6 и не удивляюсь если у них появиться какой-то $require сервис для работы с AMD.
              0
              Я любитель в вэб разработке, но мне вообще не ясно где это писать?
              $ npm install --global gulp $ npm install --save-dev gulp gulp-concat
              Что это вообще? Командная строка? Gulp это оффлайн тулза или js-файл или что?

              Я вообще пропустил виток развития программирования на js (я остался в том веке когда нужно просто писать код в js файлике, ну максимум еще использовать jQquery), что такое Ангуляр, require.js? Может кто-то в двуг словах или реальные примеры?
                0
                Это командная строка. Npm (node package manager) — переменная среда, в контексте которой будет запущена команда (install), с теми параметрами, которые Вы в нее передадите (--global gulp). Для использования npm нужно установить node.js.
                Node.js можно особых проблем поставить и на Windows.
                  0
                  без особых*
                  –1
                  Реальные примеры? Ну хотя бы mail.google.com (AngularJS)…
                  А еще есть BackboneJS, EmberJS, ReactJS…
                  Что касается RequireJS — это модульный загрузчик для сборки приложения в кучу ( с учетом того, что разобрали мы его сами, дабы не запутаться).
                  На русском можно почитать например здесь

                  Ах, и да… мы теперь на js даже серверы пишем…

                  Ну а как там у вас в 2005-м? Все нормально? Да, кстати… Ни в коем случае(!) не ставь на французов в следующем году… они тебя разочаруют.

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