Привет, меня зовут Борис Шабанов, я — руководитель 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.