TypeScript все-в-одном: Монорепозиторий со своими трудностями и преимуществами
В этой статье я не буду сравнивать монорепозиторий и полирепозиторий, поскольку это всего лишь философия. Вместо этого я сосредоточусь на процессе создания и развитии, предполагая, что вы знакомы с экосистемой JS/TS.
Введение
Я всегда мечтал о монорепозитории.
Я увидел подход к монорепозиторию, работая на Airbnb, но он был только для фронтенда. Имея глубокую любовь к экосистеме JavaScript и "счастливому" опыту разработки на TypeScript, я начал выравнивать фронтенд и бэкенд код на одном языке примерно три года назад. Это было здорово (для найма), но не так здорово для разработки, так как наши проекты все еще были разбросаны по нескольким репозиториям.
Как говорится, "лучший способ рефакторинга проекта - начать новый". Так что, когда я начинал свой стартап около года назад, я решил использовать полностью монорепозиторию: положить проекты фронтенда и бэкенда, даже схемы баз данных, в один репозиторий.
В этой статье я не буду сравнивать монорепозиторий и полирепозиторий, поскольку это всего лишь философия. Вместо этого я сосредоточусь на процессе создания и развитии, предполагая, что вы знакомы с экосистемой JS/TS.
Окончательный результат доступен на GitHub.
Почему TypeScript?
Честно говоря, я люблю JavaScript и TypeScript. Мне нравится сочетание его гибкости и строгости: вы можете откатиться к unknown
или any
(хотя мы запретили любую форму any
в на шем коде), или использовать суперстрогий набор правил линтера, чтобы согласовать стиль кода в команде.
Когда мы говорили о концепции "полным стеком" раньше, мы обычно представляли себе не менее двух экосистем и языков программирования: один для фронтенда и один для бэкенда.
Однажды я вдруг понял, что это может быть проще: Node.js достаточно быстр (поверьте мне, в большинстве случаев качество кода важнее скорости выполнения), TypeScript достаточно зрел (хорошо работает в больших проектах фронтенда), и концепция монорепозитория была опробована целой кучей известных команд (React, Babel и т.д.) - так почему бы нам не объединить весь код вместе, от фронтенда до бэкенда? Это позволит инженерам выполнять задания без переключения контекста в одном репозитории и реализовывать полноразмерный функционал на (почти) одном языке.
Выбор менеджера пакетов
Как разработчик, как обычно, я не мог дождаться, чтобы начать кодирование. Но на этот раз все было иначе.
Выбор менеджера пакетов критически важен для опыта разработки в монорепозитории.
Боль инерции
Это было в июле 2021 года. Я начал с [email protected]
, так как использовал его долгое время. Yarn был быстр, но вскоре я столкнулся с несколькими проблемами с Yarn Workspaces. Например, некорректное увеличение зависимостей, и множество проблем помечено как "исправлено в современном", что перенаправляет меня на v2 (berry).
"Хорошо, я обновляюсь сейчас." Я перестал бороться с v1 и начал переход. Но длинное руководство по миграции от berry напугало меня, и я сдался после нескольких неудачных попыток.
Это просто работает
Так началось исследование менеджеров пакетов. Я был восхищен pnpm
после пробного использования: быстрый, как yarn, встроенная поддержка монорепозитория, похожие команды на npm
, жесткие ссылки и т.д. Что самое главное, это просто работает. Как разработчик, который хочет начать с продукта, а не разрабатывать менеджер пакетов, я просто хотел добавить некоторые зависимости и начать проект, не зная, как работает менеджер пакетов или любые другие необычные концепции.
Исходя из той же идеи, мы выбрали старого друга lerna
для выполнения команд по пакетам и публикации пакетов в рабочем пространстве.
Определение области пакетов
Сложно четко определить окончательную область каждого пакета в начале. Просто начните со своей лучшей попытки в соответствии с существующим положением дел, и помните, что вы всегда можете рефакторить в процессе разработки.
Наша начальная структура содержала четыре пакета:
core
: монолитный сервис бэкенда.phrases
: i18n ключ → фразовые ресурсы.schemas
: база данных и общие схемы TypeScript.ui
: веб SPA, взаимодействующий сcore
.
Технологический стек для полного стека
Так как мы принимаем экосистему JavaScript и используем TypeScript в качестве нашего основного языка программирования, многие выборы очевидны (на основе моих предпочтений 😊):
koajs
для бэкенд сервиса (core): У меня был трудный опыт использованияasync/await
вexpress
, поэтому я решил использовать что-то с встроенной поддержкой.i18next/react-i18next
для i18n (phrases/ui): мне нравится его простота API и хорошая поддержка TypeScript.react
для SPA (ui): Просто личное предпочтение.
А как насчет схем?
Здесь все еще что-то не хватает: система базы данных и схема <-> Сопоставление определений TypeScript.
Общее против упрощенного
На тот момент я попробовал два популярных подхода:
- Использование ORM с множеством декораторов.
- Использование построителя запросов, например, Knex.js.
Но оба из них производили странное ощущение во время предыдущей разработки:
- Для ORM: Я не фанат декораторов, и еще один абстрактный слой базы данных вызывает больше усилий по обучению и неопределенности для команды.
- Для построителя запросов: Это как написание SQL с некоторыми ограничениями (в хорошем смысле), но это не настоящий SQL. Таким образом, нам нужно использовать
.raw()
для необработанных запросов во многих сценариях.
Тогда я увидел эту статью: "Перестаньте использовать Knex.js: использование построителя SQL-запросов является антипаттерном". Название выглядит агрессивно, но содержание великолепно. Она сильно напомнила мне, что "SQL - это язык программирования", и я понял, что могу писать SQL напрямую (как CSS, как я мог пропустить это!) чтобы использовать родной язык и возможности базы данных, а не добавлять еще один слой и уменьшать мощность.
В заключение, я решил придерживаться Postgres и Slonik (открытый клиент Postgres), как говорится в статье:
...преимущество разрешения пользователю выбирать между различными диалектами баз данных незначительно, а накладные расходы на разработку для нескольких баз данных одновременно значительны.
SQL <-> TypeScript
Еще одно преимущество написания SQL в том, что мы можем легко использовать его как единственный источник истины определений TypeScript. Я написал генератор кода для транспиляции SQL-схем в код TypeScript, который мы будем использовать в нашем бэкенде, и результат выглядит неплохо:
Мы даже можем соединить jsonb
с типом TypeScript и обрабатывать валидацию типа в сервисе бэкенда при необходимости.
Результат
Окончательная структура зависимостей выглядит так:
Вы, возможно, заметили, что это диаграмма в одном направлении, которая в значительной степени помогла нам сохранить четкую архитектуру и способность расширяться по мере роста проекта. Плюс, код в основном на TypeScript.
Опыт разработки
Совместное использование пакетов и конфигурации
Внутренние зависимости
pnpm
и lerna
прекрасно справляются с внутренними зависимостями рабочего пространства. Мы используем команду ниже в корне проекта для добавления пакетов-братьев:
Она добавит @logto/schemas
как зависимость к @logto/core
. При сохранении семантической версии в package.json
вашей внутренней зависимости, pnpm
также может правильно связать их в pnpm-lock.yaml
. Итог выглядит так:
Совместное использование конфигурации
Мы рассматриваем каждый пакет в монорепозитории как "независимый". Таким образом, мы можем использовать стандартный подход для обмена конфигурации, который покрывает tsconfig
, eslintConfig
, prettier
, stlyelint
и jest-config
. См. этот проект для примера.
Код, линт и коммит
Я использую VSCode для ежедневной разработки, и коротко, ничего не меняется, когда проект правильно настроен:
- ESLint и Stylelint работают нормально.
- Если вы используете плагин ESLint для VSCode, добавьте нижеуказанные настройки VSCode, чтобы он соблюдал конфигурацию ESLint для каждого пакета (замените значение
pattern
на свое):
- Если вы используете плагин ESLint для VSCode, добавьте нижеуказанные настройки VSCode, чтобы он соблюдал конфигурацию ESLint для каждого пакета (замените значение
- husky, commitlint, и lint-staged работают, как ожидалось.
Компилятор и прокси
Мы используем разные компиляторы для фронтенда и бэкенда: parceljs
для UI (React) и tsc
для всех других чистых пакетов TypeScript. Я настоятельно рекомендую вам попробовать parceljs
, если вы еще этого не делали. Это настоящий компилятор "без конфигурации", который с грацией обрабатывает различные типы файлов.
Parcel имеет собственный сервер разработки фронтенда, а вывод продакшна - это просто статические файлы. Так как мы хотели монтировать API и SPA под одним и тем же началом, чтобы избежать проблем с CORS, следующая стратегия сработает:
- В среде разработки использовать простой HTTP-прокси для перенаправления трафика на сервер разработки Parcel.
- В продакшне, обслуживать статические файлы напрямую.
Вы можете найти реализацию функции промежуточного обработчика фронтенда здесь.
Режим наблюдения
У нас есть скрипт dev
в package.json
для каждого пакета, который следит за изменениями файлов и перекомпилирует их при необходимости. Благодаря lerna
, вещи становятся легкими, используя lerna exec
для параллельного выполнения скриптов пакетов. Скрипт корня выглядит так:
Итог
В идеале, всего два шага для нового инженера/участника для начала работы:
- Клонировать репозиторий
pnpm i && pnpm dev
Заключительные заметки
Наша команда разрабатывает по этому подходу в течение года, и мы очень довольны. Посетите наш репозиторий на GitHub, чтобы увидеть последнюю версию проекта. Чтобы подвести итог:
Проблемы
- Необходимо знать экосистему JS/TS
- Необходимо выбрать правильный менеджер пакетов
- Требуется некоторая дополнительная установка один раз
Преимущества
- Разрабатывать и поддерживать весь проект в одном репозитории
- Упрощение требований к навыкам программирования
- Общие стили кода, схемы, фразы и утилиты
- Улучшение эффективности общения
- Больше нет вопросов вроде: Каково определение API?
- Все инженеры говорят на одном языке программирования
- Упрощение CI/CD
- Использование одной и той же инструментарии для построения, тестирования и публикации
В этой статье остались некоторые темы неосвещенными: настройка репозитория с нуля, добавление нового пакета, использование GitHub Actions для CI/CD, и т.д. Это станет слишком длинным для этой статьи, если я распространю каждую из них. Не стесняйтесь комментировать и сообщайте мне, какую тему вы хотели бы видеть в будущем.