Angular 1.x: скрадливий webpack, що крадеться grunt

Історія про те, як ми поміняли складання проекту з grunt на webpack
Приходиш на роботу, відкриваєш IDE, пишеш
npm start
, запускаючи систему збирання, починаєш працювати. Тобі зручно орієнтуватися в структурі проекту, зручно налагоджувати код і стилі, очевидно, як саме і в якому порядку збирається проект.

Проходить два роки. У процесі розробки періодично замислюєшся, куди правильно покласти файли з новим модулем, як бути з загальними ресурсами, і не завжди з ходу відповідаєш на питання джуніора «а яким чином цей файл взагалі потрапляє в бандл?». Чи відповідаєш сакральне «так історично склалося» і сумуєш за тим, що було два роки тому.

Як з'ясувалося, таке трапляється, якщо не модернізувати систему складання разом із зростанням проекту. Хороша новина в тому, що це успішно лікується! Влітку ми підтвердили це в бою і хочемо поділитися досвідом.



Вихідна ситуація
Ми розробляємо пакет офісних додатків МойОфис з 2013 року, web-версію (про яку і піде мова) – з 2014-го.

Є кілька суміжних проектів (файловий менеджер, авторизація і профіль, веб-редактор документів) з загальними сабрепозиториями, кожен з яких представляє собою SPA-частина великої програми МойОфис. Розробка ведеться на
angular 1.5
, для continuous integration використовується jenkins.

Вихідна система складання на grunt, що складається з складносурядних взаємозалежних завдань, була створена на початку проекту і мало змінювалася з тих пір. Щоб визначити масштаби: dev-збірка запускала близько 30 grunt-тасок, 30% з яких збирали модулі і стилі, 70% – перекладали зображення і шрифти, оновлюючи посилання на них. Порядок виконання критично важливий, однак інформацію про взаємозалежність можна було отримати лише від колег.

Навіщо мігрувати і чому webpack
Збирати angular-проект насправді не так вже й складно: потрібно всього лише сконкатенировать всі вихідні файли в один, не забуваючи, щоб модуль був оголошений раніше його контролера. Ми робили ще простіше: збирали взагалі всі файли з папки src (використовуючи `_` на початку імені файлу для забезпечення правильного порядку підключення), додавали масив зовнішніх пакетів, а далі підключали файли прямо в head (звичайно, тільки для dev-складання, для production код конкатенировался в бандл з подальшою обфускацией і минификацией).



Збірка очевидно застаріла морально. Останні критичні зміни були датовані 2015 роком, що в умовах сучасного фронтенда можна прирівнювати до оплетенному павутиною кутку, в якому вже, відверто кажучи, немодний grunt зберігає свої проміжні файли.
Плюс у неї був тільки один: пересборка проекту була умовно безкоштовної зважаючи прямого підключення файлів в head.

Мінусів набагато більше:

  • Збираючи файли по масці, ми не могли розробляти незалежні модулі.
  • HTTP-запитів в dev-режимі вимірювалося сотнями, а час перезавантаження сторінки в такому режимі було на кілька секунд більше, ніж у зібраному додатку.
  • з'єднання
    *.js
    масці в проект потрапляли невикористовувані модулі.
  • " При додаванні нового js-файлу доводилося перезавантажувати всю збірку.
  • Для підключення сторонніх залежностей ми зберігали окремий json з іменами модулів.
  • Grunt змушував створювати велику кількість проміжних і конфіг-файли, з-за яких наш
    .hgignore
    містив у собі більше 50 рядків.
І чим більше розширювався наш проект, тим сильніше заважали недоліки білд-системи.

Відійшовши на крок назад, глянувши на себе, на інших, на тренди, згадавши досвід попередніх проектів, ми вибрали webpack, який ефективно вирішує ці проблеми.

Організація робіт


Головний секрет успішного рефакторінгу – заздалегідь чітко визначити кроки і скласти план.

  1. Скласти список всіх вимог.
  2. Реалізувати proof of concept на невеликій ділянці проекту. Ідея в тому, щоб зібрати всі можливі граблі дешево і у фоновому режимі, без ризиків для основної розробки.
  3. Здійснити повний перехід з урахуванням всіх тонких місць, виявлених у п. 2. Знаючи всі проблеми і маючи досвід перекладу частини коду на нові рейки, можна досить точно оцінити трудомісткість.
Реверс-інжиніринг вимог
Нова система повинна була не тільки вирішити існуючі проблеми збірки, але і підтримати кілька нових і давно бажаних фіч (і, звичайно ж, не втратити старих). Склали перелік того, що повинен уміти webpack в готовому вигляді:
  • Інкрементальний білд.
  • Watch mode.
  • Підтримка source map (по прапору).
  • Минификация (по прапору).
  • Hot module replacement.
  • Підтримка Babel.
  • Dead code elimination.
  • Розділити вендорний код і наш на два пакету.
  • Додавати хеш до імен файлів.


При цьому grunt з процесу виключати не можна, так як він відповідає за складання стилів, роботу з зображеннями і шрифтами, генерацію документації. Для однаковості хочемо навіть webpack запускати через grunt, а не через npm-сум, щоб взагалі не міняти команду для складання проекту і нічого не перенастроювати на CI.

Proof of concept
На розтерзання було віддано одну з наших програм – СПА, що відповідає за всі маніпуляції з аутентифікацією і профілем користувача. По закінченні робіт з ним можна було б братися за інші.

По-хорошому вся робота розбивалася на три частини:

1. Створити конфіг для webpack.
2. Підготувати файли для такої збірки. Формати файлів:
  • html
  • js,
  • css,
  • media (картинки і шрифти),
  • велика кількість конфігів, що зберігаються в
    json
    та інтегруються в збірку десь посередині.
3. Переписати юніт-тести.

Css разом з media тимчасово відклали, так як вони не інтегровані в angular і можуть продовжувати жити своїм життям.

js-модулі

Для тих, хто вже давно не заглядав у angular, нагадаємо, як він виглядає зсередини:
// module.js
angular.module('moduleName',[
'dependencyOneName',
'dependencyTwoName' 
])
.controller('SomeController', function(){...})
.directive('someDirecive', function(){});

// someService.js
angular.module('moduleName')
.service('SomeService', function(){...});

Головне, що нас турбує у випадку з webpack: всі залежно вказані просто рядком-ім'ям необхідного модуля. Для побудови графа залежностей в webpack ж необхідно явно вказати, який файл підключити.

З часом утворився такий план:
//module.js
module.exports = angular.module('moduleName', [ 
require('path/to/firstDependency'), 
require('path/to/secondDependency')
])
.controller(...require('controller.js')) //es6 spread syntax feature yay!
.name;

//controller.js
module.exports = ['SomeController', function(){}];

За рахунок використання es6 spread syntax ми змогли витончено уникнути дублювання імені модуля при оголошенні компонента.

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

HTML-шаблони

Шаблони діляться на дві категорії:
index.html
і всі інші. Збирати
index.html
нескладно за допомогою html-webpack-plugin. Всіма іншими раніше займався grunt-ng-template. Довелося пошукати webpack-плагін для роботи з шаблонами. Вимог до нього було всього два:

  • Щоб всі шаблони, згадані в модулях, тут же потрапляли в $templateCache.
  • Щоб всі внутрішні підключення шаблонів (ng-include) теж оброблялися.
З першим пунктом було впоратися легко, а з другим виникли проблеми. Досі не існує відповідного рішення, і, хоча написати його нескладно, для нас підключити всі такі шаблони руками в js було швидше. У майбутньому ми хочемо розробити webpack-лоадер для цих цілей. Якщо ви вже написали такий самі – поділіться з нами в коментарях посиланням на github.

C попаданням в
$templateCache
цікавий нюанс: якщо робити
require
директиви або контролера, то в кеш він спробує додатися тільки в рантайме, не потрапляючи заздалегідь в бандл. З появою angular-компонентів це було виправлено, в інших місцях доводилося підключати шаблони до оголошення контролерів.

Щоб легко виявляти пропущені підключення шаблонів, ми додали в
webpack-dev-middleware
прошарок, що забороняє завантаження будь-яких вкладених
html
.
function blockLocalTemplatesMiddleware(req, res, next) {
var urlPath = parseUrl(req).pathname;
if (/[^\/]+\/[^\/]+\.html$/g.test(urlPath)) {
res.statusCode = 404;
res.end('Request to .html template denied');
} else {
next();
}
}

Конфіги

У кожного з наших проектів є конфігурації, зашиваемые в проект на етапі складання. Раніше всі конфіги зберігалися в декількох json-файлах, grunt-ng-constant загортала їх в angular-модуль і підключала до проекту на етапі складання, зменшуючи прозорість читання і налагодження. Використання DefinePlugin зробило це набагато зручніше і простіше.

Юніт-тести

  • Щоб не сповільнювати тестову збірку, для підключення всього, крім js, був використаний ignore-loader.
  • юніт-тестах довелося безпосередньо звертатися до angular.mock з-за webpack.
  • Почали масово падати тести, що використовують
    angular.element
    . Знатно поламавши голову, ми згадали, що
    angular.element
    використовує jQuery, але не тягне його з собою, тому бібліотеку треба підключати окремо в
    karma.config.js
    .
Остаточна міграція
Три тижні акуратного POC тому ми були готові до остаточної міграції всього додатка

image

Робота з css

Ми провели міграцію в червні-липні, про статті задумалися у серпні, до написання рішуче приступили в грудні і за цей час вже встигли звикнути до зручності модульної структури і зважилися на переклад збірки sass-стилів на webpack.

І хоча ця стаття спочатку передбачала розповідь тільки про першому етапі міграції, ми не можемо не поділитися досвідом відмови від
grunt-sass
.

Процес такої міграції по суті досить тривіальний: просто підключати всі необхідні стилі в модулях, де вони використовуються. Однак без підводних каменів також не обійшлося.

Як працювала збірка раніше? Подібно збірці js-модулів. По масці збиралися всі
*.scss
і импортились в одному файлі. Далі
sass
відпрацьовував на ньому одному, всі підключені один раз міксини і хелпери були доступні скрізь, перехресних импортов практично не спостерігалося.

Для реалізації модульної структури ми почали в кожному файлі стилів импортить змінні, міксини,
node-bourbon
, лунапарк, блекджек. З-за цього сталося дві біди:

  • за браком
    import-once
    (в ньому не було особливої потреби раніше) наші підсумкові
    .css
    настільки розпухли, що IE (в Chrome, Firefox і навіть Safari таких проблем, звичайно ж, не було) не був в силах їх розпарсити. Тобто сторінка завантажувалася,
    .css
    файл завантажувався, але усвідомити, що він повний стилів, IE був не в змозі. Ця проблема вирішилася простим додаванням
    import-once
    .
  • sass-loader
    , не володіє инкрементальным білдів, пересобирал проект всякий раз заново і з-за великої кількості точок входу і импортов в них витрачав на перезбирання близько 5 секунд. Впоратися з цим не змінюючи архітектури не представлялося можливим.
Проте оновлення до недавно вийшов
node-sass@4.0.0
прискорило перезбирання приблизно в 1,5 рази, і ми вирішили відкласти масову переробку стилів.

Налагодження і тестування

Головне в розробці – не написати, а налагодити, за результатами налагодження ми склали шпаргалку для тих, хто вирішить повторити шлях міграції (до речі, тут абсолютно неважливо, з чого злазити – з gulp або grunt). В основному всі зустрінуті на шляху дефекти та їх діагностика виглядали як:

  • «Нічого не збирається, в консолі IDE повно букв»: спроба підключити залежність, якої немає (невірний шлях або некоректний експорт).
  • «Зібралося, але нічого не працює, в консолі браузера дуже довгі помилки»: модулю не вистачає залежності. У старій складанні такої проблеми не було, бо всі модулі потрапляли в збірку і легко можна не згадувати необхідну залежність, не отримавши побічних ефектів. Тепер же ніде не згадані файли у бандл не потрапляють.
  • «Всі завантажилося, але при переході або кліці – падає»: на вибір три варіанти – попередній, брак шаблону
    $templateCache
    чи ж забули додати в збірку webworker.
  • “Додаток виглядає не так“: з-за зміни порядку стилів ми ще досить довго знаходили маленькі дефекти, спричинені початковим розрахунком на певний (алфавітний) порядок підключення файлів.
Окреме ручне тестування від QA-команди в цій задачі не знадобилося, було досить просто прогонки автотестів. Єдине, про що ми попросили тестувальників, перевірити, що jenkins успішно і коректно збирається з усіма можливими прапорами.

Технічні моменти

Можливостей оптимізувати процес роботи angular за допомогою webpack дуже багато. На github можна виявити десятки лоадерів (цікаво, що з того моменту, як ми завершили міграцію, до дня, коли був початий цей абзац, там вже з'явилися деякі плагіни, яких нам не вистачало тоді). Однак третина з них не має документації і містить тільки минифицированный код, тому користуватися ними не представляється можливим, друга третина працює неоднозначно (наприклад, існує три лодера для шаблонів, роблять начебто одне і те ж, а заробив коректно у нас тільки один).

Неминучі труднощі

  • У цьому розділі розповімо про труднощі, зустрінуті нами на шляху роботи з обраними інструментами.
  • Незручно експортувати ім'я модуля, незрозуміло, як вирішити цю проблему на angular 1.x.
  • не Можна підключити необхідну залежність і залишитися непойманным, якщо вона використовується в іншому модулі. Це виявляють юніт-тести, які запускаються ізольовано, але загальна тенденція не видається здоровою.
  • У деяких зовнішніх angular-модулів з залежностей немає експорту, це заподіює страждання і зменшує прозорість.
Наприклад:
require('ng-file-upload');
angular.module('app', ['ngFileUpload'])

Справитися з цим можна за допомогою батареї пулл-реквестов на github, у вільну хвилинку часом ми відправляємо їх.

  • Якщо писати модулі в форматі експорту функції, необхідно згадка
    @ngInject
    . Якщо це не зроблено, минифицированная версія не працює, а линтерами цю ситуацію не відстежити.
  • Webpack, виявляється, впадає в паніку, коли у нього є два способи отрезолвить файл.
Наприклад, структура проекту така:
├── src
│ └── module.js
└── common
└── src
└── module.js

// webpack.config.js
resolve: {
root: ['src','common/src']
}

//app.js
require('module.js');

При зміні файлу change detection не може відпрацювати коректно і проект абсолютно непередбачувано то пересобирается, то ні. Так як у нас було вказано декілька вхідних точок з однаковою внутрішньою структурою, довелося відмовитися від цього.

  • Не всі способи минификации працюють на 100% однаково. Раніше ми використовували grunt-contrib-uglify, зараз перейшли на UglifyJsPlugin. Незважаючи на однакові параметри, при переході виникла проблема з тим, що одна з бібліотек почала вважати російські символи в HTML-шаблонів небезпечними і перетворювала їх у двічі екрановані HTML entities. Логічному поясненню подібні випадки не піддаються, зате ілюструють користь частого тестування коду, зібраного з налаштуваннями, використовуваними для production.

Несподівані бенефіти

  • Ми завжди створювали два бандла – код сторонніх бібліотек і наш. З webpack поділ відбувається через СommonsChunksPlugin. Спочатку ми тримали дві точки входу, на які застосовували СommonsChunkPlugin, але знайшли відмінне хитре рішення.
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
chunks: ['app'],
filename: 'vendor.[hash].js',
minChunks: function minChunks(module) {
return module.resource
&& module.resource.indexOf('node_modules') > 0;

}
})

Навіщо розділяти код на дві частини? Щоб використовувати DLL, прискорюючи перезбирання. Плюс при досить частих релізах (а ми прагнемо збільшити їх частоту) список залежностей не встигає змінитися, зберігаючи той же hash. Це дозволяє користувачеві завантажувати зайвий файл, а просто брати його з кешу браузера.

  • Використовуючи opensource-бібліотеки, ми зобов'язані вказувати імена їх авторів. З webpack стало дуже зручно збирати цю інформацію за допомогою license-webpack-plugin, орієнтується на шляху до підключеному модулю.
Не пішло в роботу

Автозавантаження модулів

Звичайно, переписувати всі залежності модулів з рядків на
require
нам не дуже хотілося. Круто було б прикрутити
loader
, який би аналізував код і сам підставляв потрібні
require
!

Однак такий підхід вимагає суворої структури проекту, щоб можна було однозначно зіставляти шлях до модуля з його ім'ям. Фактично рядок з ім'ям модуля однозначно трансформувалась би в шлях до файлу. На момент міграції структура проекту не мала до такого підходу, а реорганізація файлів зайняла б не менше часу і загрожувала великою кількістю конфліктів при об'єднанні гілок.

Зараз ми починаємо шлях до суворої організації вихідного коду і, коли закінчимо, зможемо скористатися такими можливостями. Хоча цього, швидше за все, не захочеться, тому що переходити
ctrl-click
відразу в залежний модуль вкрай зручно.

Hot module replacement

На жаль, від HMR для js-коду ми змушені були відмовитися. Існує два плагіна, але обидва вони вимагають не тільки дуже суворої структури проекту, але і точного формату експорту, а також працюють тільки з контролерами, але не з директивами. Навіть при відповідній структурі користуватися оновленням тільки для частини коду зовсім незручно. Однак для стилів HMR працює коректно.

Поради собі в минуле

Процес міграції пройшов досить гладко і поступенчато, однак, як це зазвичай трапляється, завершивши роботу, ми придумали, як можна було її полегшити:

  • Замість того, щоб вручну замінювати всі рядкові імена залежностей на
    require()
    , простіше написати одноразовий nodejs-скрипт, аналізує поточну кодову базу і сам документ імена модулів на шляху до них.
  • Неперевірений рада! Можливо, має сенс спочатку переписати код з використанням browserify, а потім вже приробляти до нього webpack, щоб не розбиратися, у чому саме тонни внесених змін, проблема – в неправильних шляхах підключаються файлів або ж у самому збирача.
Про цифрах
Найцікавіше це, звичайно ж, цифри. Цікаві логи завдань:



Пере — і недооцінка мають місце бути, але в цілому нам вдалося спрогнозувати трудомісткість досить точно. Запорукою цього успіху ми вважаємо чітке формулювання вимоги до початку реалізації завдання. Досвід попередніх і наступних масових рефакторингов підтверджує це: якщо братися за завдання, плануючи скласти список вимог у процесі розробки, — забути щось важливе легше легкого.

Час складання

На старій складанні при виявленні змін в js сторінка починала перезавантаження відразу ж. Якщо були внесені зміни в стилі, пересборка css займала близько 3,5 секунд.
Після переїзду пересборка відбувається за 5 секунд незалежно від того, де були внесені зміни.

Час завантаження сторінки в dev-версії на старій збірці займало близько 1,5 секунд з-за великої кількості підключаються js-файлів. Після переходу на webpack воно скоротилося до 0,8 секунди. При зміні стилів, як тоді, так і зараз, перезавантаження не потрібно.

Таким чином, отримуємо наступні дані. У таблиці вказано час від внесення змін до їх застосування на сторінку:



Висновки

Мінуси:
  • час від внесення змін до перезавантаження сторінки збільшилася
Плюси:
  • масштабованість проекту зросла – тепер додати новий плагін або loader (підключити babel або postcss) набагато простіше
  • перехід на модульну структуру нарешті став можливим
  • легко орієнтуватися по залежностях модуля за допомогою ctr+click
  • бандл не потрапляють зайві файли
  • стало зручніше збирати інформацію про сторонніх ліцензіях і відокремлювати opensource-код від нашого
  • при додаванні нових файлів не потрібно перезапускати всю збірку заново
  • позбавилися від довгого заплутаного списку grunt-тасок, замінивши його на список webpack-плагінів, користуватися якими куди зручніше
  • можна перемикатися з гілки на гілку, не перезапуская працюючу збірку
Плани на майбутнє:
  • прискорити збирання
  • навчитися збирати assets, використовувані в стилях, за допомогою postcss-плагінів, а решта – webpack'ом
  • вжити заходів з підтримки HMR для будь-яких змін
В цілому орієнтуватися в проекті стало простіше, поріг входження для нового співробітника став нижче, рефакторинг – доступніше, відстеження залежностей – зручніше. Тепер можна розробляти окремі модулі і не побоюватися, що частина коду або css потрапить у загальний пакет.

Було б прикро прочитати таку довгу статтю і не отримати в кінці бонус! Ми прикладаємо для вас готові конфіги для webpack і karma!
Джерело: Хабрахабр

0 коментарів

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