Параметры маршрутов и их обработка

Маршрутизация в приложениях на Knockout.js не входит в состав самого фреймворка, однако на практике почти всегда используется совместно с внешними решениями или собственной реализацией. Ключевую роль при этом играют параметры маршрутов, позволяющие передавать состояние и контекст между экранами без перезагрузки страницы.

Knockout.js фокусируется на декларативном связывании данных и шаблонов. Маршруты реализуются поверх него — через:

  • изменение location.hash;
  • HTML5 History API (pushState, popstate);
  • сторонние библиотеки (Sammy.js, Director, Page.js, Durandal Router).

Во всех вариантах маршрут представляет собой строку, содержащую шаблон пути и параметры, извлекаемые из этого пути.

Пример маршрута в hash-стиле:

#/users/42/profile

Здесь 42 — параметр маршрута, определяющий контекст отображения.

Типы параметров маршрута

Позиционные параметры

Наиболее распространённый вариант. Параметры определяются положением в URL.

Пример шаблона:

#/users/:id

Фактический URL:

#/users/42

В результате сопоставления параметр id получает значение "42".

Именованные параметры

Используются в query-строке:

#/users/profile?id=42&tab=settings

Параметры:

  • id = 42
  • tab = settings

Такой формат удобен для необязательных или дополнительных параметров состояния.

Комбинированные маршруты

На практике часто сочетаются оба подхода:

#/users/42/profile?tab=settings

Позиционные параметры определяют сущность, query-параметры — режим отображения.

Извлечение параметров маршрута

Работа с hash напрямую

Минимальная реализация маршрутизации может основываться на обработке события hashchange.

window.addEventListener("hashchange", onRouteChanged);

function onRouteChanged() {
    const hash = location.hash.slice(1); // users/42
    const parts = hash.split("/");
    const route = parts[0];
    const id = parts[1];
}

Такой подход прост, но требует ручной обработки всех вариантов маршрутов и параметров.

Использование регулярных выражений

Регулярные выражения позволяют формализовать маршруты.

const routePattern = /^users\/(\d+)$/;

function onRouteChanged() {
    const hash = location.hash.slice(1);
    const match = hash.match(routePattern);

    if (match) {
        const userId = match[1];
    }
}

Этот способ повышает контроль над форматом параметров и упрощает валидацию.

Сторонние маршрутизаторы

Пример с Sammy.js:

Sammy(function() {
    this.get('#/users/:id', function() {
        const userId = this.params.id;
    });
});

Библиотека берёт на себя:

  • парсинг параметров;
  • сопоставление маршрутов;
  • обработку истории навигации.

Связывание параметров маршрута с ViewModel

Параметры маршрута почти всегда отображаются в observables, чтобы автоматически обновлять интерфейс.

function UserViewModel() {
    this.userId = ko.observable(null);
}

const vm = new UserViewModel();
ko.applyBindings(vm);

При изменении маршрута:

vm.userId(userId);

Все элементы интерфейса, связанные с userId, обновляются без дополнительного кода.

Реакция ViewModel на изменение параметров

Изменение параметров маршрута может означать:

  • загрузку новых данных;
  • переключение шаблона;
  • изменение режима отображения.

Пример с подпиской:

vm.userId.subscribe(function(newId) {
    loadUser(newId);
});

Такой подход отделяет логику маршрутизации от бизнес-логики ViewModel.

Параметры маршрута и асинхронные данные

Частый сценарий — загрузка данных по параметру маршрута.

function loadUser(id) {
    fetch(`/api/users/${id}`)
        .then(r => r.json())
        .then(data => {
            vm.user(data);
        });
}

Важно учитывать:

  • возможные быстрые переключения маршрутов;
  • устаревшие асинхронные ответы;
  • состояние загрузки.

Для этого используются дополнительные observables:

vm.isLoading = ko.observable(false);

Обработка отсутствующих и некорректных параметров

Маршрут может быть вызван без обязательных параметров или с ошибочными значениями.

Примеры ситуаций:

  • #/users/
  • #/users/abc

Обработка выполняется на уровне маршрутизатора или ViewModel:

if (!id || isNaN(id)) {
    navigateToNotFound();
}

Это предотвращает переход приложения в неконсистентное состояние.

Синхронизация параметров маршрута и состояния приложения

Иногда изменение состояния внутри ViewModel должно отражаться в URL.

Пример:

vm.selectedTab.subscribe(function(tab) {
    location.hash = `#/users/${vm.userId()}?tab=${tab}`;
});

Так достигается:

  • воспроизводимость состояния по URL;
  • корректная работа кнопок браузера;
  • возможность закладок.

Декомпозиция маршрутов и параметров

В крупных приложениях параметры маршрута распределяются между несколькими ViewModel.

Пример:

  • userId — глобальный контекст;
  • tab — локальный контекст представления.

Часто используется иерархия:

  • корневая ViewModel обрабатывает базовые параметры;
  • дочерние ViewModel подписываются на нужные observables.
function RootViewModel() {
    this.userId = ko.observable();
}

function ProfileViewModel(root) {
    this.userId = root.userId;
}

Такой подход снижает связанность и упрощает повторное использование компонентов.

Параметры маршрута и шаблоны

Параметры маршрута часто используются для выбора шаблона:

vm.currentTemplate = ko.computed(function() {
    return vm.userId() ? "user-profile" : "user-list";
});

HTML:

<div data-bind="template: currentTemplate"></div>

Изменение маршрута автоматически приводит к смене представления.

Кэширование параметров и состояний

При возврате к ранее открытому маршруту может потребоваться восстановление состояния.

Используются:

  • локальные хранилища (localStorage);
  • in-memory кэш;
  • сопоставление параметров маршрута и данных.
const cache = {};

function loadUser(id) {
    if (cache[id]) {
        vm.user(cache[id]);
        return;
    }
}

Итоговая архитектурная роль параметров маршрута

Параметры маршрута в приложениях на Knockout.js выполняют роль связующего слоя между:

  • навигацией;
  • состоянием ViewModel;
  • асинхронными источниками данных;
  • визуальными шаблонами.

Грамотная обработка параметров маршрута позволяет сохранить декларативную природу Knockout.js, избегая жёсткой связки между URL, логикой и представлением, и обеспечивает масштабируемость даже при отсутствии встроенного роутера.