Привет, Хабр! Меня зовут Василий Беляев. Я руководитель группы разработки по направлению фронтенда в ИТ-компании «Криптонит». В этой статье я расскажу про организацию работы с версиями и компонентами, оптимизацию рабочего процесса внутри команды, а также опишу несколько лайфхаков, которые мы применили.

В прошлый раз мы остановились на том, что оптимизировали package.json и избавились от прямых импортов. Это было необходимое, но недостаточное условие. Прежде, чем рассказывать о следующих шагах, я отвечу на те вопросы, которые подвисли с прошлой статьи.

Почему мы используем vite?

На самом деле всё просто. При инициализации проекта через create vue@latest по умолчанию ставится сборщик vite. На мой взгляд, у него есть ряд плюсов перед webpack. Главный из них заключается в том, что, с точки зрения Developer Experience, значительно быстрее работает hot reload, особенно для объёмных проектов.

Почему мы выбрали Yarn?

Дополнительно к Node.js ставится пакетный менеджер. Это инструмент, который автоматизирует установку, обновление и удаление сторонних библиотек (пакетов), а также управляет их версиями и зависимостями. Без него разработчику пришлось бы вручную скачивать все необходимые файлы, отслеживать их совместимость и переносить изменения при обновлениях. Он также позволяет запускать вспомогательные скрипты (сборка, тестирование), указанные в поле scripts файла package.json. 

По умолчанию в качестве пакетного менеджера ставится npm, также существуют Yarn Classic (1.x), pnpm, Bun, Yarn Berry (2.x и выше). 

Мы раньше использовали npm, но потом перешли на Yarn по разным причинам. Для меня появился очень важный момент, связанный с подходами при работе с peerDependencies при установке зависимостей. 

При работе с npm устанавливаются все зависимости, указанные в peerDependencies. В некоторых случаях это может вызвать конфликты версий при установке пакетов и привести к неожиданным результатам. Также это может повлиять на скорость развёртывания на prod стенде, так как с дополнительным флагом (--prod/--production) они тоже устанавливаются. 

Мы провели эксперимент, выполнив следующие действия и зафиксировав количество установленных зависимостей:

1. Инициировали проект (для этого требуется node, npm версии 7.x и выше, Yarn classic) c помощью команды npm create vue@latest (из официальной документации vuejs);

2. Выбрали TypeScript, Vue Router, Pinia, Vitest, End to End testing, ESLint.

3. Перешли в папку с проектом.

4. Установили зависимости c помощью команды npm i и посчитали их.

5. Удалили package-lock.json и папку node_modules, выполнили команду npm i --production.

6. Удалили package-lock.json и папку node_modules, отправили команду yarn.

7. Удалили yarn.lock и папку node_modules, выполнили команду yarn --prod.

В качестве дополнительного подтверждения откроем документацию npm и найдём цитату: «As of npm v7, peerDependencies are installed by default». То есть, все зависимости, указанные в peerDependencies, устанавливаются npm по умолчанию. В Yarn такого не происходит. 

Данный момент можно обойти с помощью блока peerDependenciesMeta, указав, что зависимость НЕ является обязательной. Из минусов такого подхода — нет информации про уведомления. При использовании Yarn peer-зависимости не устанавливаются автоматически, но выдают в консоли предупреждения. 

Думаю, теперь вы представляете наш стек и понимаете, почему он такой. Перейдём к разбору дальнейших шагов проделанной оптимизации.

Убираем проблемы с совместимостью

После того как мы разобрались со скоростью сборки и размерами бандлов, у нас остались ещё несколько проблем:

  • «зоопарк» версий по core-зависимостям в библиотеках и проектах;

  • периодическая потеря обратной совместимости библиотек между собой.

 Для решения этих проблем мы создали две библиотеки: dev-config и dev-core.

Убираем зоопарк версий core-зависимостей

Для того, чтобы решить проблему с зоопарком не только в библиотеках, но и в конфигах и синхронизации версии библиотек, мы создали библиотеку dev-config. 

Раньше было довольно сложно между проектами и библиотеками переносить не только конфигурационные файлы, но иногда и заимствовать решения, так как была примерно следующая ситуация по проектам:

То есть в разных проектах использовались разные версии core-библиотек. Например, довольно большая разница между ESLint 8 и ESLint 9 в конфиг-файлах, а во Vue 3.3 появились новые фичи, которых ещё нет во Vue 3.0.

Что мы поместили в dev-config?

  1. Все библиотеки из проектов и библиотек, которые встречаются более одного раза. Тем самым мы синхронизировали везде версии библиотек. То есть если везде установлена единая версия dev-config, то мы знаем, что версии одинаковые.

  2. Единые конфигурационные файлы для вспомогательных инструментов — Vite, ESLint, TypeScript и т.д. Одно из самых интересных решений: мы теперь знаем, что вспомогательные инструменты у нас единой версии, и создали единые конфиги для всего! Как это работает? При создании проекта мы по умолчанию подключаем конфиги из dev-config и в случае необходимости перезаписываем определённые правила, например, для ESLint. Дополнительно с помощью ESLint мы покрыли огромное количество договоренностей по code-style, что позволяет отслеживать их на этапе проверки в CI/CD и в полуавтоматическом режиме править код в соответствии с нашими правилами, принятыми внутри команды для общего удобства.

  3. Вынесли туда CI/CD для деплоя проектов и библиотек. Ещё один интересный момент. Мы сделали две конфигурации CI/CD: одна для библиотек, вторая для проектов. При этом мы вынесли большое количество опций в переменные, с помощью которых можно отключить часть линтеров по необходимости, а также определять дополнительные параметры, например путь в Nexus, куда нужно положить библиотеку или проект.

  4. Создали шаблоны для проектов и библиотек. Теперь, когда мы создаём новую библиотеку или проект, мы создаём новый репозиторий из одного из этих шаблонов. Это довольно удобно, так как не надо тратить время на первоначальную настройку.

Что в итоге получилось с точки зрения зависимостей? В библиотеки мы стали тянуть dev-config как devDependencies, а в проекты — уже как основную зависимость.

При этом у нас в библиотеках и проектах сократился package.json, упростилась работа по отслеживанию зависимостей – большая часть пакетов ушла в dev-config. Если раньше в проекте в package.json тянулось около 40 – 60 зависимостей, то сейчас стало значительно меньше. Пример на библиотеке:

Дополнительно мы подключили кэширование результатов выполнения части работы в рамках CI/CD и ускорили процессы.

Реализуем поддержку обратной совместимости наших библиотек

При активном развитии и рефакторинге библиотек стали возникать вопросы о совместимости библиотек между собой. То есть условный Datepicker мог не поддерживать изменения из других библиотек. Регулярно были вопросы на тему совместимости:

В dev-core мы поместили совместимые версии библиотек и перенесли управление версиями библиотек с проектов на dev-core. Добавили сборку композитного Storybook, чтобы дизайнеры, мы и аналитики могли смотреть на актуальные компоненты.

Как теперь выглядит схема подключения в проекты:

После реализации dev-core дополнительно изменили package.json

Так выглядит общий результат, который упростил жизнь разработчикам:

Дополнительно фиксируем процессы!

Проблема с рабочим процессом (workflow) была связана с тем, что все хотели изменений для своих проектов «здесь и сейчас», и иногда они противоречили друг другу. Спустя несколько итераций мы пришли к такому алгоритму: 

0. Требуется доработки? Начинаем смотреть: соответствует UI-KIT, или нет?

1. Если нет, создаём запрос к дизайнерам.

2. Заводим задачу на реализацию (или баг, если есть ошибка).

3. Делаем задачу и создаём merge request.

4. Проводим кросс-командное ревью.

5. Выпускаем новые версии библиотек.

6. Создаём MR в dev-core.

7. Проверяем, все ли работает и совместимо.

8. Если нет, возвращаемся к пункту 2.

9. Если все работает, выпускаем dev-core.

10. Используем dev-core на проектах.

Что мы ещё добавили для оптимизации (бонусные лайфхаки)

  1. Добавили использование кэша GitLab Runners — он позволяет сократить время на переустановку зависимостей в CI. Зависимости переустанавливаются только в том случае, если были изменения в package.json (добавили, убрали зависимость или изменили версию) — в случае, если мы исправили опечатку или сделали hot-fix — этот шаг пролетает буквально за несколько секунд – инициализация джобы, проверка зависимостей (из кэша) – все OK, обновлять не надо — завершаем.

  2. Добавили проверку на соответствие peerDependencies в CI. Если у нас есть несоответствия в зависимостях проекта несоответствие peerDependencies из dev-core, то CI падает и выдаёт список невалидных версий библиотек. Но… мы оставили возможность перезаписывать версии библиотек на усмотрения проекта через overrides и resolutions.

  3. Для того, чтобы не плодить instance’ы одной и той же библиотеки (что иногда может вызвать проблемы, например — AGGrid) мы добавили в процесс сборки наших библиотек вырезание бандлов cпомощью core-зависимостей. Теперь при установке библиотеки у нас получается, что устанавливается OpenLayers, но он не входит в бандл карт, а ссылается на зависимость OpenLayersиз node_modules.

Общие итоги

Мы сократили время сборки всех библиотек на 70.1% — с 45 минут до 13 (без использования кэша). С кэшем — ещё быстрее.

Мы сократили общий вес бандлов на 95,8% — с 68,3 Мб до 2,9 Мб, что повлияло и на время сборки библиотек.

Мы стабилизировали процессы и взаимодействие между разработчиками и дизайнерами, проводя все изменения через UI-KIT. И это все при активном развитии библиотек.

Выводы

Проделанная работа вышла за рамки простого «наведения порядка». Мы не просто ускорили сборку и уменьшили бандлы (хотя это само по себе отличный результат), но и фундаментально изменили подход к управлению зависимостями и версиями. 

Создание библиотек dev-config и dev-core позволило нам перейти от разрозненных версий инструментов и конфликтующих компонентов к единой и предсказуемой системе. Теперь мы можем быть уверены, что, если мы используем dev-core на одном продукте, то он будет так же работать и во всех остальных, а настройка нового проекта или библиотеки занимает минуты, а не часы. 

Ещё одно важное, на мой взгляд, достижение — это изменение подхода к работе над общими библиотеками для наших продуктов. Фиксация workflow и жёсткая привязка к UI-KIT сделали процесс разработки прозрачным. Они на 99% устранили ситуации, когда «у меня всё работало, а после твоего обновления сломалось». 

Работа над оптимизацией и организацией библиотек показала: к ним стоит относиться как к внутренним продуктам. Ведь конечные пользователи здесь — мы сами, фронтенд-разработчики.