Локальный хост — это не продакшн: диагностика проблем с приложениями React при развертывании
Узнайте, почему приложение на React, которое корректно работает на вашем компьютере, терпит неудачу после развертывания, и как отследить API-адреса, переменные окружения, механизм CORS, маршрутизацию, ресурсы и процедуры аутентификации до уровня, вызывающего сбой.
Источники: Why Your React App Works Locally but Breaks After Deployment: 10 Common Problems and How to Fix Them
Приложение на React, которое работает без проблем на localhost, но сразу же перестаёт функционировать после развертывания, является одной из самых распространённых проблем в работе с фронтендом. Запросы перестают возвращать данные, обновление страницы приводит к ошибке 404, изображения исчезают, процесс входа молча терпит неудачу, или платформа хостинга отказывается компилировать код, который успешно собирался на вашем ноутбуке. Почти ни одна из этих проблем не связана с самим React. Они возникают из-за различий между средой, в которой вы разрабатываете, и той, с которой сталкиваются ваши пользователи. В этом руководстве рассматриваются десять наиболее частых причин, показано, как проявляется каждая из них, а в конце приводятся методы отладки и чек-лист перед развертыванием, которые можно использовать при каждой публикации.
Почему один и тот же код ведёт себя по-разному в продакшене
Фронтенд на React редко является всей приложением. Он взаимодействует с API, а API — с базой данных, причем каждый звенье в этой цепочке зависит от настроек: переменных окружения, имен доменов, сетевых правил, параметров аутентификации и того, как платформа хостинга обрабатывает входящие запросы. На вашем компьютере вы косвенно контролируете всё это: вы знаете, на каком порту работает бэкенд, какой файл .env загружается и какие пакеты находятся в каталоге node_modules. В производственной среде каждое из этих предположений заменяется на что-то конкретное, и любое несоответствие может сделать корректный код неработоспособным.
Имейте это в виду, читая следующие разделы. Большинство решений связаны с изменением настроек, а не с изменением кода.
1. URL API по-прежнему указывает на localhost
Когда развернутое приложение не отображает данных, первым делом стоит проверить, куда направляются его запросы. Во время разработки естественно запускать бэкенд локально и вставлять его адрес в код фронтенда:
const API_URL = "http://localhost:8080/api";
Это работает потому, что браузер и бэкенд запускаются на одном компьютере. Как только фронтенд развернут, например, по адресу https://my-app.example.com, та же строка продолжает отправлять запросы по адресу http://localhost:8080/api. Проблема в том, что localhost определяется браузером, в котором выполняется код. Для посетителя это означает его собственный компьютер, а не ваш разработочный сервер. Путь запроса фактически становится:
User's Browser
↓
localhost:8080
↓
User's computer
Если у посетителя случайно не запущен бэкенд на порту 8080, каждый запрос терпит неудачу.
Решение заключается в том, чтобы перестать жестко задавать адрес и читать его из переменной среды. В проекте Vite значение указывается в файле env:
VITE_API_URL=https://your-backend.example.com/api
а затем оно читается в коде с помощью import.meta.env:
const API_URL = import.meta.env.VITE_API_URL;
Теперь во время разработки можно использовать адрес http://localhost:8080/api, в то время как в производственной версии — что-то вроде https://api.example.com/api, причем код при этом не меняется. Это небольшая рефакторинговая операция, но она устраняет одну из самых частых ошибок при развертывании: попытку заставить браузер кого-то другого взаимодействовать с вашим ноутбуком.
Дополнительной мерой защиты является четкое сообщение об ошибке при отсутствии переменной. Если значение API_URL равно undefined, запросы направляются по таким путям, как undefined/users, что затрудняет диагностику. Однострочная проверка при запуске, которая записывает лог или вызывает исключение при отсутствии значения, позволяет сэкономить время в будущем.
2. Переменные окружения для продакшн-среды так и не были настроены
Переменные окружения — следующий распространенный источник проблем. Локально у вас, вероятно, есть файл .env, в котором хранится все необходимое приложению:
VITE_API_URL=http://localhost:8080/api
VITE_APP_NAME=MyApp
Эти значения существуют только на вашем компьютере. Платформа хостинга не читает ваш локальный файл .env (и не должна этого делать, поскольку такой файл обычно исключается из систем контроля версий). Ваша версия приложения для продакшн-среды должна получать собственные значения, настроенные в параметрах переменных окружения провайдера:
VITE_API_URL=https://your-backend.example.com/api
После добавления или изменения значения там необходимо запустить новую развертку. С Vite этот шаг является обязательным: только переменные, префиксированные VITE_, становятся доступными для клиентского кода, и Vite заменяет их в пакете во время сборки. Изменение переменной в панели управления никак не влияет на пакет, который уже был собран с старым значением.
Переменные фронтенда не являются секретами
В этом кроется важный урок по безопасности. Размещение значения в .env не делает его конфиденциальным. Все, что становится доступным для клиента, компилируется в JavaScript, который загружается и может быть прочитан каждым посетителем. Пароли к базам данных, приватные ключи API и другие учетные данные серверной части никогда не должны храниться в переменных VITE_*. Храните их на серверной части или за функцией серверной стороны, позволяя фронтенду вызывать конечную точку, которая будет использовать их от его имени.
3. Правила CORS разрешают только исходный адрес вашего приложения
Ошибки кросс-оригинального обмена ресурсами — ещё одно типичное сюрприз после развертывания. Представьте фронтенд, предоставляемый с:
https://myapp.example.com
и бэкенд, предоставляемый с:
https://api.example.com
Это разные исходные адреса, поскольку исходный адрес — это комбинация схемы, хоста и порта. Браузеры соблюдают политику одинакового исходного адреса, поэтому чтобы фронтенд мог читать ответы от API, бэкенд должен отправлять заголовки CORS, которые прямо разрешают использование исходного адреса фронтенда.
Во время разработки бэкенд, вероятно, был настроен так, чтобы принимать запросы от разработческого сервера Vite:
http://localhost:5173
Всё работало, поэтому легко предположить, что настройки CORS выполнены. Однако после развертывания исходный адрес фронтенда становится:
https://myapp.example.com
Для браузера это совершенно новый источник. Если на бэкенде по-прежнему указан только http://localhost:5173, развернутый фронтенд будет заблокирован. Для бэкенда Spring Boot разрешение использования производственного источника в контроллере может выглядеть так (фрагмент на Java):
@CrossOrigin(origins = "https://myapp.example.com")
@RestController
public class UserController {
// ...
}
Аннотирование каждого контроллера подходит для небольших сервисов, но в более крупной кодовой базе проще поддерживать единую централизованную конфигурацию CORS, и её не так легко забыть при добавлении новых конечных точек. Независимо от технологической стековой сборки правило остаётся прежним: разрешённые источники должны полностью соответствовать реальному производственному источнику фронтенда, включая https и любые поддомены. Распространённым рабочим решением является чтение списка разрешённых источников из собственной конфигурации окружения бэкенда, чтобы в режимах разработки, тестирования и производства разрешался доступ соответствующего фронтенда.
Имейте в виду, что CORS — это механизм браузера. Запрос, который отклоняется с ошибкой CORS в браузере, может быть успешным при использовании curl или Postman, поэтому тестирование API вне браузера может дать ложно положительный результат.
4. Обновление клиентского маршрута возвращает 404
Эта проблема заставляет разработчиков изучать код маршрутизатора, даже когда тот работает нормально. Предположим, у приложения есть такой маршрут:
/dashboard
Вы попадаете на главную страницу, нажимаете ссылку на панель управления, и она отображается без проблем. Вы можете свободно навигировать. Затем вы нажимаете «Обновить», и сервер отвечает:
404 Not Found
Причина кроется в принципе работы одностраничных приложений. Когда вы нажимаете на ссылку, React Router обновляет URL и полностью отрисовывает соответствующий компонент в браузере; запрос к /dashboard никогда не доходит до сервера. Когда вы обновляете страницу или открываете этот URL напрямую, браузер запрашивает хостинговый сервер файл /dashboard. Если сервер знает только о реальных файлах, он ищет ресурс с таким путем, не находит его и возвращает ошибку 404.
Решение заключается в использовании правила обратного захвата на хосте: любой запрос к маршруту фронтенда, который не соответствует реальному файлу, должен быть перезаписан или перенаправлен на index.html. Затем браузер загружает приложение, а React Router считывает URL и отображает соответствующий экран. В документации по развертыванию React Router описано это требование, и большинство статических хостов предоставляют для этого настройку или небольшой файл конфигурации. Поэтому, если клик работает, но обновление не происходит, оставьте маршруты в покое и сначала проверьте настройки перезаписи SPA хоста. Если вам нужно вспомнить, как изначально объявляются маршруты, ознакомьтесь с основами React Router.
5. Пути импорта с неправильной раскладкой букв
Некоторые ошибки остаются незамеченными до тех пор, пока сборка не выполняется на другой операционной системе. Представьте файл компонента с таким именем:
Header.jsx
Импортируется вот так:
import Header from "./components/header";
Единственная разница — одна заглавная буква, которую легко упустить при проверке кода. Файловые системы расходятся во мнениях относительно того, имеет ли это значение. По умолчанию в macOS и Windows не учитывается регистр, поэтому импорт проходит успешно и приложение запускается. Большинство серверов сборки и контейнеров работают под Linux, чьи файловые системы учитывают регистр; поэтому Header.jsx и header.jsx считаются двумя разными именами, и сборка проваливается с ошибкой отсутствия модуля.
Решение заключается просто в том, чтобы каждый импорт полностью совпадал по символам с файлом на диске. Исходя из этого:
components/Header.jsx
Запишите импорт вот так:
import Header from "./components/Header";
Такое же внимание следует уделять именам каталогов. Ещё одна ловушка, о которой стоит знать: изменение имени файла только с учётом регистра может остаться незамеченным Git на файловой системе, нечувствительной к регистру, поэтому репозиторий может сохранять старое имя, даже если в вашем редакторе отображается новое. Проверка имени, которое на самом деле фиксирует Git, быстро проясняет ситуацию.
6. Изображения и статические ресурсы исчезают
Иногда само приложение загружается, но его изображения, шрифты или иконки — нет. Обычно это означает, что к ресурсам обращаются по путям, которые работали на сервере разработки, но отсутствуют в финальной версии приложения.
Vite предлагает два разных механизма, и полезно знать, какой из них вы используете. Первый — импорт ресурса из исходного кода:
import logo from "./assets/logo.png";
и использование импортированного значения в качестве источника изображения:
function App() {
return <img src={logo} alt="Logo" />;
}
Поскольку ресурс проходит через граф модулей, Vite обрабатывает его во время сборки, копирует его в результаты сборки с генерированным именем файла и предоставляет правильный URL. Вам никогда не придётся угадывать окончательный путь.
Второй механизм — это каталог public для файлов, которые должны предоставляться в неизменном виде без дополнительной обработки:
public/
└── logo.png
Файлы из этого каталога копируются в корневую директорию результатов сборки, поэтому к ним обращаются с абсолютным URL-путём:
<img src="/logo.png" alt="Logo" />
src/assets/logo.png, но браузер понимает только те URL, которые действительно предоставляет развернутый сервер. Если приложение размещено под подпутем вместо корня домена, абсолютные пути вроде /logo.png также могут не сработать, и именно для решения этой проблемы существует опция base у Vite.
7. Разработочный сервер — это не продакшн-версия
npm run dev
Запускает сервер разработки с возможностью динамической замены модулей, подробными предупреждениями и преобразованием модулей по требованию. Однако на сервере развертывается нечто другое — собранный и оптимизированный набор статических файлов. Для проекта Vite его создают с помощью:
npm run build
А хост-сервер предоставляет полученный результат. Поскольку эти два способа обрабатывают код по-разному, вполне возможно, что приложение будет работать под:
npm run dev
Но откажет при работе под:
npm run build
Такая ошибка — хорошая новость, если вы обнаружите её сами, поскольку сможете её исправить до того, как что-либо дойдёт до пользователей. Vite также позволяет локально предоставлять собранный результат с помощью:
npm run preview
Это гораздо ближе к тому, что увидят посетители, чем на разработчическом сервере. Сформируйте привычку собирать проект и просматривать его локально перед каждой разверткой, вместо того чтобы позволять платформе хостинга становиться первым местом, где запускается производственная версия. Подключение команды npm run build к процессу CI автоматически обеспечивает такую же гарантию при каждом pull request.
8. Ваш локальный каталог node_modules скрывает отсутствующие зависимости
Долгосрочное существование каталога node_modules может создавать впечатление, что проект в лучшем состоянии, чем на самом деле. За недели разработки там накапливаются пакеты: те, что устанавливались временно для тестирования, косвенные зависимости других пакетов, остатки с отказанной ветки. Ваш код может импортировать один из этих пакетов и работать только потому, что он случайно там присутствует.
Среда развертывания начинается с чистого состояния. В неё устанавливается ровно то, что описано в файле package.json и lockfile, ничего больше. Поэтому если ваш код содержит:
import axios from "axios";
то Axios необходимо указать как реальную зависимость проекта, например:
{
"dependencies": {
"axios": "^1.0.0",
"react": "^19.0.0",
"react-router-dom": "^7.0.0"
}
}
Числа версий в этом примере носят иллюстративный характер; используйте те версии, которые действительно требуется вашему проекту. Цель состоит в том, чтобы при копировании проекта и его установке в нём всегда было всё необходимое для сборки и запуска приложения. Сохраняйте как файл package.json, так и lockfile, поддерживая между ними синхронизацию, чтобы при каждой установке загружались одинаковые версии.
Чтобы воссоздать условия развертывания локально, удалите папку node_modules и установите зависимости заново, желательно с помощью команды npm ci, которая выполняет установку строго на основе файла lockfile и прерывает процесс, если в этом файле и в package.json есть расхождения. Если после этого сборка сломается, значит, вы обнаружили необъявленную зависимость раньше, чем это сделало серверное окружение.
9. Фронтенд работает нормально, а бэкенд не запускается
Хочется винить React каждый раз, когда интерфейс выглядит неправильно, но проблема часто кроется где-то дальше в стеке вызовов. Предположим, что развернутый фронтенд загружается успешно по адресу:
https://myapp.example.com
Вы открываете панель управления — она пуста. В вкладке Network вы видите:
GET https://api.example.com/api/dashboard
500 Internal Server Error
Эта строка многое говорит о проблеме. Приложение на React выполнило свою задачу и отправило запрос; сервер его получил, но потом сбойно обработал. Возможные причины — поврежденное соединение с базой данных, отсутствие переменной окружения на стороне сервера, проблемы с аутентификацией, неразрешенное исключение или настройки, отличные от тех, что используются в локальной среде.
Типичный запрос полноценного приложения проходит через следующие слои:
React Frontend
↓
REST API
↓
Backend
↓
Database
Сбой на нижнем уровне влияет на работу всего приложения. Если к базе данных невозможно подключиться, панель управления будет выглядеть неисправной, хотя каждая строка кода фронтенда корректна. Поэтому диагностика не должна ограничиваться браузером. Когда запрос к API сбивается, проверьте вкладку «Сеть», а затем логи сервера. Если браузер сообщает о коде 500, а в логах сервера указано:
Database connection failed
Вы сталкиваетесь с проблемой бэкенда или инфраструктуры, а не с ошибкой отображения. Фронтенд, который показывает четкое сообщение об ошибке при неудачных запросах вместо пустой страницы, делает эту разницу видимой как для пользователей, так и для разработчиков.
10. Аутентификация работает локально, но терпит неудачу после развертывания
Процессы входа особенно чувствительны к различиям среды. Локально последовательность действий может выглядеть так:
localhost → Login → Dashboard
В производственной среде она становится такой:
Production → Login → Dashboard doesn't load
Несколько ранее упомянутых проблем могут самостоятельно вызвать эту ситуацию. Фронтенд может по-прежнему обращаться к неверному URL API, правила CORS бэкенда могут не включать источник из производственной среды, или токены могут быть неправильно настроены для новой среды. Однако чаще всего причиной являются куки.
Почему куки ведут себя иначе в производственной среде
То, будет ли браузер хранить и отправлять куки, зависит от их атрибутов: Secure, HttpOnly, SameSite, Domain и Path. В локальных условиях обычно все работает с одним хостом, поэтому эти атрибуты редко имеют значение. В производственной среде фронтэнд и API могут находиться на разных доменах через HTTPS, и тогда браузер начинает соблюдать установленные правила. Например, куки с атрибутом SameSite=None принимаются только в том случае, если у них также указан атрибут Secure.
Запросы между разными источниками, передающие учетные данные, также требуют сотрудничества обеих сторон. Клиент должен явно разрешить отправку учетных данных, а сервер — прямо разрешить их использование для конкретного источника; для запросов с учетными данными не принимаются источники вида «звездочка». Если хотя бы одна из сторон не выполняет свою часть, запрос на вход может казаться успешным, но файл cookie сессии так и не будет отправлен при последующих вызовах.
Вывод: процесс входа, который работает на localhost, был протестирован только в самой простой возможной среде. Проведите тестирование снова в условиях HTTPS, доменов, настроек cookie и правил CORS, близких к реальным.
Метод отладки по слоям
Когда происходит сбой при развертывании, наименее эффективным решением является изменение случайных компонентов в надежде на улучшение. Эффективным подходом является выяснение того, в каком слое происходит сбой, прежде чем что-либо менять в коде.
Начните с консоли
Откройте инструменты разработчика браузера (обычно F12) и просмотрите консоль. Сообщения вроде Failed to fetch, нарушений правил CORS, незафиксированных исключений JavaScript и ошибок 404 для скриптов или ресурсов указывают на разные причины проблемы.
Просмотр вкладки Network
Перейдите на вкладку Network и перезагрузите страницу. Для каждого запроса вы можете увидеть точный URL, метод HTTP, код состояния, тело ответа и часто заголовки. При проблемах с API это самый информативный вид отображения. Возможно, вы увидите:
GET /api/users
Status: 404
или:
GET /api/users
Status: 401
или:
GET /api/users
Status: 500
Каждый код состояния сужает круг поиска:
404: запрашиваемый ресурс или маршрут отсутствует по этому URL.401: требуется аутентификация, или отправленные учетные данные отсутствуют или недействительны.
403: сервер понял, кто вы, но отказывается разрешить выполнение действия.500: сервер столкнулся с внутренней ошибкой при обработке запроса.Проверьте URL, который на самом деле использовал браузер
Не полагайтесь на предположения относительно действий исходного кода; посмотрите на запрос, который фактически отправил браузер. Если в продакшене используется:
http://localhost:8080/api/users
а на самом деле бэкенд расположен по адресу:
https://api.example.com/api/users
вы практически наверняка нашли проблему — это проблема с URL API из раздела 1.
Проследите за ошибками сервера в журналах бэкенда
500 означает, что ответ находится на сервере. Журналы работы бэкенда могут выявить сбой при подключении к базе данных, отсутствие переменной или исключение, о котором фронтенд не может сообщить. Именно привычка переходить к следующему уровню, когда есть такие признаки, позволяет быстро отлаживать процесс развертывания.
Рассмотрим реальный сбой
Представьте приложение полного стека, которое развертывается без ошибок, но панель управления отображает пустоту. Инстинктивная реакция — предположить, что в компоненте панели управления есть баг:
"Что-то определенно не так с компонентом панели управления."
Вместо того чтобы открывать этот компонент, откройте вкладку Network. Там вы увидите:
GET https://api.example.com/api/dashboard
Status: 500
Это уже исключает фронтенд как основную причину: запрос был отправлен. Далее в журналах бэкенда видно:
Database connection failed
При наличии обоих доказательств вся цепочка событий становится ясной:
React Frontend
↓
API request
↓
Backend
↓
Database connection failed
↓
500 response
Компонент панели управления никогда не был причиной проблем. Десять минут на изучение инструментов браузера и логов сервера заменили бы часы работы над рефакторингом кода. Умение одновременно анализировать ошибки браузера, сетевой трафик и логи сервера — один из самых ценных навыков в повседневной работе с программным обеспечением.
Чек-лист перед развертыванием
Самый надежный способ избежать неожиданностей при развертывании — отследить перед отправкой каждый шаг запроса от браузера до базы данных. Практический чек-лист:
- Запустите
npm run buildлокально и убедитесь, что операция завершилась успешно, затем проверьте результат с помощьюnpm run preview. - Убедитесь, что указан URL производственного API и что ни один запрос не направлен на
localhost.
Ни один чек-лист не гарантирует безупречной выпускной версии, но этот помогает выявить большинство распространённых проблем на ранних этапах, когда их легко исправить. Чтобы получить более полное представление о том, что должно входить в надёжную настройку проекта с самого начала, базовые требования к React для производственного использования охватывают аспекты инструментария.
Почему утверждение «всё работает локально» мало что говорит
Ваша машина представляет собой контролируемую и способную к компромиссам среду. Вы знаете, где работает бэкенд, какие переменные определены, что установлено в node_modules и какая база данных используется приложением. В производственной среде почти всё меняется: другой домен, другой адрес API, другие переменные среды, другая база данных, более строгие правила CORS и куки, сервер, для которого необходимо указать, как обрабатывать запросы с клиента, а также часто другая операционная система. Код может быть совершенно правильным, в то время как конфигурация вокруг него ошибочна.
Именно поэтому развертывание следует рассматривать как ещё одну среду для тестирования, а не как окончательный этап после завершения разработки. Среды стейджинга, сборки CI и превью-развертывания существуют для того, чтобы перенести тестирование на более ранний этап.
Основные выводы
- Когда после развертывания что-то ломается, сначала узнайте, какой слой не работает, прежде чем спрашивать, в чём проблема с React.
- Если ничего не загружается, проверьте результаты сборки и ресурсы; если страница загружается, но данные — нет, посмотрите вкладку «Сеть» и URL API.
- Код
500направляет вас к логам серверной части; ошибка CORS — к списку разрешённых источников сервера. - Маршрут, который работает при клике, но возвращает ошибку 404 при обновлении страницы, требует резервного варианта SPA на хосте, а не изменений маршрутизатора.
- Отсутствие значений обычно означает, что переменные окружения продакшена так и не были заданы или приложение не пересобиралось после их установки.
- Сбои аутентификации часто связаны с URL API, настройками CORS и атрибутами куки, поэтому проверьте все три аспекта.
Столкновение с такими проблемами — это не признак слабых навыков программирования; именно так все учатся понимать, из чего состоят настоящие системы. Цель не в том, чтобы никогда не видеть ошибок, а в том, чтобы быстро находить место их возникновения.