Как создать собственный React Router v4
Перевод статьи Тайлера Макгинниса
До сих пор помню свои чувства, когда я начал изучать клиентский роутинг в веб-приложениях. В то время я был молокососом, делающим первые шаги в SPA, и это порядком выносило мне мозг. С самого начала я был склонен относиться к основному коду приложения и коду роутера как к двум независимым друг от друга сущностям. Своего рода сводным братьям, которые недолюбливают друг друга, но вынуждены жить вместе.
В течение последних нескольких лет мне посчастливилось обучать идее роутинга других разработчиков. К сожалению, большинство из них сталкиваются с теми же трудностями. Я вижу несколько причин для этого. Во-первых, роутинг — вещь достаточно сложная и комплексная. Это вынуждает авторов библиотек создавать вокруг него еще более сложные абстракции. Во-вторых, пользователи библиотек склонны слепо им доверять и не пытаются понять, что происходит “внутри”.
Данный урок мы посвятим решению обеих проблем. Мы создадим собственную упрощенную версию роутера, чтобы понять, оправдана ли абстракция, с которой мы имеем дело при работе с RRv4.
Ниже — код приложения. Используем его для тестирования, когда роутер будет готов. Поэкспериментировать с результатом можно здесь.
Для тех, кто не знаком с React Router v4, — короткое введение. <Route>
'ы рендерят нужный UI, если URL приложения соответствует пути, определенному в свойстве path
. Компонент Link
обеспечивает понятный и декларативный способ навигации по приложению. Другими словами, Link
изменяет URL, а Route
обновляет UI для этого URL.
Научить основам RRv4 — не цель урока. Поэтому, если код выше всё еще не понятен, я рекомендую изучить документацию, поэкспериментировать с примерами и вернуться в нему, когда почувствуете в себе силы.
Первое, на что следует обратить внимание: есть два уже знакомых нам компонента — Link
и Route
. Оба —части библиотеки. В React Router v4 мне больше всего импонирует то, что его API —это “просто компоненты” (“Just Components™”). Для разработчиков знакомых с React это значит, что наработанные практики работы с компонентами и их композицией будут полезны: делая собственный роутер, мы будем создавать ни что иное, как реакт-компоненты.
Начнем с компонента Route
. Но, перед тем как погрузиться в код, посмотрим на его API (по большому счету —props
, которые он принимает).
В примере выше, как вы могли заметить, Route
имеет три свойства:exact
, path
и component
. Значит, propTypes
для него будут выглядеть так:
Обратите внимание на следующее. Во-первых, path
не является обязательным свойством: если он не задан, Route
автоматически отрендерит заданный UI. Во-вторых, component
тоже не обязателен, так как есть несколько способов сообщить роутеру, какой UI выводить. Один из способов (его нет в примере выше) — свойство render
. Выглядит это следующим образом:
Свойство render
позволяет использовать функцию, возвращающую нужный UI, без создания отдельного компонента. Добавим его в propTypes
:
Теперь, когда мы имеем представление о props
компонента Route
, вернемся к вопросу, что этот компонент делает. Route
“выводит определенный UI, если URL совпадает с путем, указанным в свойстве path
”. Из этого следует, что требуется определенная функциональность для проверки текущего URL и значения path
. Если они совпадают, рендерится нужный UI; в ином случае ничего не происходит (компонент возвращает null).
В коде это выглядит так (учитывайте, что функция проверки соответствия — matchPath
— будет создана ниже):
В таком виде, Route
выглядит более-менее полно. Если текущий URL и path
компонента совпадают, выводим нужный UI; если нет — возвращаем null
.
Теперь сделаем шаг назад и поговорим о концепции роутинга в целом. В SPA у пользователя есть только два способа изменить URL. Первый — ссылки, второй — кнопки браузера вперед/назад. Роутер должен знать текущий URL и выводить соответствующий UI. Кроме того, он должен уметь ловить изменение URL. Мы знаем, что есть только два способа его изменения — ссылки и кнопки навигации браузера, а значит, мы можем реагировать на эти события.
Со ссылками разберемся чуть позже, когда будем говорить о Link
-компоненте, а пока посмотрим на кнопки вперед/назад. React Router слушает изменение URL с помощью метода listen
библиотеки history. Но, чтобы не тянуть дополнительный код, мы будем работать с событием popstate
. popstate
срабатывает при клике на кнопки навигации браузера —как раз то, что нам нужно. Так как именно <Rout>
’ы отвечают за вывод UI, необходимо, чтобы они слушали popstate
и могли обрабатывать его по описанной выше схеме: URL подходит — страница перерисовывается, нет — ничего не происходит. Итак, как это выглядит в коде:
Что изменилось: в componentWillMount
* добавили слушатель события popstate
: когда событие срабатывает, вызываем принудительный ререндер методом forceUpdate
.
*Прим. переводчика: componentWillMount
— метод так называемого жизненного цикла компонента в React. Вызывается непосредственно перед началом рендеринга компонента. componentWillUnmount
— перед удалением компонента из DOM.
Сейчас количество <Rout>
'ов в проекте не имеет значения. Каждый из них подписывается на событие, проверяет URL на соответствие и при необходимости рендерит UI.
Один момент, который мы пока незаслуженно упустили ,— функция matchPath
. Она имеет первостепенное значение, так как именно она “принимает решение” о соответствии URL и path
роута. При работе с ней важно учитывать свойство exact
. Вот выдержка из документации:
Если
exact
равно true, то соответствием будет считаться только полное совпадениеpath
иlocation.pathname
.
Самое время разобраться с этим подробнее. В нашем Route
-компоненте, matchPath
используется следующим образом:
const match = matchPath(location.pathname, { path, exact })
Переменная match
должна принимать объект либо null, в зависимости от совпадения URL и path
. Зная это, попробуем написать первую часть функции matchPath
:
const matchPatch = (pathname, options) => {
const { exact = false, path } = options
}
В этом примере мы имеем дело магией ES6. Мы говорим: “создать константу exact, записать в нее значение options.exact, а если oно отсутствует — значение false; также создать константу path и записать в нее значение options.path”.
Я уже упоминал, что path
— не обязательное свойство, потому что если оно не задано, Route
будет обработан автоматически. Так как рендер зависит от функции matchPath
, добавим ей эту функциональность.
Теперь черед поиска соответствия. React Router v4 использует для этой цели pathToRegexp, но мы ограничимся простым регулярным выражением.
Метод .exec
возвращает массив, который включает в себя строку-совпадение, иначе — null.
Вот каким будет результат match
в нашем примере при переходе на URL /topics/components
Обратите внимание, здесь мы видим результат по каждому из <Rout>
'ов приложения, так как каждый из них вызывает matchPath
-функцию.
Теперь, понимая как работает match
, посмотрим, что делать при найденном соответствии.
Мы говорили, что есть только два способа изменения URL в приложении, и определились как работать с кнопками навигации браузера (через слушатель события popstate
). Теперь научимся взаимодействовать со ссылками, реализованными с помощью компонента Link
.
Его свойства будут выглядеть так:
<Link to='/some-path' replace={false} />
to
— строка с адресом ссылки
replace
— булево значение: при true клик по ссылке заменит текущую запись в истории вместо добавления новой.
Добавив их в propTypes, получим код:
Мы знаем, что метод render
этого компонента возвращает ссылку, но очевидно, необходимо избежать перезагрузки всей страницы при смене роута. Добавим функцию-обработчик для onClick
.
Сам React Router использует методы push
и replace
библиотеки history, мы же можем ограничиться возможностями HTML5 History API (методы pushState
и replaceState
) и избежать лишних зависимостей.
Хотя мы отказались от history library, для реального проекта с React Router она имеет критическое значение, так как нормализует работу c историей в разных браузерах.
Методы pushState
и replaceState
принимают три аргумента. Первый — объект, ассоциированный с объектом history
: он не потребуется, потому передадим пустой объект. Второй — заголовок, также не нужен, поэтому передадим null. Третий, и его мы используем, — относительный путь URL.
Внутри компонента Link
, в зависимости от значения replace
вызываем один из этих методов:
Теперь нам осталось добавить один критически важный функционал. Если вы уже попробовали поработать с роутером в том виде, в каком он сейчас, то должны были обнаружить следующую проблему. Попытки навигации по приложению приводят к изменению URL, но не UI. Причина — наши <Rout>
’ы не знают об этих изменениях. Необходимо, чтобы каждый из экземпляров компонента следил за обновлением URL и при необходимости вызывал forceUpdate
-функцию.
React Router работает с этой функциональностью посредством комбинации инструментов (setState, context, history.listen).
Чтобы не усложнять код, мы будем отслеживать текущие экземпляры <Route>
и добавлять их в массив. При изменении URL проходим циклом по этому массиву, вызывая forceUpdate
для каждого элемента.
Заметьте: мы создали две функции. register
вызывается в начале жизненного цикла компонента, unregister
— в конце. Теперь, при изменении URL (клике на <Link>
и вызове historyPush
и historyReplace
) мы перебираем циклом элементы массива instances
и вызываем forceUpdate
.
Обновим код <Route>
-компонента:
Обновим historyPush
и historyReplace
:
🎉 Теперь все экземпляры компонента Route
готовы к проверке URL и ре-рендерингу при каждом клике на <Link>
.
Ниже — полный код нашего роута. Тестовое приложение работает с ним идеально.
Бонус: в React Router API также есть компонент <Redirect>
. Но, используя наш вариант роутера, создать аналог будет вовсе не трудно:
Заметьте, <Redirect>
не рендерит UI, его задача — управление роутингом, что следует из его имени.
Я надеюсь, этот урок был полезен для понимания работы React-роутера, и поможет вам оценить всю “элегантность” его API, основанного на компонентном подходе. Я люблю повторять, что React помогает стать лучшими JavaScript-разработчиками. Теперь я могу добавить, что React Router помогает стать лучшими React-разработчиками. Всё в этой библиотеке— компоненты, и если вы знаете React, вы знаете React Router.
P.S. Спасибо Райану, автору первой реализации минироутера и человеку, помогавшему мне с этой статьей.