- PVSM.RU - https://www.pvsm.ru -
Приходишь на работу, открываешь IDE, пишешь npm start
, запуская систему сборки, начинаешь работать. Тебе удобно ориентироваться в структуре проекта, удобно отлаживать код и стили, очевидно, как именно и в каком порядке собирается проект.
Проходит два года. В процессе разработки периодически задумываешься, куда правильно положить файлы с новым модулем, как быть с общими ресурсами, и не всегда с ходу отвечаешь на вопрос джуниора «а каким образом этот файл вообще попадает в бандл?». Или отвечаешь сакральное «так исторически сложилось» и тоскуешь по тому, что было два года назад.
Как выяснилось, такое случается, если не модернизировать систему сборки вместе с ростом проекта. Хорошая новость в том, что это успешно лечится! Летом мы подтвердили это в бою и хотим поделиться опытом.
Мы разрабатываем пакет офисных приложений МойОфис с 2013 года, web-версию (о которой и пойдет речь) – с 2014-го.
Имеется несколько смежных проектов (файловый менеджер, авторизация и профиль, веб-редактор документов) с общими сабрепозиториями, каждый из которых представляет собой SPA-часть большого приложения МойОфис. Разработка ведется на angular 1.5
, для continuous integration используется jenkins.
Исходная система сборки на grunt, состоящая из сложносочиненных взаимозависимых задач, была создана на заре проекта и мало менялась с тех пор. Чтобы обозначить масштабы: dev-сборка запускала около 30 grunt-тасок, 30% из которых собирали модули и стили, 70% – перекладывали изображения и шрифты, обновляя ссылки на них. Порядок выполнения был критически важен, однако информацию о взаимозависимости можно было получить только от коллег.
Собирать angular-проект на самом деле не так уж и сложно: нужно всего лишь сконкатенировать все исходные файлы в один, не забывая, чтобы модуль был объявлен раньше его контроллера. Мы делали еще проще: собирали вообще все файлы из папки src (используя `_` в начале имени файла для обеспечения правильного порядка подключения), добавляли массив внешних пакетов, а дальше подключали файлы прямо в head (конечно, только для dev-сборки, для production код конкатенировался в бандл с последующей обфускацией и минификацией).
Сборка очевидно устарела морально. Последние критические изменения были датированы 2015 годом, что в условиях современного фронтенда можно приравнивать к оплетенному паутиной углу, в котором уже, откровенно говоря, немодный grunt хранит свои промежуточные файлы.
Плюс у нее был только один: пересборка проекта была условно бесплатной ввиду прямого подключения файлов в head.
Минусов же гораздо больше:
*.js
маске в проект попадали неиспользуемые модули..hgignore
содержал в себе более 50 строк.И чем больше расширялся наш проект, тем сильнее мешали недостатки билд-системы.
Отойдя на шаг назад, взглянув на себя, на других, на тренды, вспомнив опыт предыдущих проектов, мы выбрали webpack, который эффективно решает вышеописанные проблемы.
Главный секрет успешного рефакторинга – заранее четко определить шаги и составить план.
Новая система должна была не только решить существующие проблемы сборки, но и поддержать несколько новых и давно желаемых фич (и конечно же, не лишиться старых). Составили список того, что должен уметь webpack в готовом виде:
При этом grunt из процесса исключать нельзя, так как он отвечает за сборку стилей, работу с изображениями и шрифтами, генерацию документации. Для единообразия хотим даже webpack запускать через grunt, а не через npm-таску, чтобы вообще не менять команду для сборки проекта и ничего не перенастраивать на CI.
На растерзание было отдано одно из наших приложений – SPA, отвечающее за все манипуляции с аутентификацией и профилем пользователя. По окончании работ с ним можно было бы приниматься за остальные.
По-хорошему вся работа разбивалась на три части:
1. Cоздать конфиг для webpack.
2. Подготовить файлы для такой сборки. Форматы файлов:
json
и интегрирующихся в сборку где-то посередине.3. Переписать юнит-тесты.
Css вместе с media временно отложили, так как они не интегрированы в angular и могут продолжать жить своей жизнью.
Для тех, кто уже давно не заглядывал в 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 [4] мы смогли изящно избежать дублирования имени модуля при объявлении компонента.
Так как формат подключения зависимостей менялся критически, в рамках POC нельзя было трогать общие сабрепозитории, чтобы не зацепить другие проекты. Поэтому все общие файлы пришлось методом тыка подключать вручную длинным-предлинным списком.
Шаблоны делятся на две категории: index.html
и все остальные. Собирать index.html
несложно при помощи html-webpack-plugin [5]. Всеми остальными раньше занимался grunt-ng-template [6]. Пришлось поискать webpack-плагин для работы с шаблонами. Требований к нему было всего два:
С первым пунктом справиться было легко [7], а со вторым возникли проблемы. До сих пор не существует подходящего решения, и, хотя написать его несложно, для нас подключить все такие шаблоны руками в js было быстрее. В будущем мы хотим разработать webpack-лоадер для этих целей. Если вы уже написали такой сами – поделитесь с нами в комментариях ссылкой на github.
C попаданием в $templateCache
интересный нюанс: если делать require
внутри директивы или контроллера, то в кеш он попытается добавиться только в рантайме, не попадая заранее в бандл. С появлением angular-компонентов [8] это было исправлено, в остальных местах приходилось подключать шаблоны до объявления контроллеров.
Чтобы легко выявлять пропущенные подключения шаблонов, мы добавили в 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 [9] заворачивала их в angular-модуль и подключала к проекту на этапе сборки, уменьшая прозрачность чтения и отладки. Использование DefinePlugin [10] сделало это гораздо удобнее и проще.
angular.element
. Знатно поломав голову, мы вспомнили, что angular.element
использует jQuery, но не тянет его с собой, поэтому библиотеку надо подключать отдельно в karma.config.js
.Три недели аккуратного POC спустя мы были готовы к окончательной миграции всего приложения целиком.
Мы провели миграцию в июне-июле, о статье задумались в августе, к написанию решительно приступили в декабре и за это время уже успели привыкнуть к удобству модульной структуры и решились на перевод сборки 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). В основном все встреченные на пути дефекты и их диагностика выглядели как:
$templateCache
или же забыли добавить в сборку webworker.Отдельное ручное тестирование от QA-команды в этой задаче не потребовалось, было достаточно просто прогонки автотестов. Единственное, о чем мы попросили тестировщиков, проверить, что jenkins успешно и корректно собирается со всеми возможными флагами.
Возможностей оптимизировать процесс работы angular при помощи webpack очень много. На github можно обнаружить десятки лоадеров (любопытно, что с того момента, как мы завершили миграцию, до дня, когда был начат этот абзац, там уже появились некоторые плагины, которых нам не хватало тогда). Однако треть из них не имеет документации и содержит только минифицированный код, поэтому пользоваться ими не представляется возможным, вторая треть работает неоднозначно (например, существует три лоадера для шаблонов, делают вроде одно и то же, а заработал корректно у нас только один).
Например:
require('ng-file-upload');
angular.module('app', ['ngFileUpload'])
Справиться с этим можно при помощи батареи пулл-реквестов на github, в свободную минутку мы порой отправляем их.
@ngInject
. Если это не сделано, минифицированная версия не работает, а линтерами эту ситуацию не отследить. Например, структура проекта такова:
├── src
│ └── module.js
└── common
└── src
└── module.js
// webpack.config.js
resolve: {
root: ['src','common/src']
}
//app.js
require('module.js');
При изменении файла change detection не может отработать корректно и проект совершенно непредсказуемо то пересобирается, то нет. Так как у нас было указано несколько входных точек с одинаковой внутренней структурой, пришлось отказаться от этого.
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 [16], ускоряя пересборку. Плюс при достаточно частых релизах (а мы стремимся увеличить их частоту) список зависимостей не успевает поменяться, сохраняя тот же hash. Это позволяет пользователю не загружать лишний файл, а просто брать его из кеша бразуера.
Конечно, переписывать все зависимости модулей со строк на require
нам не очень хотелось. Круто было бы прикрутить loader
, который бы анализировал код и сам подставлял нужные require
!
Однако такой подход требует строгой структуры проекта, чтобы можно было однозначно сопоставлять путь к модулю с его именем. Фактически строка с именем модуля однозначно трансформировалась бы в путь к файлу. На момент миграции структура проекта не располагала к такому подходу, а реорганизация файлов заняла бы не меньше времени и грозила большим количеством конфликтов при объединении веток.
Сейчас мы начинаем путь к строгой организации исходного кода и, когда закончим, сможем воспользоваться такими возможностями. Хотя этого, скорее всего, не захочется, потому что переходить по ctrl-click
сразу в зависимый модуль крайне удобно.
К сожалению, от HMR для js-кода мы вынуждены были отказаться. Существует два плагина [18], но оба они требуют не только очень строгой структуры проекта, но и точного формата экспорта, а также работают только с контроллерами, но не с директивами. Даже при подходящей структуре пользоваться обновлением только для части кода совершенно неудобно. Однако для стилей HMR работает корректно.
Процесс миграции прошел довольно гладко и поступенчато, однако, как это обычно случается, уже завершив работу, мы придумали, как можно было ее облегчить:
require()
, проще написать одноразовый nodejs-скрипт, анализирующий текущую кодовую базу и сам заменяющий имена модулей на пути к ним.Самое интересное это, конечно же, цифры. Интересные логи по задачам:
Пере- и недооценка имеют место быть, однако в целом нам удалось спрогнозировать трудоемкость довольно точно. Залогом этого успеха мы считаем четкую формулировку требования до начала реализации задачи. Опыт предыдущих и последующих массовых рефакторингов подтверждает это: если браться за задачу, планируя составить список требований в процессе разработки, — забыть что-то важное легче легкого.
На старой сборке при обнаружении изменений в js страница начинала перезагрузку сразу же. Если были внесены изменения в стили, пересборка css занимала около 3,5 секунд.
После переезда пересборка происходит за 5 секунд независимо от того, где были внесены изменения.
Время загрузки страницы в dev-версии на старой сборке занимало около 1,5 секунд из-за большого количества подключаемых js-файлов. После перехода на webpack оно сократилось до 0,8 секунды. При изменении стилей, как тогда, так и сейчас, перезагрузки не требуется.
Таким образом, получаются следующие данные. В таблице указано время от внесения изменений до их применения на странице:
Минусы:
Плюсы:
Планы на будущее:
В целом ориентироваться в проекте стало проще, порог вхождения для нового сотрудника стал ниже, рефакторинг – доступнее, отслеживание зависимостей – удобнее. Теперь можно разрабатывать отдельные модули и не опасаться, что часть кода или css попадет в общий бандл.
Было бы обидно прочитать такую длинную статью и не получить в конце бонус! Мы прикладываем для вас готовые конфиги для webpack и karma [20]!
Автор: Новые Облачные Технологии
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/243129
Ссылки в тексте:
[1] proof of concept: https://en.wikipedia.org/wiki/Proof_of_concept
[2] Инкрементальный билд: https://en.wikipedia.org/wiki/Incremental_build_model
[3] Dead code elimination.: https://en.wikipedia.org/wiki/Dead_code_elimination
[4] spread syntax: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator
[5] html-webpack-plugin: https://github.com/jantimon/html-webpack-plugin
[6] grunt-ng-template: https://www.npmjs.com/package/grunt-ng-template
[7] легко: https://github.com/WearyMonkey/ngtemplate-loader
[8] angular-компонентов: https://docs.angularjs.org/guide/component
[9] grunt-ng-constant: https://github.com/werk85/grunt-ng-constant
[10] DefinePlugin: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
[11] ignore-loader: https://www.npmjs.com/package/ignore-loader
[12] webpack: http://stackoverflow.com/questions/32499108/karma-jasmine-webpack-module-is-not-a-function/32584798#32584798
[13] grunt-contrib-uglify: https://github.com/gruntjs/grunt-contrib-uglify
[14] UglifyJsPlugin: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
[15] СommonsChunksPlugin: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
[16] DLL: http://robertknight.github.io/posts/webpack-dll-plugins/
[17] license-webpack-plugin: https://www.npmjs.com/package/license-webpack-plugin
[18] два плагина: https://github.com/yargalot/Angular-HMR
[19] browserify: http://browserify.org
[20] конфиги для webpack и karma: https://gist.github.com/newcloudtech/b5001f993cb2e2974725ff58962bf01b
[21] Источник: https://habrahabr.ru/post/321584/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.