...

пятница, 10 февраля 2017 г.

Angular — настройка среды разработки и production сборки с AOT-компиляцией и tree-shaking (Gulp, Rollup, SystemJS)

Одна из особенностей Angular, присущая и первой и новой версии — высокий порог вхождения. Новый Angular, помимо всего прочего, трудно даже запустить. А и запустив, легко получить 1-2 Мб скриптов и порядка нескольких сотен запросов при загрузке hello world страницы. Можно, конечно, использовать всякие стартеры, seed'ы или Angular CLI, но для использования в серъезном проекте нужно самому во всем разбираться.


В этой статье я постараюсь описать, как настроить удобную среду разработки с использованием SystemJS, и production сборку Angular приложения на основе Rollup, с выходом около 100кб скриптов и нескольких запросов при открытии страницы. Использовать будем TypeScript и SCSS.


Попробовать все в деле можно в моем angular-gulp-starter проекте.


Среда разработки


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


Технически, нам нужно решить три задачи:


  1. Скомпилировать TypeScript
  2. Скомпилировать SCSS
  3. Загрузить все (в т.ч. зависимости) в браузер в нужном порядке

Первые две задачи удобнее всего решать при помощи функции 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-файл, который может выглядеть примерно так:


Пример конфигурации SystemJS для Angular приложения
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"]
});

Здесь мы делаем следующее:


  1. Указываем, чтобы SystemJS автоматически подставляла расширения js к файлам (defaultJSExtensions).
  2. Указываем, что если не указано иное, искать все в папке node_modules ("*": "node_modules/*"). Это позволит легко устанавливать зависимости через npm.
  3. Прописываем, что модули, начинающиеся с app, нужно загружать не из node_modules, а из папки app (наша основная папка со скриптами). Это используется только в index.html, где импортируется app/main.
  4. Прописываем пути к angular модулям. В идеале, это должно происходить автоматически благодаря параметру packageConfigPaths, но у меня не получилось заставить его работать (что я сделал не так?).
  5. Если какая-то сторонняя библиотека не находится автоматически, то также прописываем путь к ней явно.

После этого, нам достаточно включить в 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 в один бандл для ускорения загрузки страницы в dev-окружении

Сборка этого 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, не JSPM, не Browserify

Webpack


Angular CLI и многие starter и seed проекты используют Webpack. Он теперь умеет делать tree-shaking, и говорят, даже hot module reloading (кто-нибудь пробовал именно в контексте Angular?). Но я не разделяю ажиотажа вокруг этого сборщика, и не понимаю, откуда он берется. Webpack это бандлер, и он может только построить бандл. Это порождает множество проблем:


  1. Сборка занимает некоторое значительное время (как минимум, несколько секунд). Мы не можем использовать compile-on-save, который намного быстрее (по крайней мере, это не так просто).
  2. Да, можно использовать watch, так что при сохранении изменений сборка будет запускаться автоматически. Но это не решает проблемы. 1. На практике все выглядит так: я ввожу часть кода, сохраняю, запускается сборка, пока она длится, я ввожу следующий код и сохраняю — в результате получается устаревший бандл, без последних правок. Кто-нибудь сталкивался с такой проблемой? Как вы ее решаете?
  3. Если у вас не работают 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 включает в себя следующие этапы:


  1. Ahead of time (AOT) компиляция шаблонов (html и css части компонентов). В dev-среде они компилируются прямо в браузере, однако для релиза лучше делать это заранее. Тогда нам не придется тащить в браузер код компилятора, повысится эффективность tree-shaking, немного ускорится запуск приложения.
  2. Компиляция TypeScript в ES6 (включая результаты первого шага). Нужен именно ES6, т.к. Rollup умеет работать только с ES6. Компилируем также SCSS, запускаем пост-процессинг.
  3. Сборка бандла с использованием tree-shaking при помощи Rollup. В результате из кода удаляются все неиспользуемые части, и размер скриптов сокращается в десятки раз.
  4. Транспиляция результата в ES5 при помощи того же TypeScript, минификация.
  5. Подготовка релизного index.html, копирование файлов в dist.

AOT-компиляция


AOT-компиляция осуществляется при помощи пакета @angular/compiler-cli (называемый также ngc), который построен на базе компилятора TypeScript. Для выполнения компиляции нужно:


  1. Установить пакеты: @angular/compiler, @angular/core, @angular/platform-browser-dynamic, typescript и собственно, @angular/compiler-cli. Лучше всего, устаналивать все локально в проекте.
  2. Создать файл tsconfig.json (например, такой).
  3. Запустить компиляцию командой "./node_modules/.bin/ngc" -p tsconfig.ngc.json, либо при помощи gulp-плагина.

Неприятные особенности AOT-компилятора

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-проект за его авторством мне не понравился, из-за высокой сложности и не очень высокого удобства.


На этом все, удачи!

Комментарии (0)

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий