Маршрутизация
Эта статья содержит общее описание механизма маршрутизации в Wasaby Framework. Из статьи вы получите ответы на такие вопросы, как:
- какие контролы обеспечивают инфраструктуру маршрутизации;
- что такое маска подстроки в URL и как с ней работать;
- как программно переходить по ссылкам;
- как настроить красивые адреса (ЧПУ или человекопонятные URL);
- как настроить сопоставление маршрутов;
Настройка маршрутизации для конкретных приложений СБИС, например online.sbis.ru, может иметь свои особенности. Поэтому перед началом работы рекомендуется обратить внимание на связанные с темой статьи:
- Использование маршрутизации Wasaby
- SEO controls
- Создание веб-страниц в приложении online.sbis.ru
- Правила составления «хороших» URL в приложении online.sbis.ru
Термины и определения
Маршрутизация
Процесс определения маршрута внутри приложения в зависимости от запроса. В процессе маршрутизации разработчик определяет шаблон URL и связывает его со своим кодом. Если возникает необходимость изменения конкретного URL, то следует просто поменять шаблон, тогда код по-прежнему будет работать отлично, и не понадобится менять какую-либо логику.
Механизм маршрутизации решает задачи:
- построения на сервере (рендеринга);
- SPA (позволяет переходить на клиенте со страницы на страницу без запроса на сервер).
SPA (Single Page Application)
Это web-приложение, контролы которого загружаются единожды на одной странице, а контент подгружается по необходимости.
Маршрутизация в SPA-системах используется для загрузки определенных частей веб-приложения.
Корневой контрол
По URL-адресу возможно вычислить корневой контрол, который построит HTML-верстку.
В качестве примера рассмотрим:
https://online.sbis.ru/Tasks/FromMe
Чтобы вычислить корневой контрол, необходимо:
- Выбрать первое слово в пути, определяющее раздел, в котором находится корневой контрол;
- Через символ "/" добавить
Index.js
. - В итоге получим корневой контрол —
Tasks/Index.js
, в шаблоне которого находится SbisEnvUI/Bootstrap, который построит HTML-верстку страницы.
Параметр FromMe
в данном примере показывает, какая вкладка будет отображаться. Он используется в процессе построения верстки.
Переключение корневого контрола
При переключении с одной ссылки на другую может переключиться и весь раздел.
Пример. В веб-приложении СБИС переключаемся из вкладки "Задачи" на вкладку "Контакты". На клиенте посредством VDOM-библиотеки будет вычислен другой корневой контрол, который полностью заменит собой старый и отстроит от него новую верстку.
/Tasks/FromMe -> /Contacts/
В итоге Tasks/Index.js
поменяется на Contacts/Index.js
.
Ключевые контролы
Инфраструктуру маршрутизации обеспечивают контролы:
- Router/router:Reference — по параметрам вычисляет новую ссылку и, посредством опции, передает ее в собственный контент.
- Router/router:Route — согласно своим настройкам из URL-адреса получает параметры и передает их в собственный контент.
Контролы изоморфны, т.е. используются как на сервере, так и на клиенте.
Маска и синтаксис её задания
Для работы с параметрами контролов используется маска — это строка с параметрами, синтаксически выделяемыми двоеточием, единственным образом определяющая подстроку в URL-адресе.
Например, маска /doc/:guid для
online.sbis.ru/doc/c065
вычислит guid = c065
Более подробно про маску и способы её задания можно прочитать в отдельной статье.
Контрол Router/router:Reference
Router/router:Reference — по параметрам вычисляет новую ссылку и, посредством опции, передает ее в собственный контент. Для добавления параметров в URL-адрес необходимо использовать опцию state. Контрол предназначен для организации SPA переходов в рамках приложения.
Пример 1. Опция state
хранит маску с интересующим нас параметром. Опция href
является контентной опцией внутри шаблона Router/router:Reference
. В результате, к текущей ссылке добавится destination/ и название страны, которое определит Router/router:Reference
.
<!-- WML -->
<Router.router:Reference state="destination/:country" country="{{ country }}">
<a href="{{ content.href }}">Go to {{ country }}</a>
</Router.router:Reference>
Пример 2. Чтобы удалить параметр из URL-адреса, необходимо его добавить в маску, но не передавать для него значение.
<!-- WML -->
<Router.router:Reference state="destination/:country/day/:dayName" country="Italy">
<a href="{{ content.href }}">Go to Italy</a>
</Router.router:Reference>
Текущий адрес: "/book/destination/USA/day/Tue?price=mid" -> После: "/book/destination/Italy?price=mid"
Контрол Router/router:Route
Router/router:Route — согласно своим настройкам из URL-адреса получает параметры и передает их в собственный контент. Для вычисления параметров используется опция mask. Значение этого параметра извлекается из URL-адреса при его изменении и передается внутри Router/router:Route с тем же именем.
Пример 1. Для online.sbis.ru/task/123/task/456
нельзя задать маску task/:taskId
, которая из URL-адреса выберет 456
. Это связано с ограничениями, которые мы задали для маски. При наличии в URL двух одинаковых параметров всегда будет выбираться только первый.
Чтобы выбирать 456
, нужно в URL-адресе изменить имена параметров и сделать их уникальными. Например, вот так:
online.sbis.ru/task/123/task1/456
В этом случае можно выбрать 456
при помощи маски task1/:taskid
.
Пример 2. В примере ниже, если URL-адрес содержит подстроку destination/Russia
, пользователь увидит Selected destination: Russia
. Если URL не соответствует маске destination/
, то country
будет неопределенным.
<!-- WML -->
<Router.router:Route mask="destination/:country">
<p>Selected destination: {{ content.country }}</p>
</Router.router:Route>
Router/router:Route поддерживает два типа масок: стандартный параметр (path
) и параметр запроса (query
):
URL | Параметр, определенный маской |
---|---|
/paramName/valueOne | valueOne |
/paramName/value/Two | value |
/paramName/value?num=three | value |
/paramName/value#Four | value |
URL | Параметр, определенный маской |
---|---|
/page?paramName=valueOne | valueOne |
/page?paramName=value&two=true | value |
/page?paramName=value#three | value |
Доступ к методам API роутинга
Методы API роутинга доступны из контекста WasabyContext в поле Router. Для удобства данные из контекста WasabyContext прокидываются в опциии Wasaby-контролов.
// MyView.ts
import { Control } from 'UI/Base';
import { IRouter, IHistoryState } from 'Router/router';
export default class MyView extends Control<{ Router: IRouter }> {
_beforeMount(options: { Router: IRouter }): void {
// options.Router.maskResolver.calculateUrlParams();
}
_afterMount(): void {
// this._options.Router.replaceState();
}
}
Если методы API роутинга нужны извне контрола, то можно использовать метод Router/router#getRootRouter. Метод вернет "корневой" объект Router с методами API.
Необходимо понимать, что если этот код будет использоваться внутри контролов, у которых в контексте объект Router уже другой, то могут быть ошибки...
// MyHelper.ts
import { getRootRouter, IRouter } from 'Router/router';
export function helper() {
const Router: IRouter = getRootRouter ();
// Router.maskResolver.calculateUrlParams(...)
}
Если методы API роутинга нужны в методе предзагрузки данных getDataToRender, то в этот метод третьим аргументом приходит объект Router с методами API.
// MyModule/Index.ts
import { IRouter } from 'Router/router';
export function getDataToRender(url: string, params: any, Router: IRouter): Promise<unknown> | unknown {
// Router.maskResolver.calculateUrlParams(...)
return false;
}
History
Методы из Router/router:IHistory позволяют определить состояние истории браузера, если это необходимо. Эти методы доступны в _options любого контрола
// MyView.ts
import { Control } from 'UI/Base';
import { IRouter, IHistoryState } from 'Router/router';
export default class MyView extends Control<{ Router: IRouter }> {
_beforeMount(options: { Router: IRouter }): void {
const prevState: IHistoryState = options.Router.history.getPrevState();
}
}
Метод | Описание |
---|---|
getPrevState() | Определить предыдущее состояние. |
getNextState() | Определить следующее состояние. |
getCurrentState() | Определить текущее состояние. |
События маршрутизации
У Router/router:Route есть события enter и leave. Они позволяют значительно упростить управление открытием и закрытием панелей. Событие enter происходит после перехода при котором URL-адрес начинает соответствовать маске этого маршрута, а leave — срабатывает после перехода, при котором URL-адрес перестает соответствовать указанной маске. При переходах в рамках одной маски события не вызываются.
Пример. Рассмотрим код, который приведён ниже. При переходе с URL-адреса /home
на /page/search/My-query
произойдёт событие enter
, т.к. в URL появится параметр My-query
. При переходе с /page/search/My-query
на about
, произойдёт событие leave
, т.к. в URL параметр My-query
пропадет.
<!-- WML -->
<Router.router:Route mask="search/:query">...</Router.router:Route>
Использование событий для маршрутизации всплывающих окон
События можно использовать для выполнения пользовательских действий при изменении URL-адреса, например для открытия всплывающих окон.
Пример. Рассмотрим код, который приведён ниже.
<!-- WML -->
<Router.router:Reference mask="alert/:popupInfo" popupInfo="{{ _productId }}">
<a href="{{ content.href }}">Open popup</a>
</Router.router:Reference>
<!-- WML -->
<Router.router:Route mask="alert/:popupInfo" on:enter="showPopup()" on:leave="closePopup()"/>
При клике по крестику всплывающего окна из URL-адреса будет удалён параметр, который задан в маске. Далее произойдёт событие leave
, которое закроет всплывающее окно. При этом все состояния контрола будут сохранены. Таким образом, можно легко управлять открытием и закрытием всплывающих окон.
При клике на контрол Router/router:Reference
в URL-адрес будет добавлен параметр, который указан в маске. В примере этим параметром является идентификатор всплывающего окна. Произойдёт событие enter
, по которому showPopup() покажет всплывающее окно с соответствующим идентификатором.
Программный переход
Для данного механизма, также, существует возможность не использовать Router/router:Reference
, а переходить по ссылкам программно.
Следующий код демонстрирует, как можно выполнить программный переход:
// MyView.ts
import { Control } from 'UI/Base';
import { IRouter } from 'Router/router';
export default class MyView extends Control<{ Router: IRouter }> {
_beforeMount(options: { Router: IRouter }): void {
// ...
}
_afterMount(): void {
// Вычисляем новый адрес, в котором будет tab/:tab
var newUrl = this._options.Router.maskResolver.calculateHref('tab/:tab', { tab: tabName });
// Переходим на новый адрес
this._options.Router.navigate({ state: newUrl });
}
}
Класс maskResolver обеспечивает работу с масками.
Важная особенность в использовании метода IRouter.navigate. На вход ожидается объект с полями state и href. Если не передавать поле href, то оно будет автоматически вычислено из файла маршрутов router.json. href это то, что будет показано в адресной строке браузера.
Программный переход в браузере
Если есть необходимость осуществить программный переход в прикладном контроле, то необходимо убедиться, что был зарегистрирован хотя бы один "маршрут". При использовании контрола Router/router:Route
это происходит автоматически в хуке жизненного цикла _beforeMount(). Поэтому вызовы методов IRouter.navigate/IRouter.replaceState необходимо делать после _beforeMount()
, например в хуке жизненного цикла _afterMount(). Это связано с тем, что при вызове _beforeMount()
прикладного контрола еще не будет вызван _beforeMount()
контрола Router/router:Route
, т.е. не будет зарегистрирован "маршрут".
Пример:
// MyView.ts
import { Control } from 'UI/Base';
import { IRouter } from 'Router/router';
export default class MyView extends Control<{ Router: IRouter }> {
_beforeMount(options: { Router: IRouter }): void {
// ...
}
_afterMount(): void {
// вызов this._options.Router.navigate или this._options.Router.replaceState
}
}
<!-- MyView.wml -->
<Router.router:Route mask="/mask/:maskId">
<ws:content>
<!-- ... -->
</ws:content>
</Router.router:Route>
Программный переход на сервисе представления
При необходимости выполнить переход на новый URL-адрес на сервисе представления нужно пользоваться методом:
import { location } from 'Application/Env';
location.replace('/new/path');
Это позволит выполнить переход на новый URL-адрес без лишней загрузки страницы.
Пример. Задача определения города посетителя сайта. По умолчанию нам не известен город. Можно на сервисе представления вычислить город. Выполнить переход на новый URL-адрес типа /city/Yaroslavl
.
// MyView.ts
import { Control } from 'UI/Base';
import { location } from 'Application/Env';
import { IRouter } from 'Router/router';
export default class MyView extends Control<{ Router: IRouter }> {
_beforeMount(options: { Router: IRouter }): void {
if (typeof window === 'undefined') {
data = options.Router.maskResolver.calculateUrlParams('city/:city');
if (!data?.city) {
location.replace('/city/Yaroslavl');
return;
}
}
}
// ...
}
<!-- MyView.wml -->
<Router.router:Route mask="/city/:city">
<ws:content>
Ваш город: {{ content.city }}
</ws:content>
</Router.router:Route>
Настройка сопоставления маршрутов
Механизм сопоставления маршрутов позволяет изменить URL-адрес с помощью json-файла.
Например, есть ссылка online.sbis.ru/doc/c065922e, в которой требуется произвести замену {"doc":"EDO3/Route/Dialog"}
. Тогда получим путь online.sbis.ru/EDO3/Route/Dialog/c065922e.
Т.к. маска работает по замененному пути, она должна соответствовать формату Dialog/:guid. Корневой контрол будет вычислен, также, по замененному пути — EDO3/Index.js.
Подобные замены следует хранить в файле router.json:
{
"/" : "OnlineSbisRu",
"/Contractors/" : "Contractor",
"/Tasks" : "OnlineSbisRu/Tasks",
"doc" : "EDO3/Route/Dialog",
"fastvdisk": "DOCVIEW3/fastvdisk"
}
Создать этот файл можно в любом каталоге интерфейсного модуля, поскольку билдер при сборке проекта соберет все router.json в один файл и положит его в корень проекта. Этот файл в последствии будет использоваться для механизма маршрутизации.
Добавление адресов в файл router.json
должно быть согласовано с ответственным за приложение. Для online.sbis.ru такое согласование должно быть с Новиковым Д.В., и за добавление адреса без согласования будут выписываться ошибки.
Более подробно с настройкой перенаправлений в router.json
можно ознакомиться в отдельной статье.
Настройка "красивых" ссылок в адресной строке
Для того чтобы привести ссылку в адресной строке к красивому виду, используется параметр href.
<!-- WML -->
<Router.router:Reference state="page/:pageType" pageType="register" href="/signup">
<a href="{{ content.href }}">Sign up</a>
</Router.router:Reference>
В данном примере мы перейдем по ссылке page/register
, маски отработают, и вычислится корневой контрол, также, по ссылке /page/register
. Однако, благодаря параметру href, в адресной строке отобразится /signup
.
маска в href
В параметре href, также, можно использовать маску, соблюдая все правила использования масок.
Запуск демо
Чтобы запустить демонстрационный сервер, скачайте исходный код и выполните следующие команды:
npm install
npm run build
npm start
Перейдите по адресу http://localhost:777/RouterDemo в своем браузере, чтобы увидеть демонстрационную страницу.