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

Эта статья содержит общее описание механизма маршрутизации в 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.

Пример. Опция 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 обеспечивает работу с масками.

Важная особенность в использовании метода Controller.navigate. На вход ожидается объект с полями state и href. Если не передавать поле href, то оно будет автоматически вычислено из файла маршрутов router.json. href это то, что будет показано в адресной строке браузера.

Программный переход в браузере

Если есть необходимость осуществить программный переход в прикладном контроле, то необходимо убедиться, что был зарегистрирован хотя бы один "маршрут". При использовании контрола 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.

маска в href

В параметре href, также, можно использовать маску, соблюдая все правила использования масок.

Запуск демо

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

npm install
npm run build
npm start

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