[RU] Разработка в монорепозитории

A.Bortnikov

javascriptrepositorymonorepoyarnworkspaceslernalibraryrussian

frontend, web development

Photo by JOSHUA COLEMAN


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

Что такое монорепозиторий

Монорепозиторий (так же монорепо) - способ организации кода, при котором несколько проектов (или пакетов) хранятся в одном репозитории.

Из основных преимуществ монорепозитория перед изолированными репозиториями является:

  • Упрощенное управление зависимостями - видно, как и где используются пакеты.
  • Атомарность - изменения в пакете-зависимости применятся ко всем сервисам.
  • Распространение инженерной культуры.

Но, как и у любого решения, у монорепозитория есть недостатки:

  • Требовательность к дисковому пространству.
  • Размытие ответственности за компоненты.
  • Устаревание изменений из-за частых вливаний в мастер другими командами.

Примеры монорепозиториев: React, Jest, Babel, Next.js, Apollo server и другие.

Оптимизация изменений

Одно из неудобств проявляется в процессе внесения изменений: когда в репозитории находятся веб-приложение и зависимость, необходимо дорабатывать пакет-зависимость и отслеживать, как это влияет на веб-приложение.

Первое, что можно сделать, придерживаться такого алгоритма:

  1. Внести изменения в зависимость.
  2. Выполнить сборку зависимости.
  3. Перезапустить приложение, которое использует зависимость.
  4. Проверить, что приложение работает корректно, если нет, то вернуться к пункту 1.

Пункты 2-3 требуют значительных временных затрат и выполняются при любых изменениях.

Оптимизируем алгоритм:

  1. Запустить приложение и зависимости в режиме разработки.
  2. Внести изменения в зависимость.
  3. Сборщик зависимости обновит результирующие файлы зависимости.
  4. Сборщик приложения обновит результирующие файлы приложения.
  5. Проверить, что приложение работает корректно, если нет, то вернуться к пункту 2.

В таком случае ресурсоемким по времени является только пункт 1, пункты 3-4 при этом обновляют только файлы, которые соответствуют изменениями из пункта 2.

Ближе к коду

На одном из наших проектов в Xsolla мы используем следующую структуру репозитория:

1.
2└── packages
3 ├── app
4 ├── uikit
5 └── sdk

Где app - веб-приложение, которое использует пакеты uikit - набор интерфейсных компонент и sdk - вся логика приложения. Такое разделение на пакеты позволяет делать наш код чище - нет спагетти-кода, взаимодействие только через внешний API. Кроме того, использование публичного пакета sdk, который нужен при интеграции нашего продукта разработчиками, внутри app дает лучшее понимание, что нужно разработчикам.

Но для примера ограничимся только пакетами app и uikit:

1.
2├── README.md
3├── node_modules
4├── package.json
5├── packages
6│ ├── app
7│ │ ├── node_modules
8│ │ ├── package.json
9│ │ ├── public
10│ │ ├── src
11│ │ └── yarn.lock
12│ └── uikit
13│ ├── node_modules
14│ ├── package.json
15│ ├── rollup.config.js
16│ ├── src
17│ └── yarn.lock
18└── yarn.lock

./packages/uikit/package.json

1{
2 "name": "@monorepo-development/uikit",
3 "version": "1.0.0",
4 "dependencies": {
5 "@emotion/core": "^10.0.27",
6 "@emotion/styled": "^10.0.27"
7 },
8 "scripts": {
9 "build": "rollup -c",
10 "start": "yarn build -w"
11 }
12}

./packages/app/package.json

1{
2 "name": "@monorepo-development/app",
3 "version": "1.0.0",
4 "dependencies": {
5 "@monorepo-development/uikit": "^1.0.0",
6 "react": "^16.12.0",
7 "react-dom": "^16.12.0",
8 "react-scripts": "3.4.0"
9 },
10 "scripts": {
11 "start": "react-scripts start",
12 "build": "react-scripts build"
13 }
14}

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

  1. Связывание зависимостей - все внутренние зависимости должны использовать самый актуальный код. Рассмотрим, что это означает на нашем примере. Приложение app связано с внутренней зависимостью uikit — это означает, что изменения результирующих файлов uikit отражаются в папке зависимостей node_modules у app. Для реализации такого механизма можно использовать lerna или yarn workspaces (что и было использовано в нашем примере):

1{
2 "name": "monorepo-development",
3 "private": true,
4 "workspaces": ["packages/*"]
5}

  2. В приложении app должен работать механизм горячей перезагрузки компонентов (Hot Module Replacement - HMR).

Чтоб проверить, работает этот механизм или нет, запустите приложение в режиме разработки (в нашем примере это yarn start) и сделайте правки в коде одного из компонентов - изменения должны автоматически примениться в браузере.

В нашем примере используется React Create App, так что этот механизм работает из коробки.

HMA example

  3. Сборка пакета uikit должна работать таким образом, чтобы итоговые файлы собирались в одну и ту же папку (например, lib), при этом сама папка не должна удаляться между сборками.

Если папка будет удаляться, то при повторной сборке приложения app в режиме HMR будет возникать ошибка:

1Module not found: Can't resolve '@monorepo-development/uikit' in '/monorepo-development/packages/app/src'

В нашем примере сборка пакета осуществляется rollup, который не удаляет папку lib с результирующими файлами.


  4. Для всех проектов в репозитории должен быть настроен режим наблюдения (watch mode) - изменения исходных файлов и зависимостей автоматически приводят к пересборке проекта. Create React App и rollup поддерживают этот режим без сложной настройки.

Итоговый код репозитория можно посмотреть по ссылке. Попробуйте запустить проект, сделать изменения в файле /packages/uikit/src/github-profile/index.jsx и посмотрите как это работает в живую.

Заключение

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

Contact me: