Как создать собственный 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. Спасибо Райану, автору первой реализации минироутера и человеку, помогавшему мне с этой статьей.


Подписывайтесь на мой аккаунт в Твиттере и читайте блог.