Навигация: несколько экранов в одном приложении
Зачем это нужно
Интерфейс к контракту — это несколько экранов: баланс, форма вклада, список операций, карточка одной операции, страница администратора. Складывать их в один экран нельзя, а перезагружать страницу на каждый переход — значит терять подключение к кошельку и всё состояние.
Выход известен: адрес меняется, страница остаётся. Браузер это умеет, и на этом умении построены все библиотеки навигации.
Эта глава — про то, как устроен механизм под ними, как выглядит работа с маршрутами и про одну ловушку, которую находят в последний час: переходы работают, а обновление страницы даёт ошибку.
Адрес меняется, документ остаётся
Прогон меняет адрес из кода и смотрит, что при этом происходит с документом:
── смена адреса из кода ──
было : /docs/blockchain/web-html/
после pushState : /ops?kind=deposit
метка на месте : да — документ тот же, перезагрузки не было
заголовок страницы не изменился: "HTML: из чего состоит страница…"
ещё один шаг : /ops/0xabc
после «назад» : /ops?kind=deposit
поймано popstate: ["/ops?kind=deposit"]
состояние шага : {"screen":"ops"}
Метка на месте, заголовок прежний, объекты живы — документ тот же самый. Именно поэтому подключение к кошельку и загруженные данные переживают переход.
Маршрутизатор: адрес решает, что показывать
Писать это руками никто не станет: библиотека делает то же самое и добавляет разбор адреса. Сегодня она называется react-router, текущая версия — восьмая; старое имя с окончанием -dom осталось для совместимости.
import { BrowserRouter, Routes, Route, Link, NavLink } from "react-router";
<BrowserRouter>
<nav>
<NavLink to="/">Баланс</NavLink>
<NavLink to="/ops">Операции</NavLink>
</nav>
<Routes>
<Route path="/" element={<Balance />} />
<Route path="/ops" element={<OpsList />} />
<Route path="/ops/:hash" element={<OpCard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
Обычная ссылка внутри приложения — самая частая ошибка на первом маршрутизаторе. Внешне всё работает: переход происходит. Но страница перезагружается целиком, подключение к кошельку теряется, и пользователь заново нажимает «подключить».
Что достаётся из адреса
function OpCard() {
const { hash } = useParams();
return <p>операция {hash}</p>;
}
function OpsList() {
const [params, setParams] = useSearchParams();
const kind = params.get("kind") ?? "все";
return (
<>
<p>фильтр: {kind}</p>
<button onClick={() => setParams({ kind: "deposit" })}>Только вклады</button>
</>
);
}
Переменная часть адреса — то, что стоит после двоеточия в маршруте. Для карточки операции это хеш транзакции.
Строка запроса — то, что после вопросительного знака. Годится для фильтров, сортировки, открытой вкладки: всего, что уточняет экран, но не меняет его.
Отсюда главный приём главы: адрес — это часть состояния. Фильтр, лежащий в адресе, переживает обновление страницы, попадает в закладки и работает при пересылке ссылки. Тот же фильтр, лежащий только в состоянии компонента, не переживает ничего.
Ловушка: обновление страницы
Переходы работают. Пользователь копирует адрес, присылает товарищу — и тот видит ошибку. Или сам нажимает «обновить» — и снова ошибка.
── пользователь нажал «обновить» на адресе приложения ──
/docs/blockchain/web-html/ сервер ответил 200
/ops/0xabc сервер ответил 404
/ops сервер ответил 404
Причина простая. Адрес /ops/0xabc придумало приложение, уже загруженное в браузер. На сервере такого файла нет и никогда не было. Пока переход делает приложение, всё хорошо; как только адрес приходит на сервер напрямую — ошибка.
Лечение одинаково у всех: сервер на любой неизвестный адрес отдаёт ту же главную страницу. Приложение загружается, видит адрес и показывает нужный экран.
# при раздаче статики
try_files $uri $uri/ /index.html;
В режиме разработки это уже сделано за вас — поэтому ошибку и находят только после первой настоящей выкладки.
Проверяется за десять секунд: соберите проект, откройте внутренний адрес и нажмите «обновить». Если вместо экрана ошибка — настройте раздачу. Делать это надо не в день сдачи.
Сколько это стоит
без маршрутизатора: 222.53 кБ │ gzip: 69.28 кБ
с маршрутизатором : 258.53 кБ │ gzip: 82.30 кБ
Тридцать шесть килобайт, тринадцать при передаче. За это вы получаете разбор адреса, историю, ссылки, вложенные экраны и переменные части пути.
Цифра приведена не для того, чтобы отговаривать, а чтобы показать порядок: библиотеки во фронтенде стоят измеримо, и отчёт сборки эту стоимость показывает. Если один пакет добавил двести килобайт — это повод посмотреть, что именно он делает.
Типичные ошибки
Обычная ссылка вместо ссылки маршрутизатора. Перезагрузка и потерянное подключение.
Нет маршрута «всё остальное». Неизвестный адрес даёт пустой экран без объяснений.
Сервер не настроен на отдачу главной страницы. Переходы работают, обновление — нет.
Фильтр в состоянии вместо адреса. Ссылку на отфильтрованный список нельзя переслать.
Новый шаг истории на каждое нажатие фильтра. Чтобы вернуться, приходится нажать «назад» десять раз. Для таких случаев есть замена текущего шага.
Маршрут с переменной частью выше точного. Порядок в списке маршрутов важен: более общий перехватит то, что предназначалось точному.
Забыли обёртку. Ссылки и маршруты падают с сообщением о том, что они вне маршрутизатора.
Слова главы
- Смена адреса из кода — адрес и история меняются, документ остаётся тот же.
- Состояние шага — объект, сохранённый вместе с адресом; возвращается при переходе назад.
- Событие «назад/вперёд» — единственный способ узнать о нажатии кнопок браузера.
- Маршрут — соответствие «адрес → компонент»; со звёздочкой ловит остальное.
- Переменная часть пути — после двоеточия; достаётся внутри компонента.
- Строка запроса — уточнение экрана: фильтр, сортировка, вкладка.
- Ссылка маршрутизатора — перехватывает нажатие вместо перезагрузки.
- Отдача главной страницы на любой адрес — настройка сервера, без которой обновление даёт ошибку.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
<Route path="/ops/:hash" element={<OpCard />} />
На чемпионате
Проверьте обновление страницы на внутреннем адресе. Десять секунд сейчас против получаса в день сдачи.
Фильтры и выбранную операцию — в адрес. На защите это выглядит убедительно: вы открываете ссылку, и экран тот же.
Маршрут «не найдено» — обязательно. Пустой экран читается как поломка, даже если всё работает.
Адрес контракта и сеть — тоже кандидаты в адрес страницы. Тогда одну и ту же сборку можно показать на двух сетях, просто открыв разные ссылки.
И про объём: маршрутизатор — первая библиотека, которую вы добавляете сверх самого необходимого. Посмотрите на отчёт сборки до и после. Привычка смотреть на эти числа окупается к третьей библиотеке.
Куда углубиться
- Вложенные маршруты — общий каркас и сменная середина.
- Загрузка данных на уровне маршрута — данные готовы до показа экрана.
- Защищённые экраны — не пускать, пока кошелёк не подключён.
- Подгрузка экрана по требованию — не тащить весь код сразу.
- Навигация на якорях — вариант, который работает без настройки сервера, и чем он плох.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- React Router: начало работы — маршруты, ссылки, параметры.
- MDN: History API — механизм, на котором всё построено.
- MDN: событие popstate — когда оно приходит, а когда нет.
- MDN: URLSearchParams — разбор строки запроса.
- Vite: развёртывание статики — настройка сервера для адресов приложения.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.