# теория · шаг 2 из 5
SPA: одна строка и три ловушки вокруг
Коротко
Одностраничное приложение держится на одной строке: «есть файл - отдай файл,
нет файла - отдай index.html». Всё остальное в уроке - про то, что ломается
вокруг неё.
- Строка:
try_files $uri /index.html;. Корень - каталог сборки (dist), а не корень репозитория. - Опечатка в адресе API вернёт не 404, а
index.htmlс кодом 200. Фронтенд получит HTML вместо JSON и упадёт с сообщением, которое ни на что не указывает. - Пропавший ассет (файл сборки: скрипт, стиль, шрифт, картинка) ведёт себя так же: 200 и HTML вместо честного 404.
- Лечение - не переписать строку, а поставить рядом ещё два блока:
/api/и/assets/сtry_files $uri =404. - Четвёртая беда -
index.htmlв кеше браузера, ей посвящена следующая глава.
Если про три блока рядом с fallback знаешь - листай до «Теперь сам».
Сначала ответь сам
Конфиг ровно такой, как советует любая инструкция:
root /var/www/shop/dist;
index index.html;
location / {
try_files $uri /index.html;
}
Фронтенд просит /api/carts, а в адресе опечатка - у бэкенда endpoint
называется /api/cart. Что придёт в ответ?
Инстинктивный ответ - 404, и фронтенд покажет ошибку. Настоящий - 200 и
index.html. Приложение получит HTML там, где ждало JSON.
На что это похоже
Fallback - это дежурный, который всех отправляет в справочную.
Пока в справочную отправляют заблудившихся посетителей, всё работает. Но дежурный не различает людей: он так же отправит в справочную курьера с посылкой, водителя за подписью и инкассатора. Каждый из них уйдёт с вежливым ответом, который к его делу отношения не имеет, - и будет считать, что его обслужили.
В конфиге роль такого дежурного играет location /. Он не знает, что /api/ -
это не адрес страницы, а /assets/ - не маршрут приложения. Различать их надо
до него.
Механизм: строка, ради которой всё затевалось
server {
listen 80;
server_name shop.local;
root /var/www/shop/dist;
index index.html;
location / {
try_files $uri /index.html;
}
}
/assets/app.4f2c1b.js - файл есть, отдаётся файл. /catalog/42 - файла нет,
отдаётся index.html, и роутер приложения разбирается сам. Пользователь
обновляет страницу на глубоком адресе и не получает 404 - ради этого строка и
нужна.
Корень - каталог сборки. Не корень репозитория: там лежат исходники, .env и
node_modules, и всё это стало бы публичным.
Правило
Fallback на index.html ставится только на маршруты приложения. У API и у
каталога сборки должны быть свои блоки, отвечающие честно.
Разбор: что происходит без выделенных блоков
Проверено на nginx 1.31.5. Файлов /api/carts и /assets/propal.js на диске
нет. Сначала конфиг из одной строки - тот, что выше:
for u in /catalog/42 /api/carts /assets/propal.js; do printf '%-20s -> ' $u; curl -so /dev/null -w '%{http_code} %{content_type}\n' localhost$u; done
/catalog/42 -> 200 text/html
/api/carts -> 200 text/html
/assets/propal.js -> 200 text/html
Теперь с тремя блоками. На /api/ отвечает подставной бэкенд - nginx на порту
8080 с return 404 и JSON, как настоящее API на неизвестный адрес:
root /var/www/shop/dist;
index index.html;
location / {
try_files $uri /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
location /assets/ {
try_files $uri =404;
}
/catalog/42 -> 200 text/html
/api/carts -> 404 application/json
/assets/propal.js -> 404 text/html
-o /dev/null выбрасывает тело ответа, а -w печатает код и тип содержимого.
Вторая строка первого замера и есть ответ на вопрос из начала урока. Фронтенд
получил HTML, попытался разобрать его как JSON и упал с сообщением вида
Unexpected token '<'. Отладка съедает вечер, потому что сообщение указывает
на что угодно, только не на конфиг веб-сервера.
Третья строка - та же беда с ассетами. Удалённый шрифт или картинка вернут
index.html с кодом 200. Браузер не покажет ошибку, просто нарисует пустоту, а
во вкладке «Сеть» будет успешный запрос со странным типом содержимого.
Разбор: где наивное правило подводит
Наивное правило: «поставил три блока - конфиг SPA готов».
Готов, кроме одного, и это самая дорогая из бед. Имена файлов сборки содержат
хеш содержимого (app.4f2c1b.js) - короткий отпечаток, который сборщик
вычисляет из самого файла: поменялся файл - поменялось имя. Ссылается на них
index.html. Если сам
index.html закешировался у пользователя надолго, браузер будет тянуть старые
ассеты по старым именам - и человек останется на предыдущей версии приложения,
не подозревая об этом.
Особенно неприятно то, что у тебя это не воспроизведётся: ты выкладывал, у тебя кеш свежий. Жалобы придут от части пользователей, будут выглядеть как «у меня не работает кнопка» и лечиться советом нажать Ctrl+Shift+R.
Разбору пары «html не кешируем, ассеты кешируем навсегда» посвящена следующая глава. Пока достаточно знать, что конфиг SPA без решения про кеш не закончен.
Что ломается без этого
Unexpected token '<'во фронтенде. API-запрос получилindex.html. Проверь, есть ли блок/api/и стоит ли он раньше fallback.- Пустое место вместо картинки, а в «Сети» код 200. Ассет не найден и
подменён страницей. Нужен
try_files $uri =404в блоке сборки. - Публичными оказались
.envиnode_modules. Корнем указан репозиторий, а не каталог сборки. - Часть пользователей сидит на старой версии приложения.
index.htmlв кеше браузера.
Зачем это в работе
Проверять конфиг SPA надо не главной страницей - она откроется всегда, - а тремя запросами на несуществующие адреса:
curl -so /dev/null -w '%{http_code} %{content_type}\n' https://сайт/catalog/42
curl -so /dev/null -w '%{http_code} %{content_type}\n' https://сайт/api/net-takogo
curl -so /dev/null -w '%{http_code} %{content_type}\n' https://сайт/assets/net-takogo.js
Правильный ответ: первый - 200 и HTML, второй и третий - не 200 и не
HTML. Если все три одинаковые, конфиг состоит из одной строки, и вечер на
Unexpected token уже оплачен, просто счёт ещё не пришёл.
Это же три проверки стоит держать в тестах выкладки: они ловят потерянный блок
/api/ быстрее, чем это заметит фронтенд.
Теперь сам
1. Зачем location /assets/ { try_files $uri =404; }, если ассеты и так
лежат на диске?
Затем, что однажды не лягут: сборка сломалась, файл переименовали, деплой прошёл наполовину. Без этого блока пропажа выглядит как успешный запрос, и дефект доезжает до пользователей молча.
2. Нужен ли $uri/ в списке кандидатов SPA-конфига?
Обычно нет: в типичной сборке каталогов с index.html внутри не бывает, и
$uri/ добавляет лишнюю проверку каталога на каждый запрос. Он нужен, если
рядом живёт статика в старом стиле - /docs/ с собственным index.html.
3. Почему блок /api/ обязан стоять раньше fallback, если приоритет
location определяется не порядком строк?
Он и не про порядок строк: location /api/ - более длинный префикс, чем
location /, и выигрывает первую фазу. «Раньше» тут значит «до того, как
запрос дойдёт до fallback по логике выбора», а не «выше в файле». Порядок в
файле решал бы, только если бы оба блока были регулярками.
Главное
SPA держится на try_files $uri /index.html - «нет файла, отдай приложение».
Рядом обязаны стоять ещё два блока: location /api/, иначе ошибки API
превращаются в HTML с кодом 200, и location /assets/ с try_files $uri =404,
иначе пропавший файл притворяется страницей. Корень - каталог сборки, а не
корень репозитория.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий