Совместное использование кода - это легко, но сделать это правильно - непросто. Есть несколько способов сделать это, и ваш вариант использования диктует, какой подход вам подходит. Низко висящий плод - это просто скопируйте и вставьте то, что вам нужно, из StackOverflow, GitHub или вашего предыдущего проекта. Несмотря на очевидное преимущество естественности, копирование и вставка тратят ваше время в долгосрочной перспективе из-за необходимости ручной синхронизации изменений.

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

Почему монорепо

При оптимизации для долгосрочного обслуживания у нас есть несколько вариантов. Я люблю делать ставки на монорепо. Монолитное хранилище - простая идея. Вы организуете код всех своих сервисов в едином репозитории. У него есть несколько преимуществ перед использованием отдельного репозитория для каждой службы.

Повторное использование кода - это просто. После того, как вы абстрагируете согласованную единицу кода в модуль, вы можете импортировать ее откуда угодно.

Непрерывная интеграция запускает тесты для всего монорепозитория, поэтому после слияния PR вы увеличиваете версию всех вспомогательных сервисов, и нет никаких сомнений в том, какие версии совместимы друг с другом. Версия 1.2 службы A всегда совместима с версией 1.2 службы B. Вот почему сложные проекты с несколькими зависимостями часто также используют монорепозиторий (Babel, React, Angular, Jest). По той же причине возможен и крупномасштабный рефакторинг.

Вы поддерживаете одно стороннее дерево зависимостей. Слишком легко, особенно со всеми новинками NPM, получить две разные версии одной и той же библиотеки, и необходимость вручную синхронизировать их в разных репозиториях вызывает у меня головную боль. Наличие одного основного файла package-lock.json позволяет значительно сэкономить время.

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

Пошаговая установка

Установите Lerna и инициализируйте проект.

npm i -D lerna
npx lerna init

Я собираюсь использовать приложение Create React для создания двух пакетов: alice и bob.

cd packages
npx create-react-app alice
npx create-react-app bob

Давайте создадим еще один пакет под названием common, в котором мы можем разместить модули, общие для alice и bob. Вызов lerna create из корня репозитория.

npx lerna create @yourproject/common -y

Использование имен с ограниченной областью видимости для пакетов вашего проекта - четкий способ отличить их от общедоступных пакетов NPM.

Мы можем убедиться, что у нас есть одна и та же версия React и ReactDOM в каждом пакете, вызвав lerna add из корня репозитория.

npx lerna add react@^16.6.3
npx lerna add react-dom@^16.6.3

Давайте создадим простой компонент, который мы сможем повторно использовать в общем пакете.

// packages/common/Heading.jsx
import React from "react";

function Heading({ level = "1", title }) {
  return React.createElement(`h${level}`, {}, title);
}

export default Heading;

Теперь вызовите lerna add из корня репозитория, чтобы связать пакеты common с alice и bob.

npx lerna add @yourproject/common

В настоящее время каждый пакет поддерживает свой node_modules со всеми необходимыми зависимостями. Возможно поднятие зависимостей в корень репозитория. Давайте очистим установленные в настоящее время зависимости и попробуем.

npx lerna clean -y && npx lerna bootstrap --hoist

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

// packages/alice/src/App.js
import React, { Component } from "react";
import Hello from "@yourproject/common/Hello";


class App extends Component {
  render() {
    return (
      <Hello title="Hello, World!" />
    );
  }
}

export default App;

Теперь запустим сервис alice.

npx lerna run start --scope=alice

Вы можете видеть, как изменения в пакете common немедленно отражаются в приложении alice, что обеспечивает отличное взаимодействие с разработчиками.

Развертывание

С монорепозиторием все усложняется с развертыванием. Возможно, вам потребуется предоставить дополнительные инструкции для вашего инструмента развертывания или создать собственный сценарий, чтобы иметь возможность развертывать одну службу в Heroku, если вы хотите отделить ее от остальной части проекта. Я решил эту проблему, используя отдельные файлы Docker, поэтому все сводится к указанию другого пути при запуске сборки докеров.

Это пример Dockerfile, который максимизирует кеширование Docker (не пугайтесь длинного раздела зависимостей). После сборки файлы копируются в легкий образ nginx, так как Node.js нам больше не нужен.

FROM node:10.13-alpine as builder

# Environment

WORKDIR /home/app
ENV NODE_ENV=production

# Dependencies

COPY package.json /home/app/
COPY package-lock.json /home/app/
COPY lerna.json /home/app/

COPY packages/alice/package.json /home/app/packages/alice/
COPY packages/alice/package-lock.json /home/app/packages/alice/

COPY packages/common/package.json /home/app/packages/common/
COPY packages/common/package-lock.json /home/app/packages/common/

RUN npm ci --ignore-scripts --production --no-optional
RUN npx lerna bootstrap --hoist --ignore-scripts -- --production --no-optional

# Build

COPY . /home/app/
RUN cd packages/alice && npm run build

# Serve

FROM nginx:1.15-alpine
COPY --from=builder /home/app/packages/alice/build /usr/share/nginx/html

Теперь вы можете создать образ и запустить контейнер. Выполните следующие команды из корня репозитория.

docker build -t yourproject/alice -f ./packages/alice/Dockerfile .
docker run --rm -p 8080:80 --name yourproject-alice yourproject/alice

Код доступен на GitHub: MichalZalecki / lerna-monorepo-example.

Первоначально опубликовано на michalzalecki.com 19 ноября 2018 г.