ExtJS 5 приносит несколько восхитительных улучшений архитектуры: мы добавили поддержку ViewModel'ей, MVVM, а также ViewController'ов для усовершенствования MVC-приложений. Что самое приятное — эти функции не взаимоисключающие, так что вы можете вводить их шаг за шагом или использовать одновременно.
О контроллерах
В ExtJS 4 контроллер — это клас, наследованный от Ext.app.Controller
. Эти контроллеры используют CSS-подобные селекторы (называемые «Component Queries») для сопоставления компонентов и обработчиков их событий. Ещё они используют так называемые refs
для выборки и получения экземпляров компонентов.
Эти контроллеры создаются при запуске приложения и работают в течение всего жизненного цикла. Представления создаются и удаляются, их может быть несколько экземпляров, и контроллеры обслуживают всех.
Сложности
Для больших приложений эти техники могут нести некоторые сложности.
В таких случаях представления и контроллеры могут создаваться разными авторами в разных командах разработчиков, а затем — интегрироваться в конечное приложение. И быть уверенным, что контроллеры реагируют только на предназначенные для них представления уже сложно. Также разработчики обычно хотят сократить число контроллеров, создаваемых при запуске приложения. А имея возможность (при некотором старании) создавать контроллеры отложено, удалять их возможности нет, так что они остаются даже если уже не нужны.
ViewControllers
Учитывая что Ext JS 5 обратно совместима с текущими контроллерами, она предлагает новый тип контроллеров, созданных для решения такого рода проблем: Ext.app.ViewController
. Решение достигается следующим образом:
- Используются конфигурационные свойства
listeners
иreference
, упрощается связь с представлениями. - Для автоматического управлениями соответствующими ViewController'ами используется жизненный цикл представлений.
- Уменьшается сложность ViewController'ов, т.к. с соответствующим представлением используется связь один-к-одному.
- Обеспечивается инкапсуляция для возможности создания вложенных представлений.
- Остаётся возможность выборки компонентов и отслеживания их событий на любом уровне ниже соответствующего представления.
Слушатели (Listeners)
Конфиг listeners
не новый, но в Ext JS 5 он обзавёлся новыми возможностями. Более полный разбор новых слушателей будет в предстоящей статье — “Declarative Listeners in Ext JS 5”. Для использования во ViewController'ах мы можем посмотреть на пару примеров. Первый — обычное использование конфига listeners
во вложенном компоненте представления:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
items: [{
xtype: 'textfield',
fieldLabel: 'Bar',
listeners: {
change: 'onBarChange' // не задаём контекст (scope)
}
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onBarChange: function (barTextField) {
// будет вызвано по событию 'change'
}
});
В данном случае у именованного обработчика onBarChange
нет определённого контекста (scope
). Система событий для текстового поля Bar
самостоятельно делает контекстом по умолчанию собственный ViewController.
Исторически конфиг listeners
был зарезервирован для использования родительским компонентом, поэтому, возникает вопрос: как представлению слушать собственные события или возможно даже те из них, которые были вызваны базовым классом? Ответом будет использование явного контекста:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
listeners: {
collapse: 'onCollapse',
scope: 'controller'
},
items: [{
...
}]
});
Этот пример использует две новых возможности ExtJS 5: именованные контексты и декларативные слушатели (named scopes and declarative listeners, — прим. перев.). Мы сфокусируемся на именованном контексте. Для именованных контекстов валидны два имени: "this"
и "controller"
. Когда мы пишем MVC-приложения, мы почти всегда используем "controller"
, который очевидно будет собственным ViewController'ом (а не ViewController'ом представления, стоящего выше по иерархии).
Т.к. представление — это Ext.Component
, то мы назначили этому представлению xtype
, который позволяет другим представлениям создавать его экземпляр так же, как мы это делали с textfield
. Чтобы увидеть, как всё совмещается, создадим иерархию:
Ext.define('MyApp.view.bar.Bar', {
extend: 'Ext.panel.Panel',
xtype: 'bar',
controller: 'bar',
items: [{
xtype: 'foo',
listeners: {
collapse: 'onCollapse'
}
}]
});
В этом случае представление Bar
создаёт экземпляр Foo
как один из своих элементов. Далее оно слушает событие collapse
так же, как и представление Foo
. В предыдущих версиях Ext JS и Sencha Touch эти определения конфликтовали бы. Тем не менее, в Ext JS 5 это решается: слушатели, объявленные в Foo
сработают во ViewController'е Foo
, а объявленные в Bar
— во ViewController'е Bar
.
Ссылки (References)
Одна из широко используемых функций в контроллерной логике — это получение необходимого компонента для выполнения какого-либо действия. Что-то вроде этого:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
tbar: [{
xtype: 'button',
text: 'Add',
handler: 'onAdd'
}],
items: [{
xtype: 'grid',
...
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onAdd: function () {
// ... получить grid и добавить запись ...
}
});
Но как нам получить таблицу (gird)? В ExtJS 4 вы могли использовать конфиг refs
или какой-либо другой способ получения компонента. Все техники требуют от вас дать узнаваемое свойство таблице для того, чтобы её уникально идентифицировать. Старые техники использовали конфиг id
(и Ext.getCmp
) или itemId
(используя refs
или другой способ выбора). Преимущество id
— быстрая выборка, но т.к. идентификаторы должны быть уникальны во всём приложении и в DOM, это не всегда достижимо. Использование itemId
и разные виды запросов (query) — более гибко, но необходимо выполнять поиск для нахождения нужного компонента.
С новым конфигом reference
в Ext JS 5 мы просто добавляем его к таблице и используем lookupReference
для её получения:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
tbar: [{
xtype: 'button',
text: 'Add',
handler: 'onAdd'
}],
items: [{
xtype: 'grid',
reference: 'fooGrid'
...
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onAdd: function () {
var grid = this.lookupReference('fooGrid');
}
});
Это похоже на присвоение itemId = 'fooGrid'
и дальнейшее this.down('#fooGrid')
. Но «под капотом» разница значительная. Во-первых, конфиг reference
требует от компонента зарегистрировать себя в представлении-владельце. Во-вторых, метод lookupReference
спрашивает кэш о необходимости обновиться (предположим, вследствие добавления или удаления в контейнере). Если всё нормально, он просто возвращает ссылку из кэша. В псевдокоде:
lookupReference: (reference) {
var cache = this.references;
if (!cache) {
Ext.fixReferences(); // обновить ссылки
cache = this.references; // теперь кэш валидный
}
return cache[reference];
}
Другими словами, здесь нет поиска, а связи, испорченные добавлением или удалением элементов из контейнера исправляются на лету, когда они необходимы. Как мы увидим ниже, кроме эффективности этот подход даёт и другие преимущества.
Инкапсуляция
Использование селекторов в реализации Ext JS 4 MVC было очень гибким, но в то же время несло некоторые риски. Тот факт, что селекторы «видели» все события на всех уровнях было и мощно, но склоняло к ошибкам. Например, контроллер, работающий 100% безошибочно в изоляции мог создавать ошибки, как только появлялись новые представления с нежелательно совпадающими селекторами.
Это могло быть решено следуя некоторым практикам, но используя слушатели и ссылки во ViewController'ах эти проблемы просто исключены. А всё потому что конфиги reference
и listeners
соединяют представление только со своим ViewController'ом. Вложенные представления могут использовать любые значения reference
внутри текущего, зная, что эти имена не будут открыты представлению, стоящему выше по иерархии.
При этом, listeners
запрашиваются у соответствующего ViewController'а и не могут быть обработаны в других контроллерах с неправильными селекторами. Учитывая, что слушатели более предпочтительны, чем селекторы, эти два механизма могут работать одновременно в тех местах, когда оправдано использование подхода, основанного на селекторах.
Чтобы завершить этот пример, рассмотрим случай, когда представление может вызывать событие, которое будет обработано ViewController'ом представления более высокого уровня. Для этого во ViewController'е есть helper-метод: fireViewEvent
. Например:
Ext.define('MyApp.view.foo.Foo', {
extend: 'Ext.panel.Panel',
xtype: 'foo',
controller: 'foo',
tbar: [{
xtype: 'button',
text: 'Add',
handler: 'onAdd'
}],
items: [{
xtype: 'grid',
reference: 'fooGrid'
...
}]
});
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
onAdd: function () {
var record = new MyApp.model.Thing();
var grid = this.lookupReference('fooGrid');
grid.store.add(record);
this.fireViewEvent('addrecord', this, record);
}
});
Это даёт возможность использовать стандартный слушатель в представлении, стоящего выше по иерархии:
Ext.define('MyApp.view.bar.Bar', {
extend: 'Ext.panel.Panel',
xtype: 'bar',
controller: 'bar',
items: [{
xtype: 'foo',
listeners: {
collapse: 'onCollapse',
addrecord: 'onAddRecord'
}
}]
});
От переводчика: звучит запутанно, но по сути fireViewEvent
позволяет вызывать событие за представление внутри ViewController'а. Т.е., если б в данном случае не было ViewController'ов, то это было бы равноценно вызову привычного fireEvent
в коде представления «Foo».
Слушатели и домены событий (Listeners and Event Domains)
В Ext JS 4.2 в диспетчер событий MVC были введены домены событий. Эти домены перехватывают события, как только они вызываются и передают их контроллерам, обрабатывающим их по совпадению селекторов. Домен событий component
имеет полную поддержку селекторов компонентов, а другие — ограниченную.
В Ext JS 5 каждый ViewController создаёт экземпляр нового типа домена событий, который называется view
. Этот домен событий позволяет ViewController'ам использовать стандартные методы listen
и control
, при этом неявно ограничивая их область действия до собственных представлений. Также он добавляет новый специальный селектор для совпадения с собственным представлением:
Ext.define('MyApp.view.foo.FooController', {
extend: 'Ext.app.ViewController',
alias: 'controller.foo',
control: {
'#': { // совпадает с собственным представлением
collapse: 'onCollapse'
},
button: {
click: 'onAnyButtonClick'
}
}
});
Ключевое различие между слушателями и селекторами можно заметить на следующем примере. Селектор button
совпадает с любой кнопкой в этом представлении или в любом вложенном, неважно, хоть в под-под-вложенном. Другими словами, обработчики, основанные на селекторах не учитывают границы наследования. Это поведение совпадает с поведением предыдущих Ext.app.Controller
и в некоторых ситуациях может быть полезной техникой.
Наконец, эти домены учитывают вложенность и эффективно пробрасывают событие вверх по иерархии представлений (bubble an event up, — прим. перев.). То есть, когда событие вызывается, сначала оно передаётся стандартным слушателям. Затем оно передаётся владеющему ViewController'у, после чего — вверх по иерархии родительскому ViewController'у (если есть). В конечном итоге, событие передаётся стандартному домену событий component
для обработки в контроллерах Ext.app.Controller
.
Жизненный цикл
Стандартная практика с большими приложениями — это динамически создавать контроллеры по мере надобности. Это может помочь снизить время загрузки приложения и улучшить его производительность, не затрагивая остальные контроллеры. Ограничение этого подхода в предыдущих версиях было в том, что будучи созданными, эти контроллеры остаются работать на протяжении всей жизни приложения. Было невозможно уничтожить их для освобождения ресурсов. Также, ничего не менялось в том плане, что контроллеры могли обслуживать как несколько экземпляров представлений, так и ни одного.
Поэтому, в жизненном цикле представления ViewController создаётся сразу и он привязан к этому представлению на протяжении всей его жизни. Когда представление удаляется, ViewController — тоже. Это означает, что от ViewController'а больше не требуется работать, когда представлений нет или их несколько (no longer forced to manage states where there is no view or many views, — видимо, идёт сравнение с классическими контроллерами; прим. перев.).
Отношение один-к-одному упрощает отслеживание ссылок и делает невозможным утечку удалённых компонентов. В жизненном цикле ViewController'ов вызываются следующие ключевые события:
- beforeInit — метод, который может быть переопределён для управления представлением до того, как у него вызовется метод
initComponent
. Вызывается сразу после создания контроллера, когда выполняетсяinitConfig
компонента или его конструктор. - init — вызывается сразу после того, как в представлении завершился метод
initComponent
. Это типичный момент для выполнения инициализации контроллера, представление которого уже инициализировано. - initViewModel — вызывается, когда создалась ViewModel представления (если она определена).
- destroy — очистка любых ресурсов (не забудьте вызвать
callParent
).
Заключение
Мы думаем, что ViewController'ы отлично модернизируют ваши приложения. Также они хорошо работают с ViewModel'ями, так что вы можете комбинировать сильные стороны обоих подходов. Мы рады предстоящему релизу и будем также рады улучшениям в работе ваших приложений.
P.S.
Предыдущий пост: Ext JS 5: MVC, MVVM и др
Автор: alexstz