Создаем почтовый клиент на Ext JS с нуля. Часть 7: Deep Linking и Router

За шесть частей мы построили полноценный почтовый клиент Ext Mail: от фундамента и моделей данных до мобильной версии и интеграции с REST API. Однако до сих пор наше приложение существовало под одним URL, который никогда не менялся. Это означало, что:

  • при обновлении страницы приложение сбрасывалось в исходное состояние;

  • нельзя было поделиться ссылкой на конкретное письмо или метку;

  • кнопка «Назад» в браузере работала не так, как ожидает пользователь.

В заключительной, седьмой части мы исправим это, добавив Deep Linking с помощью встроенных Router-классов Ext JS. Это сделает наше приложение по-настоящему веб-ориентированным: URL будут меняться, состояние сохраняться, а ссылки — делиться.

Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.

🔗 Что такое Deep Linking и зачем это нужно

Deep Linking — это возможность переходить напрямую к определённому разделу приложения по URL. В традиционных серверных приложениях каждая страница имела свой адрес: myapp.com/users, myapp.com/orders/123. В современных Single-Page Applications (SPA) пользователь никогда не покидает корневую страницу, и все переходы происходят внутри JavaScript. Это ломает привычную веб-навигацию.

Преимущества Deep Linking:

  • Пользователь может обновить страницу и остаться на том же месте.

  • Ссылкой на конкретное письмо или метку можно поделиться.

  • Кнопка «Назад» работает ожидаемым образом.

  • Улучшается SEO и аналитика.

Ext JS предоставляет набор классов в пространстве имён Ext.route, которые позволяют реализовать маршрутизацию на основе хэша URL. Хэш не вызывает перезагрузку страницы, но его изменения отслеживаются приложением.

Пример URL с хэшем: www.myapp.com#user/view/123

🧩 Ключевые возможности Ext JS Router

Router в Ext JS поддерживает:

  • Параметры в маршрутах — например, user/:action/:id.

  • Необязательные параметры.

  • Валидацию параметров — можно проверять, что ID является числом.

  • Несколько активных маршрутов одновременно — полезно, когда на экране открыто несколько независимых областей.

  • before-хуки — функции, которые выполняются перед применением маршрута. Их можно использовать для проверки авторизации или загрузки данных.

  • Именованные маршруты — позволяют обновлять один маршрут, не затрагивая другие.

🗺️ Добавляем три маршрута в Ext Mail

Мы добавим три маршрута:

  1. label/:label — выбор метки (Inbox, Starred, Drafts и т.д.).

  2. view/:messageId — просмотр конкретного письма.

  3. draft/:messageId — открытие черновика в форме редактирования.

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

🏷️ Добавляем поле slug для меток

Чтобы использовать метки в URL, нужно преобразовать их названия в URL-безопасный формат — slug. Добавим вычисляемое поле в модель Label:

javascript
{
    name: 'slug',
    calculate: function(data) {
        return (data.name || '').toLowerCase()
            .replace(/ /g, '-')
            .replace(/[^\w-]+/g, '');
    }
}

Теперь метка «All Mail» превратится в all-mail, «Drafts» — в drafts и т.д.

🧭 Маршрут для выбора метки

Определяем маршруты в MainControllerBase:

javascript
routes: {
    'label/:label': {
        name: 'label',
        before: 'onBeforeViewLabel',
        action: 'onViewLabel'
    }
}

before-хук проверяет, загружено ли хранилище меток. Если нет — ждёт события load:

javascript
onBeforeViewLabel: function(label, action) {
    var labelsStore = this.getViewModel().getStore('labels');
    if (labelsStore.loadCount > 0) {
        action.resume();
    } else {
        labelsStore.on('load', function() {
            action.resume();
        }, this, { single: true });
    }
}

action-метод находит метку по slug и устанавливает её как выбранную:

javascript
onViewLabel: function(label) {
    var labelsStore = this.getViewModel().getStore('labels');
    var labelRecord = labelsStore.findRecord('slug', label);
    if (!labelRecord) {
        labelRecord = labelsStore.first();
    }
    this.getViewModel().set('selectedMessage', null);
    this.getViewModel().set('selectedLabel', labelRecord);
}

Чтобы URL обновлялся при клике по дереву меток, добавляем обработчик selectionchange:

javascript
onLabelSelectionChange: function(labelTree, selectedLabelRecords) {
    var selectedLabelRecord = selectedLabelRecords[0];
    var slug = '';
    if (selectedLabelRecord) {
        slug = selectedLabelRecord.get('slug');
    }
    this.redirectTo({
        label: Ext.String.format('label/{0}', slug),
        message: null
    });
}

📩 Маршрут для просмотра письма

Добавляем маршрут view/:messageId:

javascript
'view/:messageId': {
    name: 'message',
    before: 'onBeforeViewMessage',
    action: 'onViewMessage'
}

before-хук аналогичен предыдущему — ждёт загрузки хранилища messages.

javascript
onBeforeViewMessage: function(messageId, action) {
    var store = this.getViewModel().getStore('messages');
    if (store.loadCount > 0) {
        action.resume();
    } else {
        store.on('load', function() {
            action.resume();
        }, this, { single: true });
    }
}

action-метод находит письмо по ID и устанавливает его как выбранное:

javascript
onViewMessage: function(messageId) {
    var store = this.getViewModel().getStore('messages');
    var messageRecord = store.getById(messageId);
    this.getViewModel().set('selectedMessage', messageRecord);
    if (!messageRecord) {
        this.redirectTo({ message: null });
    }
}

Теперь нужно изменить обработчик клика по письму в handleMessageClick, чтобы он не менял состояние напрямую, а вызывал redirectTo:

javascript
handleMessageClick: function(messageRecord) {
    if (messageRecord.get('draft')) {
        this.redirectTo({
            draft: Ext.String.format('draft/{0}', messageRecord.getId())
        });
    } else {
        this.redirectTo({
            message: Ext.String.format('view/{0}', messageRecord.getId())
        });
    }
}

✏️ Маршрут для черновика

Третий маршрут — draft/:messageId:

javascript
'draft/:messageId': {
    name: 'draft',
    action: 'onDraftMessage'
}
javascript
onDraftMessage: function(messageId) {
    var messageRecord = this.getViewModel().getStore('messages').getById(messageId);
    if (!messageRecord) {
        this.redirectTo({ draft: null });
    } else {
        this.showDraftWindow(messageRecord);
    }
}

И обновляем onComposeMessage, чтобы создание нового черновика тоже шло через маршрут:

javascript
onComposeMessage: function() {
    var messageRecord = Ext.create('ExtMail.model.Message', {
        labels: [ExtMail.enums.Labels.DRAFTS],
        outgoing: true,
        draft: true
    });
    messageRecord.addLabel(ExtMail.enums.Labels.DRAFTS);
    this.getViewModel().getStore('messages').add(messageRecord);
    this.getViewModel().getStore('messages').commitChanges();
    this.redirectTo({
        draft: Ext.String.format('draft/{0}', messageRecord.getId())
    });
}

🧠 Смена парадигмы: UI → URL → Состояние

Самое важное изменение в этой части — рефакторинг мышления. Раньше клики по элементам интерфейса напрямую меняли состояние ViewModel. Теперь все взаимодействия должны вызывать изменение URL, а маршруты — реагировать на это и обновлять состояние.

Это может потребовать пересмотра некоторых участков кода, но в результате приложение становится предсказуемым, состояние сохраняется, а ссылки работают.

🖥️ Как это выглядит в браузере

После добавления маршрутов URL может выглядеть так:

text
http://localhost:1841/#label/all-mail/view/85/draft/1

Это означает, что одновременно активны три маршрута: выбрана метка All Mail, открыто письмо с ID 85, и в фоне редактируется черновик с ID 1. При обновлении страницы приложение восстановит это состояние.

💎 Заключение

Мы завершили серию из семи частей, создав полнофункциональный почтовый клиент на Ext JS:

  • Часть 1: Фундамент приложения, модели, хранилища, Grid и Reader.

  • Часть 2: Метки, дерево, динамические действия, подсчёт непрочитанных.

  • Часть 3: Создание писем, работа с черновиками.

  • Часть 4: Мобильная версия на Modern Toolkit.

  • Часть 5: Выдвижное меню и Compose в Modern.

  • Часть 6: Интеграция с REST API.

  • Часть 7: Deep Linking и Router.

Приложение готово к использованию в production: оно работает на десктопе и мобильных устройствах, сохраняет данные на сервере, поддерживает ссылки и кнопку «Назад». Это отличный пример того, как Ext JS позволяет создавать сложные корпоративные приложения с единой кодовой базой.

Оставьте комментарий