Как развернуть монорепозиторий React и ExpressJS на Heroku

Привет всем, это первый раз, когда я пишу здесь, и сегодня я хотел бы поделиться своим опытом развертывания одностраничного приложения на Heroku, однако это не простой случай, потому что мое приложение построено поверх React, ExpressJS и Yarn Workspaces, чтобы разместить все в одном репозитории GitHub. Я столкнулся с этим случаем, работая над проектом задания для Болонского университета, формат монорепозитория был обязательным, поэтому мне нужно исправить эту проблему, чтобы продолжить работу над проектом.

Вы, наверное, уже знаете, что Heroku удобен для разработчиков, прост в использовании и имеет уровень бесплатности, тогда у нас есть React, наиболее используемая в настоящее время внешняя библиотека Javascript, и ExpressJS, самая известная внутренняя среда Javascript.

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

Если вы находитесь в той же ситуации, что и я, то вы уже знаете, что такое React и Express, поэтому я попытаюсь перейти к делу.

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

Что такое одностраничное приложение?

Одностраничное приложение состоит из набора HTML, CSS, Javascript и ресурсов, которые будут отправлены клиенту (браузеру пользователя) при загрузке страницы, и с этого момента клиент будет следовать своему собственному рабочему процессу и навигации, в основном построенным на Javascript. , при отправке и получении данных из серверной части с помощью фоновых HTTP-запросов. В React мы можем получить этот пакет, выполнив команду build script.

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

Монорепозиторий состоит из одного репозитория (папки проекта), который содержит как интерфейсный, так и внутренний исходный код. Наличие монорепозитория означает, что клиент и сервер, в данном случае React и Express, остаются в одном репозитории. Конечно, у монорепозитория есть свои плюсы и минусы, но в моем случае это было требованием, поэтому у меня не было выбора.

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

- совместно использовать общие пакеты узлов между клиентом и сервером, но в то же время иметь разные package.json для каждого из них

- следовать «стандартной» структуре каталогов

- иметь независимые конфигурации для клиента и сервера (например, tsconfig, babel и т. д.)

Вот как выглядит мой репозиторий с использованием Yarn Workspaces:

my-project
├── node_modules
├── client
│   ├── build
│   ├── package.json
│   └── ...
├── server
│   ├── package.json
│   └── ...
├── package.json
└── ...

Первая попытка развернуть

Будучи наивным, я, хотя

Эй, давайте добавим немного кода в Procfile и все.

поэтому я написал что-то вроде:

web: yarn — cwd “client” build && yarn — cwd “server” start

и вот как я получил свое первое неудачное развертывание.

Проблема

Учитывая, что мы создаем одностраничное приложение, нам нужно генерировать пакет React каждый раз, когда код изменяется, и мы хотим его развернуть. Однако Heroku просто развернет наш репозиторий GitHub, который не содержит самого пакета, поэтому у нас есть два варианта:

- 1. Добавьте каталог пакета в репозиторий и фиксируйте каждый раз, когда пакет изменяется. (Вы действительно не хотите этого делать)

- 2. Поручить Heroku генерировать пакет каждый раз, когда мы развертываем приложение. (правильно выбрать [x])

Используя Heroku, мы не сможем разместить веб-сервер, такой как Nginx или Apache, для распространения пакета среди клиентов, для отправки пакета клиентам должен быть Express. Более того, мы должны соблюдать этапы развертывания Heroku.

Этапы развертывания Heroku

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

1. Загружаем код: здесь ничего интересного

2. Соберите проект: он генерирует пакет React и, при необходимости, другие типы сборки и компиляции (например, если вы используете Typescript, в это время весь ваш код будет транспилирован в Javascript).

На этом этапе Heroku будет использовать компилятор слагов для создания слагов:

Slugs — это сжатые и предварительно упакованные копии вашего приложения, оптимизированные для распространения в диспетчере dyno.

Для этого он запускает скрипт сборки, размещенный в корневом package.json.

Максимально допустимый размер слага (после сжатия) составляет 500 МБ.

как указано в официальной документации, но нам не нужно об этом беспокоиться, потому что, например, средний проект React занимает около ~50 МБ после сжатия.

3. Релиз (т.е. выполнение) приложения: здесь мы ничего не можем собрать, потому что не можем занять более 500 МБ памяти. На данный момент у нас уже должен быть пакет React и код Express на Javascript.

На этом этапе Heroku выполняет Procfile, если он доступен в репозитории, в противном случае он запускает start script файла package.json.

## Решение

Ошибка, которую я совершил, заключалась в том, что я пытался собрать пакет React на этапе выпуска, и в несжатом виде он занял более 500 МБ памяти.

Чтобы не добавлять еще одну сложность, я буду использовать только Javascript в своем проекте, однако с некоторыми изменениями это решение будет работать и для проектов Typescript.

1. Прежде всего нам нужно проверить правильность настройки каждого package.json.

Вот каким должен быть корневой package.json:

“private”: true,
“workspaces”: [
“server”,
“client”
]

в то время как server/package.json должен иметь

“name”: “server”,
“scripts”: {
“start”: “node index.js”
}

и client/package.json должен иметь

“name”: “client”,
“scripts”: {
“start”: “react-scripts start”,
“build”: “react-scripts build”
}

как вы, наверное, поняли, имена клиента и сервера должны быть одинаковыми как в package.json, так и в подкаталогах, т. е. если вы решите называть своего клиента как react-client, вам нужно изменить имя каталога, свой собственный package.json и корневой package.json в массиве рабочих пространств.

2. В корневой package.json прописываем buildscript, который будет генерировать frontend бандл при деплое:

“scripts”: {
“build”: “yarn workspace client build”
}

3. Этот шаг на самом деле не требуется, чтобы все заработало, но лучше иметь хорошо организованный проект. Итак, мне нравится перемещать каталог build, сгенерированный React, который находится в client/build, в корень проекта, потому что мне кажется нехорошим, что сервер переходит в каталог клиента.

Для этого мы можем создать новый файл Javascript с именем postbuild.js в каталоге клиента, куда мы поместим следующие строки кода:

const fs = require(‘fs’);
fs.rmdirSync(‘../build’, { recursive: true });
fs.renameSync(‘build’, ‘../build’);

и мы определяем сценарий пост-сборки в client/package.json:

“postbuild”: “node postbuild.js”

Этот скрипт будет автоматически вызываться Heroku сразу после создания пакета клиента, но до выполнения кода.

4. Как я уже сказал, для отправки пакета клиентам должен быть Express, поэтому мы должны объявить, где Express может найти этот пакет. Если вы выполнили предыдущий шаг, он будет находиться в корневом каталоге проекта, следовательно, его относительный путь с точки зрения Express будет ../build.

Итак, наш server/index.js должен содержать:

const express = require(‘express’);
const PORT = process.env.PORT || 5000;
const app = express();
app.use(express.static(‘../build’))
app.get(‘/hello’, (req, res) => {
res.send(‘Hello World!’)
})
app.listen(PORT, () => console.log(‘Server started!’));

5. И последнее, но не менее важное: нам нужно определить, как Heroku может запускать наше приложение, в частности, как запускать наш сервер Express. Есть два способа сделать это:

Просто добавьте start script в корень package.json со следующим:

“start”: “yarn workspace server start”

этот скрипт будет автоматически выполняться Heroku на этапе выпуска.

Теперь создайте Procfile в корневом каталоге проекта, этот файл позволит нам указать более сложные действия, которые будут выполняться во время выпуска приложения, например. было бы очень полезно, если бы наш проект использовал Typescript.

В этом случае мы могли бы написать:

web: yarn — cwd “server” node index.js

этот код просто запускает node index.jsx в каталоге server.

6. В итоге структура проекта будет примерно такой:

my-project
├── node_modules
├── Procfile
├── build
│ └── …
├── client
│ ├── package.json
│ ├── postbuild.js
│ └── …
├── server
│ ├── package.json
│ └── …
├── package.json
└── …

Пример проекта

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



Я также открыл исходный код оригинального проекта, который привел меня к обнаружению этой проблемы, он использует Typescript и может быть полезен, если вам нужен более сложный пример: