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/вами/bindCallback'; 
System && System.set && System.set('rxjs/add/вами/bindCallback', System.newModule(pkg0));

import * as pkg1 from 'rxjs/add/вами/bindNodeCallback'; 
System && System.set && System.set('rxjs/add/вами/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, не BrowserifyWebpack
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 исходников, як було описано .
<a href=«github.com/PFight/angular-gulp-starter/blob/master/tsconfig.prod.json>Конфігурація релизной компіляції включає папки
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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.