
Привет, меня зовут Борис Шабанов, я — руководитель Frontend-разработки в департаменте разработки рекламных технологий Rambler Group. Сегодня я расскажу вам о том, как на нашем приложении возникли проблемы маршрутизации, и про то, как мы их решали.
Рекламные технологии – большой департамент в Rambler Group, реализующий полный стек рекламных технологий (SSP, DMP, DSP). Для конечных пользователей, а именно рекламодателей и агентств, мы делаем удобный интерфейс, который называется Лето, в который они могут заводить свои рекламные кампании, смотреть по ним статистику и управлять всем.
Интерфейс Лето — это множество экранов, форм, связанных ссылками друг на друга. Находясь в одном месте, в несколько кликов пользователь может найти нужную ему информацию.

Через несколько лет эксплуатации требования к проекту стали серьезней, и мы поняли, что нам нужно исправлять это. Так как с самого начала проект был на React, то за основу новой версии мы взяли React Create App. И GraphQL – для взаимодействия с сервером, а именно – Apollo client. Тем более в департаменте имеется большая экспертиза.
Но у нас возник вопрос, а что можно использовать для маршрутизации нашего приложения? Мы пошли в Google искать решение со словами "react" и "router", и, по сути, на первых 5 страницах ничего кроме React Router мы не нашли. "Ну, ок…" – подумали мы, умные люди рекомендуют, надо брать. Через непродолжительный период вопросов не стало меньше. Например, возьмем простой пример использования react-router:
import React from "react"; import { BrowserRouter as Router, Route, Link } from "react-router-dom"; const ParamsExample = () => ( <Router> <div> <h2>Accounts</h2> <ul> <li> <Link to="/netflix">Netflix</Link> </li> <li> <Link to="/zillow-group">Zillow Group</Link> </li> <li> <Link to="/yahoo">Yahoo</Link> </li> <li> <Link to="/modus-create">Modus Create</Link> </li> </ul> <Route path="/:id" component={Child} /> <Route path="/order/:direction" component={ComponentWithRegex} /> </div> </Router> );
Возникают вопросы:
- Как быть с URL с большим количеством параметров?
- Что делать если придет Product manager и попросит поменять URL страницы на более "дружелюбный"? "Ковровые commit" по всему проекту не очень хочется делать.
В процессе разработки мы начали думать и искать пути решения, но при этом мы жили на React-router еще.
В очередном спринте мы взяли в работу раздел "Помощь". Не сложная страница, и в течение нескольких часов мы ее сделали.
Но вся соль заключается в том, что "Помощь" — такой раздел, на который ссылаются все остальные страницы, и ссылок на нее очень много. И ссылка на каждый ответ формируется динамически. Для нас это стало еще одним поводом задуматься о смене роутера.
После продолжительных поисков в интернете мы нашли приемлемое решение — Router5.

Router5 — библиотека, которая никак не связана с React, вы можете её использовать с Angular, с jQuery, с чем угодно. Сразу стоит сказать, что Router5 не является продолжением React-router@4, это две разные вещи, разрабатываются разными людьми, и они никак между собой не связаны.
Соответственно, при выборе библиотеки мы начали смотреть на то, насколько она популярна и, имеет ли смысл ее вообще использовать. Если сра��нивать по количеству звезд на github’е, то перевес в сторону react-router.

Но мы решили рискнуть. Посмотрели, как живет проект, занимается ли кто-то его разработкой, увидели, что люди привносят в него новое, и проект очень даже жив.

Из документации можно узнать, что у него есть куча интеграций с разными фреймворками и библиотеками. В частности, в документации расписана интеграция с react и redux, но если хорошо поискать, то можно найти там примеры с mobX.

Как строятся интерфейсы, и как строятся роутеры в Router5?
Здесь такая же история, как и в React-router: каждый route является контейнером для своих дочерних элементов.
Допустим, у вас есть сайт, который состоит из лендинга (это у нас будет home), и есть некая админка, которая состоит из двух разделов – список пользователей и список групп этих пользователей. Мы дальше можем это все сконфигурировать таким образом, что у нас раздел Home будет иметь вид лендинга. Дальше раздел админ будет иметь врапер для всех своих дочерних страниц и элементов, т.е. будет состоять там из футера и хэдера, например. Все вложенные страницы уже в админке будут оборачиваться в эти футер и хедер.

Для такого сайта конфиг Router5 будет следующим:
import createRouter from 'router5'; import browserPlugin from 'router5/plugins/browser'; const routes = [ { name : 'home', }, { name : 'admin', children : [ { name : 'roles', }, { name : 'users', }, ] }, ]; const options = { defaultRoute: 'home', // ... }; const router = createRouter(routes, options) .usePlugin(browserPlugin()); router.start();
В примере у нас простой вариант, но по факту можно гораздо больше узнать из документации и подобрать для себя более удобный формат.
Как я уже сказал ранее, на своих проектах в подразделении мы используем React, поэтому дальше я буду рассказывать и показывать примеры больше в сторону React’а. На примере кода ниже показано как поднять проект с использованием Router@5.
// app.js import ReactDOM from 'react-dom' import React from 'react' import App from './App' import { RouterProvider } from 'react-router5' import createRouter from './create-router' const router = createRouter() router.start(() => { ReactDOM.render(( <RouterProvider router={router}> <App /> </RouterProvider> ), document.getElementById('root')) })
// Main.js import React from 'react' import { routeNode } from 'react-router5' import { UserView, UserList, NotFound } from './components' function Users(props) { const { previousRoute, route } = props switch (route.name) { case 'users.list': return <UserList /> case 'users.view': return <UserView /> default: return <NotFound /> } } export default routeNode('users')(Users)
Дальше еще интересней. Вещь, которая нам понравилась и очень элегантно вписалась в проект, это то, как осуществляются переходы. Допустим, у нас есть сайт со структурой, описанной выше. И наш пользователь хочет перейти с лэндинга в раздел «Список пользователей». Что будет делать в этом случае router5? В первую очередь он деактивирует раздел home, вызывая соответствующие события. Обработать события можно как в самом route, так и в middleware, в котором можно обрабатывать все эти переходы. Дальше генерируются события активации роутов admin и admin.users.

У нас в приложении все middleware собраны в одном месте. Это middleware, которые отвечают за подгрузку компонентов (мы стараемся максимально «резать» приложение на куски и догружать те части, которые сейчас пользователю нужны), локализации, получения данных с сервера, проверка прав доступа и сбор аналитики переходов. В итоге разработчики вообще не думают о том, как им получать данные, проверять права и что при этом выводить на экран. Они делают очередной раздел, а все проблемы решает Router5.
Еще одна проблема, которую мы хотели решить раз и на всегда — это загрузка приложения, разбитого на разные куски. На мой взгляд загрузка приложения с React-router схожа с приключениями Ёжика в тумане. Догружая на каждом из этапов какие-то куски и данные, ваше приложение не застраховано от ошибок получения данных с сервера. И в этом случае ваши дизайнеры должны придумать состояния экранов для каждой нештатной ситуации. Кажется в этом месте вероятность ошибки разработчика высока (он может просто забыть про это), и эти риски нужно свести к минимуму.
В Router5 это решено тем, что каждый переход осуществляется транзакционно, и смена визуальной части не будет происходить до тех пор, пока не будут отработаны все middleware, и роутер не убедится в том, что наше приложение сможет нарисовать то, что мы от него хотим. У такого подхода есть свои минусы и плюсы, но в нашем случае плюсов было больше. Тут же решается вопрос с индикатором загрузки. В нашем приложении используется один индикатор загрузки, которым управляет роутер.
К сожалению, работа с middleware месяца 3 или 4 назад была не полностью раскрыта в документации. Но покопавшись в issues, мы нашли отличный пример, показывающий, как можно развернуть и сделать приложение с использованием React, Redux и Router5.
Каждый из URL нашего приложения хранит в себе какой-то набор данных, нужных нам для вывода данных (идентификаторы, дополнительные параметры фильтрации данных и т.п.). Сериализация и десериализация этих параметров из URL в router5 и обратно не выглядит сверхъестественно, но она есть.
export default { name: 'help', path: '/help*slugs', loadComponent: () => import('./index'), encodeParams: ({ slugs }) => slugs.join('/'), decodeParams: ({ slugs }) => slugs.split('/'),, defaultParams: { slugs: [], }, };
Для нашего проекта это было немаловаж��ым параметром выбора роутера, потому что наше приложение обладает множеством фильтров и поп-апов. И оно должно отображать в URL то состояние, что сейчас пользователь видит на экране. Если пользователь хочет показать что-то своему коллеге, то он просто шарит URL из адресной строки своего браузера.
Ниже показаны базовые примеры:
const router = router5([ { name: 'admin', path: '/admin' }, { name: 'admin.users', path: '~//users?param1' }, { name: 'admin.user', path: '^/user/:id' }, { name: 'help', path: '^/help/*splat' } ]); console.log(router.buildPath('admin')); // '/admin' console.log(router.buildPath('admin.users'); // '/users' console.log(router.buildPath('admin.users', { param1: true })); // '/users?param1=true' console.log(router.buildPath('admin.users', { id: 100 })); // '/user/100' console.log(router.buildPath('admin.user', { id: 100 })); // '/user/100' console.log(router.buildPath('help', { splat: [1, 2, 3] })); // '/help/1/2/3'
В React компоненте формирование ссылки происходит следующим образом:
import React from 'react' import { Link } from 'react-router5' function Menu(props) { return ( <nav> <Link routeName="home">Home</Link> <Link routeName="about">About</Link> <Link routeName="user" routeParams={{ id: 100 }}>User #100</Link> </nav> ) } export default Menu
При создании простых сайтов, состоящих из 3-4 страниц с минимальным количеством переходов, я не использую Router5, а использую React-router. Но в проектах с множеством страниц и связей, мой выбор будет в сторону Router5.
Так же вы можете посмотреть видео моего выступления на RamblerFront& #5 здесь:
P.S. В первой половине октября 2018 года мы планируем провести очередной RamblerFront& #6. Следите за анонсом здесь на habr.com.
