Data Package в Ext JS: как устроен слой данных, который отделяет UI от бэкенда

В большинстве современных фронтенд-фреймворков работа с данными — это всегда «сборка» из нескольких библиотек. Вы выбираете отдельный инструмент для управления состоянием (Redux, Pinia), отдельный HTTP-клиент (Axios, Fetch), отдельную библиотеку для кэширования и синхронизации. Каждый инструмент нужно настроить, интегрировать с остальными и поддерживать.

Ext JS решает эту задачу иначе. В состав фреймворка входит Data Package — встроенный слой данных, который полностью отвечает за загрузку, хранение, манипуляцию и сохранение данных в приложении. Он отделяет UI-компоненты от источника данных и работает с любым бэкендом — REST, GraphQL, WebSocket, LocalStorage — без изменения кода интерфейса.

В этой статье мы разберем три ключевых класса Data Package — Model, Store и Proxy — и покажем, почему этот слой считается «сердцем бизнес-логики» Ext JS-приложения.

🧱 Три кита Data Package

Data Package состоит из множества классов, но три из них используются практически в каждом приложении:

  1. Model (Ext.data.Model) — представляет сущность приложения.

  2. Store (Ext.data.Store) — коллекция экземпляров моделей с клиентским кэшем.

  3. Proxy (Ext.data.proxy.Proxy) — «шлюз» для загрузки и сохранения данных на сервере.

Эти классы работают вместе как единый механизм: Proxy загружает данные, Store их хранит и управляет ими, а Model описывает структуру каждой записи. UI-компоненты (Grid, Form, Chart) подключаются к Store через механизм привязки данных (data binding) и автоматически обновляются при любых изменениях.

📦 Model: описание сущности и бизнес-правил

Model — это центральный элемент Data Package. Она представляет сущность приложения: Пользователя, Заказ, Товар, Сделку. На простейшем уровне Model определяет набор полей и связанную с ними бизнес-логику.

Создание Model

Обычно модели определяются через наследование от общей базовой модели. Это позволяет задать общие настройки (например, proxy или схему) в одном месте:

javascript
Ext.define('MyApp.model.Base', {
    extend: 'Ext.data.Model',
    fields: [{
        name: 'id',
        type: 'int'
    }],
    schema: {
        namespace: 'MyApp.model',
        proxy: {
            type: 'ajax',
            url: '{entityName}.json',
            reader: {
                type: 'json',
                rootProperty: '{entityName:lowercase}'
            }
        }
    }
});

Здесь схема (schema) определяет общие настройки для всех моделей: пространство имён и шаблон Proxy по умолчанию. Это избавляет от необходимости дублировать одни и те же настройки в каждой модели.

Поля и валидация

Каждая Model может содержать поля различных типов (string, int, float, boolean, date) и правила валидации. В Ext JS есть пять встроенных валидаторов:

  • Presence — проверяет, что поле заполнено.

  • Length — проверяет длину строки (min/max).

  • Format — проверяет соответствие регулярному выражению.

  • Inclusion — проверяет, что значение входит в заданный список.

  • Exclusion — проверяет, что значение не входит в заданный список.

Пример модели с валидацией:

javascript
Ext.define('MyApp.model.User', {
    extend: 'MyApp.model.Base',
    fields: [
        { name: 'name', type: 'string' },
        { name: 'email', type: 'string' }
    ],
    validators: {
        name: [
            'presence',
            { type: 'length', min: 3 },
            { type: 'exclusion', list: ['admin', 'root'] }
        ],
        email: [
            { type: 'format', matcher: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
        ]
    }
});

Когда вы создаёте экземпляр модели, вы можете проверить его валидность:

javascript
var user = new MyApp.model.User({ name: 'Bender', email: 'bender@planetexpress.com' });
console.log(user.isValid()); // false — имя слишком короткое
var errors = user.getValidation(); // возвращает объект с ошибками

🗄️ Store: клиентский кэш и управление данными

Store — это коллекция экземпляров Model. Он содержит локальный кэш данных на клиенте и управляет их загрузкой и сохранением через Proxy.

Создание Store и загрузка данных

javascript
var store = new Ext.data.Store({
    model: 'MyApp.model.User'
});

store.load({
    callback: function() {
        var firstName = this.first().get('name');
        console.log(firstName);
    }
});

Store также может загружать данные напрямую, без обращения к серверу:

javascript
new Ext.data.Store({
    model: 'MyApp.model.User',
    data: [
        { id: 1, name: 'Philip J. Fry' },
        { id: 2, name: 'Turanga Leela' }
    ]
});

Сортировка, фильтрация и группировка

Одна из ключевых возможностей Store — локальная сортировка, фильтрация и группировка. Это означает, что данные можно обрабатывать на клиенте без повторных запросов к серверу:

javascript
new Ext.data.Store({
    model: 'MyApp.model.User',
    sorters: ['name', 'id'],
    filters: {
        property: 'name',
        value: 'Philip J. Fry'
    }
});

В этом примере данные будут отсортированы сначала по имени, затем по id, и отфильтрованы так, чтобы остались только пользователи с именем «Philip J. Fry».

Chained Stores

Ext JS поддерживает Chained Stores — механизм, который позволяет нескольким компонентам использовать один и тот же набор данных, но с разными настройками сортировки, фильтрации и группировки. Например, Grid может показывать все заказы, а Chart — только заказы за последний месяц, при этом оба компонента используют один и тот же Store.

🔌 Proxy: шлюз к данным, не зависящий от бэкенда

Proxy — это класс, который знает, как загружать и сохранять данные. Он используется Store и Model для взаимодействия с сервером или локальным хранилищем.

Два типа Proxy

  1. Client Proxy — работает с данными на клиенте:

    • Memory — хранит данные в памяти.

    • LocalStorage — использует HTML5 LocalStorage.

  2. Server Proxy — взаимодействует с удалённым сервером:

    • Ajax — обычные AJAX-запросы.

    • REST — RESTful-запросы.

    • JSONP — запросы с обходом CORS.

Пример конфигурации Proxy

javascript
Ext.define('MyApp.model.User', {
    extend: 'Ext.data.Model',
    fields: ['id', 'name', 'email'],
    proxy: {
        type: 'rest',
        url: '/api/users',
        reader: {
            type: 'json',
            rootProperty: 'data'
        },
        writer: {
            type: 'json'
        }
    }
});

Ключевое преимущество: Proxy не зависит от протокола и от бэкенда. Вы можете переключиться с REST на GraphQL или с PHP на Node.js, не меняя ни строчки в UI-компонентах. Компоненты просто получают данные из Store — им всё равно, откуда они пришли.

🔗 Associations: связи между моделями

Большинство приложений работают не с изолированными сущностями, а со связанными данными. Например, у Пользователя может быть много Постов, а у Поста — один автор. Ext JS поддерживает несколько типов связей:

  • ManyToOne — много Постов принадлежат одному Пользователю.

  • OneToOne — один Профиль соответствует одному Пользователю.

  • ManyToMany — множество Пользователей могут состоять во множестве Групп.

Пример определения связи ManyToOne

javascript
Ext.define('MyApp.model.User', {
    extend: 'MyApp.model.Base',
    fields: [{ name: 'name', type: 'string' }]
});

Ext.define('MyApp.model.Post', {
    extend: 'MyApp.model.Base',
    fields: [
        { name: 'userId', reference: 'User', type: 'int' },
        { name: 'title', type: 'string' }
    ]
});

После определения связи Ext JS автоматически генерирует методы для работы со связанными данными:

javascript
// Загрузка пользователя и всех его постов
MyApp.model.User.load(1, {
    callback: function(user) {
        console.log('User: ' + user.get('name'));
        user.posts(function(posts) {
            posts.each(function(post) {
                console.log('Post: ' + post.get('title'));
            });
        });
    }
});

// Создание нового поста для пользователя
user.posts().add({ title: 'Новый пост' });
user.posts().sync(); // сохранение на сервере

Загрузка вложенных данных

Когда связи определены, Ext JS может загружать связанные записи в одном запросе. Если сервер вернёт данные в таком формате:

json
{
    "success": true,
    "user": [{
        "id": 1,
        "name": "Philip J. Fry",
        "posts": [
            { "title": "Post 1" },
            { "title": "Post 2" }
        ]
    }]
}

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

🆕 Что нового в Ext JS 8.0

В версии 8.0 Data Package получил несколько важных улучшений:

  • Расширенная поддержка ECMAScript до ES2025 — теперь вы можете использовать современный синтаксис (let, const, стрелочные функции) в моделях и Store без транспиляции.

  • Улучшенная производительность Grid при работе с большими данными — горизонтальная буферизация и виртуализация колонок позволяют плавно отображать таблицы с более чем 1000 колонок.

  • Улучшенная поддержка ARIA — полная поддержка полей форм для скринридеров (JAWS, Narrator, TalkBack, VoiceOver).

💎 Заключение

Data Package — это не просто «ещё один способ загрузить данные». Это архитектурный фундамент Ext JS-приложения, который:

Компонент Что решает
Model Описание сущностей, полей, валидаций и связей
Store Клиентский кэш, сортировка, фильтрация, группировка, Chained Stores
Proxy Загрузка и сохранение данных, независимость от бэкенда
Associations Связи между моделями (ManyToOne, OneToOne, ManyToMany)
Schema Централизованное управление всеми моделями приложения

В большинстве фреймворков вам нужно самостоятельно собирать этот слой из десятка библиотек. В Ext JS он встроен, интегрирован и работает согласованно. Именно поэтому Data Package считается «сердцем бизнес-логики» Ext JS-приложения.

Если вы строите корпоративное приложение, где важна надёжная работа с данными, — Data Package заслуживает самого пристального внимания.


Если у вас есть опыт работы с Data Package в Ext JS или вопросы по моделям, Store и Proxy, поделитесь в комментариях.

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