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

Эта статья содержит общее описание механизма маршрутизации в 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, в шаблоне которого находится Controls/Application, который построит 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.

Пример. Опция state хранит маску с интересующим нас параметром. Опция href является контентной опцией внутри шаблона Router/router:Reference. В результате, к текущей ссылке добавится destination/ и название страны, которое определит Router/router:Reference.

<Router.router:Reference state="destination/:country" country="{{ country }}">
    <a href="{{ content.href }}">Go to {{ country }}</a>
</Router.router:Reference>

Контрол 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 будет неопределенным.

<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

History

Синглтон Router/router:History позволяет определить состояние истории браузера, если это необходимо.

МетодОписание
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 пропадет.

<Router.router:Route mask="search/:query">...</Router.router:Route>

Использование событий для маршрутизации всплывающих окон

События можно использовать для выполнения пользовательских действий при изменении URL-адреса, например для открытия всплывающих окон.

Пример. Рассмотрим код, который приведён ниже.

<Router.router:Reference mask="alert/:popupInfo" popupInfo="{{ _productId }}">
    <a href="{{ content.href }}">Open popup</a>
</Router.router:Reference> 
<Router.router:Route mask="alert/:popupInfo" on:enter="showPopup()" on:leave="closePopup()"/>

При клике по крестику всплывающего окна из URL-адреса будет удалён параметр, который задан в маске. Далее произойдёт событие leave, которое закроет всплывающее окно. При этом все состояния контрола будут сохранены. Таким образом, можно легко управлять открытием и закрытием всплывающих окон.

При клике на контрол Router/router:Reference в URL-адрес будет добавлен параметр, который указан в маске. В примере этим параметром является идентификатор всплывающего окна. Произойдёт событие enter, по которому showPopup() покажет всплывающее окно с соответствующим идентификатором.

Программный переход

Для данного механизма, также, существует возможность не использовать Router/router:Reference, а переходить по ссылкам программно.

Чтобы осуществить программный переход, запросим его вручную:

var MaskResolver = require('Router/router').MaskResolver;
 
// Вычисляем новый адрес, в котором будет tab/:tab
var newUrl = MaskResolver.calculateHref('tab/:tab', { tab: tabName });
 
var Controller = require('Router/router').Controller;
 
// Переходим на новый адрес
Controller.navigate({ state: newUrl });

Библиотека MaskResolver обеспечивает работу с масками.

Программный переход на клиенте

Если есть необходимость осуществить программный переход в прикладном контроле, то необходимо убедиться, что был зарегистрирован хотя бы один "маршрут". При использовании контрола Router/router:Route это происходит автоматически в хуке жизненного цикла _beforeMount(). Поэтому вызовы методов Controller.navigate/Controller.replaceState необходимо делать после _beforeMount(), например в хуке жизненного цикла _afterMount(). Это связано с тем, что при вызове _beforeMount() прикладного контрола еще не будет вызван _beforeMount() контрола Router/router:Route, т.е. не будет зарегистрирован "маршрут".

Пример:

// MyView.ts
import * as Control from 'Core/Control';
import { Controller } from 'Router/router';
export default class extends Control {
    _beforeMount(cfg: any): void {
        // ...
    }
    _afterMount(): void {
        // вызов Controller.navigate/Controller.replaceState
    }
    // ...
}
<!-- MyView.wml -->
<Router.router:Route mask="/mask/:maskId">
    <ws:content>
        <!-- ... -->
    </ws:content>
</Router.router:Route>

Программный переход на сервисе представления

При необходимости выполнить переход на новый URL на сервисе представления нужно пользоваться методом:

process.domain.res.redirect('/new/path');

Это позволит выполнить переход на новый URL без лишней загрузки страницы.

Пример. Задача определения города посетителя сайта. По умолчанию нам не известен город. Можно на сервисе представления вычислить город. Выполнить переход на новый URL типа /city/Yaroslavl.

// MyView.ts
import * as Control from 'Core/Control';
import { constants } from 'Env/Env';
export default class extends Control {
    _beforeMount(cfg: any): void {
        if (constants.isServerSide) {
            data = MaskResolver.calculateUrlParams('city/:city');
            if (!data?.city && process?.domain?.res) {
                process.domain.res.redirect('/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.

<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.

Запуск демо

Чтобы запустить демонстрационный сервер, скачайте исходный код и выполните следующие команды:

npm install
npm run build
node app

Перейдите по адресу http://localhost:777/RouterDemo/ в своем браузере, чтобы увидеть демонстрационную страницу.