Одна из особенностей Angular, присущая и первой и новой версии — высокий порог вхождения. Новый Angular, помимо всего прочего, трудно даже запустить. А и запустив, легко получить 1-2 Мб скриптов и порядка нескольких сотен запросов при загрузке hello world страницы. Можно, конечно, использовать всякие стартеры, seed'ы или Angular CLI, но для использования в серъезном проекте нужно самому во всем разбираться.
В этой статье я постараюсь описать, как настроить удобную среду разработки с использованием SystemJS, и production сборку Angular приложения на основе Rollup, с выходом около 100кб скриптов и нескольких запросов при открытии страницы. Использовать будем TypeScript и SCSS.
Попробовать все в деле можно в моем angular-gulp-starter проекте.
Среда разработки
Во время разработки, на мой взгляд, самое важное — быстро увидеть свой код в работе. Ты вносишь правки в код, смотришь его в работе, правишь код снова. Чем быстрее все это происходит, тем комфортнее среда. Помимо этого, важно иметь удобную отладку, информативные сообщения об ошибках (которые легко найти в коде). На случай непредвиденных ситуаций, важно держать все под контролем — нужно иметь доступ ко всем промежуточным файлам, чтобы легко исследовать проблему.
Технически, нам нужно решить три задачи:
- Скомпилировать TypeScript
- Скомпилировать SCSS
- Загрузить все (в т.ч. зависимости) в браузер в нужном порядке
Первые две задачи удобнее всего решать при помощи функции compile-on-save, которая работает почти в любой IDE. При таком подходе достаточно сохранить свои правки в коде, переключиться на окно браузера и нажать F5 — очень быстро и удобно. Кроме того, результаты компиляции легко проконтролировать, js-файлы лежат рядом c ts, и в случае чего всегда можно их поисследовать.
Из IDE для работы с TypeScript могу порекомендовать Visual Studio (например, Visual Studio 2015 Community Edition), которая имеет встроенную поддержку TypeScript + расширение Web Compiler для SCSS. Я пробовал Atom, Visual Studio Code, но на моем ноутбуке они слишком тормозят. Visual Studio (не Code) хорошо справляется с подсветкой, автодополнением и компиляцией на лету даже на слабой машине. Хотя там есть некоторые проблемы подсветки при использовании es6 import.
Третья задача (загрузить все в браузер) — наиболее проблемная, т.к. скрипты зависят друг от друга, и должны загружаться в правильном порядке. Контролировать все это вручную трудно и не нужно. Лучше всего оставить разбираться с зависимостями библиотеке SystemJS: в коде используем ES6 import/export синтаксис, и основываясь на этом, SystemJS подгружает динамически все необходимые файлы. Не нужно строить никаких бандлов, выполнять какую-то специальную сборку, достаточно просто настроить config.
Конфигурация SystemJS — это js-файл, который может выглядеть примерно так:
System.config({
defaultJSExtensions: true,
paths: {
"*": "node_modules/*",
"app/*": "app/*",
"dist-dev/*": "dist-dev/*",
"@angular/common": "node_modules/@angular/common/bundles/common.umd",
"@angular/core": "node_modules/@angular/core/bundles/core.umd",
"@angular/http": "node_modules/@angular/http/bundles/http.umd",
"@angular/compiler": "node_modules/@angular/compiler/bundles/compiler.umd",
"@angular/platform-browser-dynamic": "node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd",
"@angular/platform-browser": "node_modules/@angular/platform-browser/bundles/platform-browser.umd",
"@angular/router": "node_modules/@angular/router/bundles/router.umd",
"@angular/forms": "node_modules/@angular/forms/bundles/forms.umd"
},
packageConfigPaths: ["node_modules/*/package.json"]
});
Здесь мы делаем следующее:
- Указываем, чтобы SystemJS автоматически подставляла расширения js к файлам (defaultJSExtensions).
- Указываем, что если не указано иное, искать все в папке node_modules (
"*": "node_modules/*"
). Это позволит легко устанавливать зависимости через npm. - Прописываем, что модули, начинающиеся с
app
, нужно загружать не изnode_modules
, а из папки app (наша основная папка со скриптами). Это используется только в index.html, где импортируетсяapp/main
. - Прописываем пути к angular модулям. В идеале, это должно происходить автоматически благодаря параметру
packageConfigPaths
, но у меня не получилось заставить его работать (что я сделал не так?). - Если какая-то сторонняя библиотека не находится автоматически, то также прописываем путь к ней явно.
После этого, нам достаточно включить в index.html ряд служебных скриптов: zone.js, reflect-metadata, core-js (или es6-shim), саму systemjs, ее конфиг и вызвать импорт главного модуля:
<script>System.import('app/main');</script>
В результате SystemJS загрузит файл app/main.js
, проанализирует его import
и загрузит эти импортируемые файлы, проанализирует их import и так по очереди будут загружены все файлы приложения.
Однако, это еще не совсем все. Дело в том, что библиотека rxjs, активно используемая в Angular, состоит из множества маленьких модулей. Поэтому, если оставить все так, то при обновлении страницы все они будут грузится по одному, что несколько медленно (до 100-300 запросов).
Поэтому, в своем стартер-проекте я собираю всю rxjs в один бандл, с помощью Rollup. Перед этим, она дополнительно компилируется в ES6, что используется после, в продакшн сборке.
Сборка этого rxjs бандла получается довольно сложной. Сначала компилируются TypeScript исходники в ES6 (из папки node_modules/rxjs/src
), после чего все это пакуется при помощи Rollup в один файл, и транспилируется в ES5. При этом, чтобы подружить этот бандл с SystemJS, создается временный файл, который служит входной точкой для Rollup, и выглядит примерно так:
import * as pkg0 from 'rxjs/add/observable/bindCallback';
System && System.set && System.set('rxjs/add/observable/bindCallback', System.newModule(pkg0));
import * as pkg1 from 'rxjs/add/observable/bindNodeCallback';
System && System.set && System.set('rxjs/add/observable/bindNodeCallback', System.newModule(pkg1));
... и так все модули rxjs
Все это можно найти в файлах build.common.js/rxjsToEs и build.dev.js/rxjsBundle. Скомпилированные в ES исходники также используется при продакшн сборке, поэтому компиляция вынесена отдельно.
После того как бандл собран, его нужно загрузить перед тем, как будет загружен код нашего приложения. Делается это так:
System.import('dist-dev/rxjs.js').then(function () {
System.import('app/main');
});
В результате получаем примерно на секунду быстрее загрузку страницы:
Для удобства разработки, вам также пригодится простой веб-сервер, с поддержкой HTML5 роутинга (когда на все запросы возвращается index.html). Пример такого сервера на основе express можно также найти в стартере.
Знающий читатель еще может спросить, почему не Webpack? Если коротко — webpack хорош для продакшн, но, имхо, неудобен во время разработки. Подробнее в спойлере ниже.
Webpack
Angular CLI и многие starter и seed проекты используют Webpack. Он теперь умеет делать tree-shaking, и говорят, даже hot module reloading (кто-нибудь пробовал именно в контексте Angular?). Но я не разделяю ажиотажа вокруг этого сборщика, и не понимаю, откуда он берется. Webpack это бандлер, и он может только построить бандл. Это порождает множество проблем:
- Сборка занимает некоторое значительное время (как минимум, несколько секунд). Мы не можем использовать compile-on-save, который намного быстрее (по крайней мере, это не так просто).
- Да, можно использовать watch, так что при сохранении изменений сборка будет запускаться автоматически. Но это не решает проблемы. 1. На практике все выглядит так: я ввожу часть кода, сохраняю, запускается сборка, пока она длится, я ввожу следующий код и сохраняю — в результате получается устаревший бандл, без последних правок. Кто-нибудь сталкивался с такой проблемой? Как вы ее решаете?
- Если у вас не работают source maps (а они почему-то постоянно ломаются и иногда тормозят), то сообщения об ошибках будет трудно локализовать.
Впрочем, я могу ошибаться, так как с Webpack особо не работал.
JSPM
JSPM — это первое, что приходит в голову, когда речь заходит о SystemJS. Действительно, с его помощью довольно легко настроить удобную среду разработки для Angular. При этом можно использовать как compile-on-save, так и TypeScript загрузчик. Говорят, там даже работает tree-shaking на основе Rollup. Казалось бы, все идеально.
Но это только на первый взгляд. Порой мне кажется, что JSPM живет в каком-то своем параллельном мире, далеком от всего происходящего вокруг. Зачем-то им понадобилось хранить все пакеты, в том числе npm-пакеты, в своей отдельной папке особым образом. В результате, вместо удобного "из коробки" инструмента, вы получаете кучу головной боли, о том, как заставить все остальные утилиты (которые, как правило, умеют работать с node_modules) подружить с JSPM.
Как минимум, придется устанавливать отдельно typings для зависимостей, чтобы подружить JSPM с TypeScipt (или еще хуже, прописывать пути). Заставить работать AOT-компилятор — тоже отдельная тема. Если нужно сделать что-то нестандартное (как с rxjs), тоже проблемы. Вообщем, у меня просто не получилось все увязать и сделать production сборку на JSPM. Если у кого-то получится, мне было бы очень интересно посмотреть.
Browserify
Вроде есть поддержка Rollup. Возможно стоит попробовать сделать на его основе продакшн сборку, не пробовал. Однако, если честно, не вижу в этом особого смысла, когда Rollup сам по себе неплохо справляется с задачей. В остальном — все то же, что и с Webpack.
Production сборка
Релизная сборка Angular включает в себя следующие этапы:
- Ahead of time (AOT) компиляция шаблонов (html и css части компонентов). В dev-среде они компилируются прямо в браузере, однако для релиза лучше делать это заранее. Тогда нам не придется тащить в браузер код компилятора, повысится эффективность tree-shaking, немного ускорится запуск приложения.
- Компиляция TypeScript в ES6 (включая результаты первого шага). Нужен именно ES6, т.к. Rollup умеет работать только с ES6. Компилируем также SCSS, запускаем пост-процессинг.
- Сборка бандла с использованием tree-shaking при помощи Rollup. В результате из кода удаляются все неиспользуемые части, и размер скриптов сокращается в десятки раз.
- Транспиляция результата в ES5 при помощи того же TypeScript, минификация.
- Подготовка релизного index.html, копирование файлов в dist.
AOT-компиляция
AOT-компиляция осуществляется при помощи пакета @angular/compiler-cli
(называемый также ngc), который построен на базе компилятора TypeScript. Для выполнения компиляции нужно:
- Установить пакеты:
@angular/compiler
,@angular/core
,@angular/platform-browser-dynamic
,typescript
и собственно,@angular/compiler-cli
. Лучше всего, устаналивать все локально в проекте. - Создать файл tsconfig.json (например, такой).
- Запустить компиляцию командой
"./node_modules/.bin/ngc" -p tsconfig.ngc.json
, либо при помощи gulp-плагина.
NGC построен на основе TypeScript, но построен, стоит сказать, плохо. Не все возможности TypeScript в нем работают, как надо. Например, наследование конфигураций не работает (поэтому в стартере 3 отдельных tsconfig-файла). В этой статье можно посмотреть, что еще не поддерживает AOT-компилятор. Список далеко не полный (вот, например), поэтому будьте готовы, что с этим будут проблемы. Компилятор может "упасть" где-то в своих недрах или уйти в бесконечный цикл, и выяснить причину не всегда просто. Проверять, что все компилируется нужно часто, чтобы потом не разбираться со всем разом.
Конфигурационный файл выглядит в основном также, как и основной tsconfig. Однако, компилятор порождает множество файлов, захламлять которыми папку с исходниками неприятно. Поэтому в конфигурации желательно указать папку, куда будут помещены результаты компиляции:
"angularCompilerOptions": {
"genDir": "app-aot"
}
Это актуально еще и потому, что компилятор обрабатывает также компоненты самого Angular. Поэтому если не указать genDir, то часть результатов появится в папке node_modules. Это как минимум странно.
Стоит обратить внимание, что AOT-файлы ссылаются на основные исходники по относительным путям. Поэтому, взаимное расположение папок важно.
Релизная компиляция TypeScript
Отличие релизной компиляции от обычной заключается, во-первых, в том, что необходимо создать отдельный main.ts файл. Во время разработки его следует исключить из компиляции, а в релизной сборке, наоборот, заменить им dev-версию. Отличие этого файла в том, что используется специальная bootstrap функция, которая задействует результаты AOT-компиляции. В частности, мы запускаем AppModuleNgFactory (результат компиляции AppModule) из genDir AOT-компиляции:
import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from '../app-aot/app/app.module.ngfactory';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
Также здесь мы включаем продакшн режим для Angular (это важно сделать, так как сильно влияет на производительность):
import { enableProdMode } from "@angular/core";
enableProdMode();
Второе отличие релизной компиляции — использование целевой платформы ES6. Если этого не сделать, Rollup не выдаст ошибки, но и tree-shaking не выполнит. По этой же причине, нам необходима ES6 версия rxjs. Раньше у rxjs был специальный пакет rxjs-es, и все примеры сборки Angular на gulp, которые показывает гугл на первых страницах, используют именно его. К сожалению, данный пакет перестали поддерживать. Поэтому нам необходимо самим компилировать rxjs из TypeScript исходников, как было описано выше.
Конфигурация релизной компиляции включает папки app
, и app-aot
(genDir AOT-компиляции) и исключает dev main.ts
, как было описано выше. Также, для порядка, в моем стартере результаты prod-компиляции помещаются в temp/app-prod-compiled. Все это находится в файле build.prod.js.
Tree-shaking при помощи библиотеки Rollup
Сборка при помощи Rollup — это ключевой этап сборки, способный превратить 1 Мб исходников в 100 Кб. Rollup анализирует исходники, и выбрасывает из них те участки кода, которые не используются.
На вход он принимает один единственный файл — main.js (точнее main-aot.js), анализируя import выражения, в котором собираются все остальные модули. Отсюда следует, что Rollup должен уметь находить нужные библиотеки. Большинство проблем решает плагин rollup-plugin-node-resolve
, который находит библиотеки в node_modules. Его использование прописывается в соответствующем конфигурационном файле.
В случае, если нужно сделать что-то специфичное, то легко написать свой плагин. Например, таким образом я указываю Rollup, что rxjs нужно брать из той самой папки, где лежит наша скомпилированная ES6 версия (RollupNG2 в том же rollup-config).
Из особенностей конфигурации, стоит отметить параметр treeshake: true
(разумеется), context: 'window'
(говорим, что собираем для браузера) и format: 'iife'
. Формат IIFE позволит обойтись без SystemJS, просто добавив результирующий файл как script-тэг в index.html.
Транспиляция результата в ES5 при помощи TypeScript довольно проста, главное выставить параметр allowJs. Операция занимает пару строчек в файле bundling.js в функции rollupBundle.
Подготовка релизного index.html
После всей проделанной выше работы, нам остается только собрать все вспомогательные библиотеки в один бандл, и добавить результат работы rollup на страницу через script-тэг. Все это стандартные для gulp задачи.
В стартере все это сделано из расчета на максимальную простоту, чтобы не заставлять пользователей лишний раз разбираться. Найти соответствующий код можно в файле build-prod.js. Для тестирования, там также настроен express-сервер, со включенным gzip-сжатием.
В итоге получаем 118 Кб после gzip:
В примере используется Tour of Heroes из официальных руководств Angular, который не совсем "Hello world". Если совсем упростить, то может получиться вплоть до 50-80 Кб.
По ссылкам ниже можно попробовать обе версии сборки вживую:
→ DEV версия online
→ PROD версия online
В заключении, хочу порекомендовать отменную статью по теме Minko Gechev. В ней он приводит пример простейшей сборки из 6 npm-скриптов, которая выполняет все основные шаги (учтите, что там используется rxjs-es, который больше не поддерживается). Правда seed-проект за его авторством мне не понравился, из-за высокой сложности и не очень высокого удобства.
На этом все, удачи!