Pull to refresh

Маршрутизация в большом приложении на React

Reading time7 min
Views40K


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

Tags:
Hubs:
+13
Comments16

Articles

Information

Website
rambler-co.ru
Registered
Employees
1,001–5,000 employees
Location
Россия