Если попытаться в двух словах описать, в чем заключается функция роутинга на фронтэнде веб-приложений, то можно придти к выводу, что каждый популярный фреймоворк совершенно по-разному представляет это себе. Даже, сравнивая версии одного и того же фреймоворка, можно придти к выводу, что функции и 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
Автор: apapacy