Клиентская маршрутизация в SPA

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

Маршрутизация в SPA решает несколько задач:

  • синхронизация состояния приложения с URL;
  • поддержка истории браузера (назад/вперёд);
  • возможность прямого перехода по ссылке;
  • разделение интерфейса на логические представления.

Подходы к маршрутизации в Knockout.js

Существует два основных подхода:

1. Использование сторонних роутеров

  • Sammy.js
  • Director
  • Page.js
  • Crossroads.js

2. Самописная маршрутизация

  • на основе location.hash
  • на основе History API (pushState)

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

Хеш-маршрутизация и Knockout.js

Хеш-маршрутизация использует часть URL после символа #. Изменение хеша не вызывает перезагрузку страницы, но генерирует событие hashchange.

Пример URL:

http://example.com/#/users/42

Браузер предоставляет доступ к хешу через location.hash.

Базовая архитектура маршрутизации

Типичная архитектура включает:

  • Router — отвечает за разбор URL и выбор маршрута
  • ViewModel-контейнер — хранит текущее представление
  • Набор ViewModel’ей экранов
  • Шаблоны (templates)

Общий принцип: маршрут определяет, какая ViewModel активна, а Knockout автоматически обновляет DOM.

Хранилище текущего состояния

Текущее представление удобно хранить в observable:

function AppViewModel() {
    this.currentView = ko.observable();
    this.currentParams = ko.observable({});
}

currentView определяет активный экран, currentParams — параметры маршрута.

Реализация простого роутера

Простейший роутер можно реализовать как объект с таблицей маршрутов:

function Router(routes) {
    this.routes = routes;
}

Router.prototype.resolve = function (hash) {
    var route = hash.replace(/^#\/?/, '');
    var parts = route.split('/');
    var key = parts[0] || 'home';

    if (this.routes[key]) {
        return {
            view: this.routes[key].view,
            params: this.routes[key].params
                ? this.routes[key].params(parts)
                : {}
        };
    }
    return null;
};

Таблица маршрутов:

var routes = {
    home: {
        view: 'home-view'
    },
    users: {
        view: 'users-view'
    },
    user: {
        view: 'user-view',
        params: function (parts) {
            return { id: parts[1] };
        }
    }
};

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

var appVm = new AppViewModel();
var router = new Router(routes);

function onRouteChange() {
    var result = router.resolve(location.hash);
    if (result) {
        appVm.currentView(result.view);
        appVm.currentParams(result.params);
    }
}

window.addEventListener('hashchange', onRouteChange);
onRouteChange();

Изменение URL автоматически приводит к смене состояния ViewModel.

Отображение представлений через компонентную систему

Knockout поддерживает компоненты, что удобно для маршрутизации.

Регистрация компонентов:

ko.components.register('home-view', {
    viewModel: function () {},
    template: '<h1>Главная</h1>'
});

ko.components.register('user-view', {
    viewModel: function (params) {
        this.userId = params.id;
    },
    template: '<div>Пользователь: <span data-bind="text: userId"></span></div>'
});

Контейнер в HTML:

<div data-bind="component: {
    name: currentView,
    params: currentParams
}"></div>

Knockout автоматически пересоздаёт компонент при изменении currentView.

Навигация внутри приложения

Навигация осуществляется обычными ссылками с хешами:

<a href="#/home">Главная</a>
<a href="#/users">Пользователи</a>
<a href="#/user/42">Профиль</a>

Изменение хеша запускает механизм маршрутизации без дополнительных обработчиков.

Параметры маршрута и реактивность

Параметры маршрута передаются в компонент как обычные данные. При необходимости их можно сделать observable внутри ViewModel компонента:

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

Это позволяет реагировать на смену параметров без пересоздания компонента, если логика маршрутизатора это поддерживает.

Асинхронная загрузка данных при смене маршрута

Частый сценарий — загрузка данных при активации экрана:

function UsersViewModel() {
    var self = this;
    this.users = ko.observableArray([]);

    fetch('/api/users')
        .then(r => r.json())
        .then(data => self.users(data));
}

Маршрут отвечает только за выбор экрана, а загрузка данных инкапсулирована в ViewModel.

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

Для повышения устойчивости приложения используется маршрут по умолчанию:

if (!result) {
    location.hash = '#/home';
}

Либо отдельный экран ошибки:

routes.notfound = {
    view: 'notfound-view'
};

Использование History API

Более современный подход — history.pushState, позволяющий избавиться от #. Для Knockout принципиальных отличий нет: изменяется только механизм отслеживания URL.

window.onpopst ate = function () {
    resolveRoute(location.pathname);
};

history.pushState({}, '', '/users');

Недостаток — необходимость серверной настройки для возврата SPA по любому URL.

Интеграция с внешними роутерами

При использовании Sammy.js или Page.js маршрутизация становится декларативной:

page('/users/:id', function(ctx) {
    appVm.currentView('user-view');
    appVm.currentParams({ id: ctx.params.id });
});

Knockout в этом случае остаётся исключительно инструментом реактивного отображения.

Структурирование маршрутов в крупных приложениях

В масштабных SPA маршруты группируются по модулям:

  • auth/*
  • admin/*
  • catalog/*

Каждый модуль содержит собственные ViewModel и шаблоны. Центральный роутер выполняет только диспетчеризацию.

Тестируемость маршрутизации

Логика маршрутов легко тестируется отдельно от DOM:

  • парсинг URL
  • сопоставление маршрута
  • формирование параметров

Это достигается выносом маршрутизатора в чистый JavaScript без привязки к Knockout.

Типичные ошибки

  • хранение логики маршрутизации внутри компонентов
  • жёсткая привязка DOM-событий к навигации
  • отсутствие маршрута по умолчанию
  • дублирование загрузки данных при каждом переходе

Грамотно реализованная клиентская маршрутизация превращает Knockout.js из простого MVVM-инструмента в полноценную платформу для построения SPA с управляемым состоянием и предсказуемым поведением интерфейса.