company_banner

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


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

    Rambler Group
    86,75
    Компания
    Поделиться публикацией

    Похожие публикации

    Комментарии 21

      0
      Я ничего не понял. Вы взяли react-router и не додумавшись написать обычный конфиг приступили к поиску другой библиотеки. Это действительно странно.
        +1

        Допустим у нас есть страница статистики с 10-ю параметрами и 15 ссылок на эту страницу, разбросанные по всему проекту. Можете помочь написать конфиг так, что новый человек в проекте мог поменять публичный адрес страницы статистики в одном месте?

          0
          подозреваю, имелось в виду что-то типа такого:

          <Link to={routes.netflix.path}>Netflix</Link>
          

          соответственно, если вам нужно поменять адрес страницы, то меняете его в том месте, где задаётся этот самый routes
            0
            Допустим. А куда деть 10 параметров?
              0
              Дополню — желательно это делать строковым enum в тайпскрипт и получить дополнительно проверку от компилятора на опечатки. Что касается 10 параметров — вам не надо пробрасывать в компоненту что либо от роутера, это неправильно. Правильно использовать в роутере метод render и все переменные с match пробрасывать в пропсы. Тогда ваши компоненты ничего не будут знать о роутере и это хорошо. А вот как систематизировать перенос 20-ти параметров — надо было просто написать HoC компоненту, ей скармливать вашу компоненты а результат скармливать render-у роутера. Собственно все.
                0
                Да, я понял. Речь идет о формировании ссылки а не ее парсинге. Тут как удобнее — в Link все равно надо знать о том что параметры существуют, можно в конфиге хранить шаблон, в линке его заполнять, можно линк завернуть в компоненту и передавать ей параметрами в пропсах, может есть еще другие пути — надо смотреть на правила формирования ссылок если они есть.
                  0
                  Мы на проекте пользуемся генерацией ссылок с помощью функций
                  <Link to={routes.netflix.path(/* 10 парметров */)}>Netflix</Link>
                0
                Ну это вопрос на вакансию джуна. Либо формировать с помощью шаблонной строки харкордно, либо написать свой кастомный Link. Если не поленится и открыть доки, то там в каждом примере показывают как сделать что-то кастомное. Тема настолько старая, что даже удивительно увидеть её на хабре, да ещё не от джуна, который хочет на фрилансим бесплатный месяц получить. Хотя может вы этого и добиваетесь?)
                  0
                  Я ничего не понял. Вы взяли react-router и не додумавшись написать обычный конфиг приступили к поиску другой библиотеки. Это действительно странно.

                  У конфига для реакт-роутера есть пачка минусов в добавок к минусам самого реакт-роутера (хотя казалось бы куда уж больше):

                  • роуты рендерятся (роуты рендерятся?? в карбюраторе конденсат!) только через Switch (renderRoutes просто превращает ваш «конфиг» в набор Route-компонентов внутри Switch)
                  • и так как реально реакт-роутер-конфиг — это тот же реакт-роутер, то в плане редиректов ничего нового не появилось, редиректы по-прежнему работают только через компонент Redirect. Вот прям в «конфиг» надо взять и запихать отрендеренный реакт-элемент.
                  • рендер вложенных роутов: вы получаете вложенные роуты и в компоненте сами их рендерите через всё тот же самый renderRoutes
                  • различное поведение withRouter и react-router-config при разборе параметров: github.com/ReactTraining/react-router/issues/5662#issuecomment-339885205


                  И самое главное: ваше представление больше не является функцией от состояния, а наоборот управляет состоянием. В статье How to decouple state and UI всё прекрасно расписал Michel Weststrate.

                  Всё это крайне расстраивает. Так получилось, что не использовал react-router и успешно его избегал, но на новом проекте он уже был. В конце концов меня очень расстраивает подход, когда инструменты, созданные для рендера (реакт-компоненты) используются для сайд-эффектов. Да, мы в итоге пришли и к react-router-config, и к кастомным Link, и к кастомным Redirect… но зачем весь этот ужас, если уже есть более удобные инструменты, как например описанный в этой статье router5.
              +1
              router5 — это Ваш выбор и наверное обоснованный. Я сам был в свое время немного в шоке от react-router версий с 1 по 3-ю. И вряд ли бы рискнул использовать их в развернутом проекте. С версии 4 react-router стал наконец более мощным и гибким. Основное преимущество react-router я нахожу в декларативном подходе к всприятию роутов, которое само по себе удобно при разработке компонентов и очень хорошо сочетается с декларативным подходом при работе со стором без redux (см. habr.com/post/358942) средствами graphql/apollo. Особенно это удобно при разработке изоморфных приложений. (Кстати в rooter5 был пример с универсальным приложением но сейчас его нет — наверное не все там у них гладко получается)

              Конечно немного не хватает именованых роутов. Но в конце концов их можно добавить через простые объекты.
                0
                А как вы видите организацию именованых роутов через объекты? У нас есть проекты с apollo и react-router@4, и я думаю ребятам было бы интересно ваше виденье.
                  0
                  Я имел в виду самый простой вариант:

                  import r from './myRoutes'
                  <Layout>
                        <Switch>
                          <Route exact path='/' component={ AllUsers } />
                          <Route exact path=`/${r.posts}` component={ TopPosts } />
                          <Route exact path='/${r.posts}/:postId' component={ Post } />
                          <Route exact path='/${r.user}/:userId' component={ NewPost } />
                        </Switch>
                  </Layout>
                  

                    +1
                    Мы думали сделать фабрики для генерации path для Route и to для Link. Но это как раз тот момент, когда начинаешь задумываться, что ты делаешь что то не то и должно быть решение проще. В конце конов react-router — это очень популярная библиотека, которой пользуются миллионы людей по всему миру. Как они живут с этим?
                      0

                      Тот случай, когда миллионы леммингов ошибаются. А я пока только мечтаю сбросить груз react-router (так получилось, что он уже был в проекте).

                0

                Спасибо за описание router5. Из первых строк и названия ожидал какое-то сравнение роутеров. Вы сравнивали router5 с universal-router или с director?

                  0

                  Director изменялся последний раз в феврале 2015.
                  Universal-router скорее всего не внимательно изучил, поэтому пропустил. Пересмотрев описание, попробую написать тестовое приложение с universal-router, если будет время.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое