Маршрутизация

Эта статья содержит общее описание механизма маршрутизации в Wasaby Framework. Из статьи вы получите ответы на такие вопросы, как:

Настройка маршрутизации для конкретных приложений СБИС, например online.sbis.ru, может иметь свои особенности. Поэтому перед началом работы рекомендуется обратить внимание на связанные с темой статьи:

Термины и определения

Маршрутизация

Процесс определения маршрута внутри приложения в зависимости от запроса. В процессе маршрутизации разработчик определяет шаблон URL и связывает его со своим кодом. Если возникает необходимость изменения конкретного URL, то следует просто поменять шаблон, тогда код по-прежнему будет работать отлично, и не понадобится менять какую-либо логику.

Механизм маршрутизации решает задачи:

  • построения на сервере (рендеринга);
  • SPA (позволяет переходить на клиенте со страницы на страницу без запроса на сервер).

SPA (Single Page Application)

Это web-приложение, контролы которого загружаются единожды на одной странице, а контент подгружается по необходимости.

Маршрутизация в SPA-системах используется для загрузки определенных частей веб-приложения.

Корневой контрол

По URL-адресу возможно вычислить корневой контрол, который построит HTML-верстку.

В качестве примера рассмотрим:

https://online.sbis.ru/Tasks/FromMe

Чтобы вычислить корневой контрол, необходимо:

  1. Выбрать первое слово в пути, определяющее раздел, в котором находится корневой контрол;
  2. Через символ "/" добавить Index.js.
  3. В итоге получим корневой контрол — 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):

Маска параметров в path — "paramName/:paramValue".
URLПараметр, определенный маской
/paramName/valueOnevalueOne
/paramName/value/Twovalue
/paramName/value?num=threevalue
/paramName/value#Fourvalue
Маска параметров в query — "paramName=:paramValue".
URLПараметр, определенный маской
/page?paramName=valueOnevalueOne
/page?paramName=value&two=truevalue
/page?paramName=value#threevalue

Доступ к методам 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 в своем браузере, чтобы увидеть демонстрационную страницу.