Универсальный роутинг для React приложений

от автора

Если попытаться в двух словах описать, в чем заключается функция роутинга на фронтэнде веб-приложений, то можно придти к выводу, что каждый популярный фреймоворк совершенно по-разному представляет это себе. Даже, сравнивая версии одного и того же фреймоворка, можно придти к выводу, что функции и API роутинга наиболее подвержены изменениям (часто без обратной совместимости). Например 4-я версия роутинга в React была переработана настолько радикально, что некоторые популярные проекты на githab.com так и не перешли на эту версию.

За всем этим просматривается общая тенденция, которая, по моему мнению, заключается в том, что функционал роутинга в многих популярных фронтэнд фрейморках перегружен. В связи с этим, он становится жестко связанным с другими компонентами, которые могли быть выделены из роутинга (например с навигацией, историей, ссылками и т.п.). Поэтому, неверное, многим знакомо то чувство, когда использование роутинга становится неудобным, а его расширение просто невозможным. По сравнению с гибкими и расширяемыми компонентами, роутинг в популярных фронтэнд фрейморках выглядит на порядок менее удобным и совсем не расширяемым. Особенно это относится первым версиям (до 4-й) роутинга в React.

В этом сообщении я рассмотрю некоторые исторические моменты, которые привели к такому положению дел с роутингом, а также использование библиотеки universal-router, совместно с React.

А нужен ли роутинг?

Технически одностраничное веб-приложения может работать без роутинга. Например, как нет роутинга в десктопном приложении. Все работало бы почти хорошо, если бы одностраничное веб-приложение не оставалось все тем же веб-приложением для браузера. То есть, пользователь может в любую минуту обновить страницу нажатием на клавишу F5 или кликом по пиктограмме «Reload» браузера. Или же пользователь может в любой момент прокрутить историю вперед или назад кликом по пиктограмме «Стрелка влево» и «Стрелка вправо», или нажатием на клавишу «Backspace».

Поэтому, для одностраничного приложения смена компонентов и изменения внутреннего состояния приложения всегда должно сопровождаться изменением url.

Почему роутинг такой?

На функцию роутинга в популярных фреймворках для фронтэнда веб-приложений, по моему мнению, влияет его историческая связь роутингом в классических веб-приложениях (с серверным рендерингом).

Первоначально, url был адресом в сети статического веб-документа, и все было очень просто. Далее началась адаптация архитектуры MVC применительно к вебу: Model 1 и Model 2. Последняя из них имеет в своем составе фронт-контролеер, который впоследствии был еще разделен на две части: роутинг (который выбирает нужный контроллер) и собственно контроллер который работает с моделью и рендерит вью. Как видим, в классическом веб-приложении роутинг определяет действие (контроллер) и, опосредованно (через контроллер), определяет вью которое должно быть отренедрено на сервере.

То есть, десктопная архитектура была в свое время адаптирована для работы с классическим веб-приложением на сервере, а потом вернулась на фронт веб-приложения в виде роутинга, который был утяжелен функциями, которые были необходимы на стороне сервера.

Что предлагает библиотека universal-router?

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

Отбросив все напластования времен, universal-router предлагает всего лишь одну четко обозначенную функцию. На основании строки (еще раз подчеркиваю строки а не объекта history, location и т.п.) вызвать асинхронную функцию, которой передать виде фактических параметров разобранную строку url. Вот и все. Как это могло бы выглядеть в React:

import React from 'react'; import UniversalRouter from 'universal-router'; import App from './App'; import Link from './Link';  const routes =     {         path: '/',         async action({next}) {             const children = await next();             return (                 <App>                     {children}                 </App>             );         },         children: [             {                 path: '',                 async action() {                     return (                         <div>Root route go to <Link href='/test'>Test</Link></div>                     );                 },             },             {                 path: '/test',                 async action({next}) {                     const children = await next();                     return (                         <App>                             {children}                         </App>                     );                 },                 children: [                     {                         path: '',                         async action() {                             return (                                 <div>Test route return to <Link href='/'>Root</Link></div>                             );                         },                     },                 ]             },         ],     };  export const basename = '';  const router = new UniversalRouter(routes, {     baseUrl: basename });  export default router; 

Вложеные роуты также поддерживаются. Они определяются в поле children, а получить их можно вызовом асинхронной функции next().

И как же это работает с React?

Определим метод navigate() для history:

import { createBrowserHistory } from 'history' import parse from 'url-parse' import deepEqual from 'deep-equal' const isNode = new Function('try {return this===global;}catch(e){return false;}') //eslint-disable-line let history  if (!isNode()) {   history = createBrowserHistory()   history.navigate = function (path, state) {     const parsedPath = parse(path)     const location = history.location     if (parsedPath.pathname === location.pathname &&       parsedPath.query === location.search &&       parsedPath.hash === location.hash &&       deepEqual(state, location.state)) {       return     }     const args = Array.from(arguments)     args.splice(0, 2)     return history.push(...[path, state, ...args])   } } else {   history = {}   history.navigate = function () {} }  export default history 

Также создадим компонент Link, который будет вызывать навигацию:

import React from 'react'; import {basename} from './router'; import history from './history';  const createOnClickAnchor = (callback) => {     return (e) => {         e.preventDefault();         history.navigate(e.currentTarget.getAttribute('href'));         callback(e);     }; };  export default ({href, onClick = () => {}, children, ...rest}) => (     <a         href={basename + href}         onClick={createOnClickAnchor(onClick)}         {...rest}     >         {children}     </a> ); 

Теперь все готово для рендеринга компонента:

import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import history from './history'; import router from './router';   const render = async (location) => {     const element = await router.resolve(location);     ReactDOM.render(             element,         document.getElementById('root'),     ); };  render(history.location); history.listen(render);    // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister(); 

Код проекта github.com/apapacy/universal-router-tut

Полезные ссылки

1. medium.com/@ippei.tanaka/universal-router-history-react-97ec79464573

ссылка на оригинал статьи https://habr.com/ru/post/484514/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *