Архитектура клиентского приложения на ExtJS. Часть 2

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


Монолитность компонент



Посещая форум или просматривая примеры использования Ext вы практически всегда будете видеть неряшливо разбросанные по всему файлу различные объекты, которые в итоге крепятся к одному компоненту. Например:

Copy Source | Copy HTML
  1. /**
      * somefile.js
      * @copyright (c) 2009, by someone
      * @date       20 November 2009
      *
      */
  2. function entityAdd(button, e){/* Код добавления сущности */}
  3. function entityRemove(button, e){/* Код удаления сущности */}
  4. function entityEdit(button, e){/* Код редактирования сущности */}
  5. var somestore = new Ext.data.JsonStore({/* Настройки хранилища */});
  6. var somewindow = new Ext.Window({/* Настройки окна */})
  7.  
  8. Ext.namespace("Application");
  9. Application.Grid = Ext.extend(Ext.grid.GridPanel, {
  10.     initComponent: function() {
  11.         Ext.apply(this, {
  12.               store: somestore
  13.             , bbar: new Ext.PagingToolbar({
  14.                   pageSize: 50
  15.                 , store: somestore
  16.                 , displayInfo: true
  17.                 , displayMsg: "Displaying accounts {0} - {1} of {2}"
  18.                 , emptyMsg: "No accounts to display"
  19.             })
  20.             , tbar: new Ext.Toolbar({
  21.                 , items: [{
  22.                     , text: "Add"
  23.                     , handler: entityAdd
  24.                 },{
  25.                     , text: "Edit"
  26.                     , handler: entityEdit
  27.                 },{
  28.                     , text: "Remove"
  29.                     , handler: entityRemove
  30.                 }]
  31.             })
  32.         });
  33.         Application.Grid.superclass.initComponent.apply(this, arguments);
  34.     }
  35. });
  36. Ext.reg("applicationgrid", Application.Grid);
  37.  


Обратите внимание на то, что класс компонента обращается к хранилищу и функциям вне своего контекста. Подобный подход совершенно непригоден в больших проектах, так как ведёт за собой целый ряд проблем:
  • Экземпляров класса у нас может быть несколько и все они будут обращаться к одному хранилищу/окну.
    • В случае с окном мы не имеем возможности из него легко обратиться к вызвавшему экземпляру грида.
    • В случае с хранилищем, существует риск увидеть данные одного экземпляра грида в другом.
    • В обоих случаях у нас ограничены возможности управления циклом жизни объектов. Таким образом, при освобождении памяти от грида нам необходимо оставлять «жить» внешние его ресурсы.


Что я могу предложить, чтоб решить эти проблемы? Советую делать «монолитные» компоненты. Т.е. проектировать их так, чтоб все внешние ресурсы были членами класса грида. Если взять предыдущий пример, то вот какой код я имею ввиду:

Copy Source | Copy HTML
  1. Ext.namespace("Application");
  2. Application.Grid = Ext.extend(Ext.grid.GridPanel, {
  3.       initComponent: function() {
  4.         // Настраиваем диалоговое окно
  5.         Ext.apply(this, {
  6.             somewindow: new Ext.Window({/* Настройки окна */})
  7.         });
  8.  
  9.         // Настраиваем хранилище
  10.         Ext.apply(this, {
  11.             store: new Ext.data.JsonStore({/* Настройки хранилища */}); // [1]
  12.         });
  13.  
  14.         // Настраиваем панель инструментов
  15.         Ext.apply(this, {
  16.               bbar: new Ext.PagingToolbar({
  17.                   pageSize: RESOURCES.SETTINGS.RECORDS_PER_PAGE // [2]
  18.                 , store: this.store
  19.                 , displayInfo: true
  20.                 , displayMsg: "Displaying accounts {0} - {1} of {2}"
  21.                 , emptyMsg: "No accounts to display"
  22.             })
  23.             , tbar: new Ext.Toolbar({
  24.                 , items: [{
  25.                     , text: "Add"
  26.                     , handler: this.entityAdd.createDelegate(this) // [3]
  27.                 },{
  28.                     , text: "Edit"
  29.                     , handler: this.entityEdit.createDelegate(this)
  30.                 },{
  31.                     , text: "Remove"
  32.                     , handler: this.entityRemove.createDelegate(this)
  33.                 }]
  34.             })
  35.         });
  36.         Application.Grid.superclass.initComponent.apply(this, arguments);
  37.     }
  38.     , onDestroy: function(){
  39.         this.somewindow.destroy();
  40.         Application.Grid.superclass.onDestroy.call(this);
  41.     }
  42.     , entityAdd: function(button, e){/* Код добавления сущности */}
  43.     , entityRemove: function(button, e){/* Код удаления сущности */}
  44.     , entityEdit: function(button, e){/* Код редактирования сущности */}
  45.     , somewindow: undefined
  46. });
  47. Ext.reg("applicationgrid", Application.Grid);


[1] Нам нужны как минимум два блока Ext.apply. Первый для настройки хранилища и второй — для настройки пагинатора. Причина: если засунуть определение хранилища и пагинатора в один блок Ext.apply, то пагинатор не сможет сослаться на хранилище по this.store (до выполнения блока this.store хранит undefined).

[2] См. предыдущую статью

[3] За объяснением, что такое createDelegate последуйте сюда. Также стоит кратко рассказать, что такое scope в ExtJS (вы часто будете его видеть как аргумент для функции или свойство в конфигурации). В туториалах есть доступное объяснение этого популярного термина во фреймворке. Но если просто, то scope это тот объект, что передастся функции в качестве её контекста (т.е. this в фунции будет равен значению scope).

Итак, что мы получим при монолитном подходе к проектированию компонент. В связке с createDelegate, мы получим компоненту с предсказуемым поведением и избавимся от всех перечисленных мною выше проблем. Пока что меня этот метод проектирования ни разу не подвёл и как подсказывает опыт, мы получим достаточно красивую реализацию ООП, вместо того убожества, что нам подсовывают в туториалах.

Рассмотрев некоторые нюансы проектирования компонент мы можем перейти к следующей теме, к теме плагинов.

Плагины



Каждый наследник класса Ext.Component имеет свойство plugins. Плагины могут быть обычными функциями (я этот вариант не проверял), но чаще всего это наследники Ext.util.Observable с единственным обязательным методом init. Как их писать написано здесь. А вот здесь лежит простая статья, объясняющая различие подходов в проектировании с использованием плагинов и компонент. Если кратко пересказать статью, выйдет что-то вроде подобного вывода: плагины хороши для небольших надстроек над компонентами. Их легко добавить и легко удалить из компонента. Компоненты же больше привязаны к вашему приложению и хороши при проектировании общего вида классов.

Я бы хотел немного расширить эти статьи. Во-первых, при проектировании пагинов учитывайте в каком контексте они могут быть использованы. Например, если вы хотите добавить некоторую функциональность к гриду, то вам следует учесть, что ваш плагин запросто могут добавить в коллекцию плагинов какому-нибудь выпадающему списку. Сделать проверку на подобные операции очень просто:

Copy Source | Copy HTML
  1. Ext.namespace("Ext.ux");
  2. Ext.ux.Plugin = Ext.extend(Ext.util.Observable, {
  3.     init : function (grid) {
  4.         if (grid instanceof Ext.grid.GridPanel){
  5.             this.grid = grid;
  6.             /* Остальной код */
  7.         }
  8.     }
  9.     /* Прочее */
  10. });
  11. Ext.preg("ourplugin", Ext.ux.Plugin);


Метод init получает экземпляр родительского объекта. Т.е. того объекта, у которого в коллекции plugins будет наш плагин. Всё, что нам остаётся, это проверить его к принадлежности нужному классу.

Нам также необходимо учитывать цикл жизни родительских объектов при проектировании плагинов. Так, например, плагин может быть добавлен к ещё не отрендереному компоненту. А это значит, что при ленивой инициализации его дочерних элементов мы к ним обратиться не сможем. Всё, что мы получим — это конфигурации его дочерних элементов. А потому нам желательно производить все наши действия уже после рендеринга родительского компонента. Делается это тоже очень просто. Подпиской на соответствующее событие:

Copy Source | Copy HTML
  1. Ext.namespace("Ext.ux");
  2. Ext.ux.Plugin = Ext.extend(Ext.util.Observable, {
  3.     init : function (grid) {
  4.         if (grid instanceof Ext.grid.GridPanel){
  5.             this.grid = grid;
  6.  
  7.             if (grid.rendered){
  8.                 // Мы получили уже отрендереный родительский компонент, т.е.
  9.                 // плагин добавлен после рендеринга компонента.
  10.                 this.onRender();
  11.             } else {
  12.                 // В противном случае подписываемся на событие рендеринга
  13.                 // родительского компонента
  14.                 grid.on({
  15.                       scope: this
  16.                     , single: true
  17.                     , render: this.onRender
  18.                     , reconfigure: this.onRender
  19.                 });
  20.             }
  21.         }
  22.     }
  23.     , onRender: function(){
  24.         /* Родительский компонент отрендерен. Можно приступать к работе */
  25.     }
  26. });
  27. Ext.preg("ourplugin", Ext.ux.Plugin);
  28.  


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

Мне пагины помогли сократить колличество кода на проекте вдвое, так как до них я реализовывал расширение функционала наследованием.

Помимо плагинов и компонент, есть ещё один способ расширения функциональности через файлы из /js/resources. Предположим, ваше приложение уже разраслось и имеет массу различного рода компонент-наследников от грида. Но тут приходит требование, чтоб все гриды при загрузке выделяли первую строку. На помощь нам приходит возможность переопределения поведения. В файле из /js/resources мы можем просто написать:

Copy Source | Copy HTML
  1. Ext.override(Ext.grid.GridView, {
  2.     onLoad: Ext.grid.GridView.prototype.onLoad.createSequence(function(){
  3.         if (this.grid.store && this.grid.getSelectionModel().selectFirstRow){
  4.             this.grid.getSelectionModel().selectFirstRow.defer(1, this.grid.getSelectionModel());
  5.         }
  6.     })
  7. });
  8.  


Подобным же образом можно решить, например, такие проблемы как подстветка следующей/предыдущей строки при удалении какой-то из строк или же подсветка только что добавленной строки.

На этом мы закончим тему плагинов и расширения функциональности нашего приложения. В завершение статьи я хотел бы дать некоторые рекомендации.

Советы и рекомендации



Вы уже заметили, что в своих примерах я запятые ставлю перед выражениями. Эта привычка не специфична для Ext, но забыв где-то поставить запятую или, напротив, поставив запятую после последнего элемента (в этому особо щепетильно относится IE) вы натолкнётесь на труднообнаруживаемые ошибки. При таком способе проставления запятых колличество ошибок, связанных с ними очень уменьшится.

При проектировании больших приложений привяжитесь к одной версии Ext и не меняйте её. В третьей ветке я обнаружил массу неприятных моментов, связанных с тем, что код в 3.0.0 не работал в 3.0.1 или работал с ошибками.

В документации по АПИ Ext даёт возможность просматривать исходные коды его классов. Не бойтесь заглядывать туда. Вам откроется много нового.

АПИ по графикам на момент написания данной статьи очень убого освещён. Воспользуйтесь предыдущим советом, а также штудируйте графики в примерах Ext.

На этом я статью закончу. Надеюсь, кому-то она поможет.
+19
20 ноября 2009, 17:18
69
oddy 14,0

комментарии (25)

+2
Etherial #
По поводу лишних запятых. Правильные IDE (например у меня SpKet) сразу показывают такие ошибки. Да и JSLint помогает.
А за статьи спасибо, я, хоть уже и почти год ExtJS использую, пару новых полезных моментов узнал. Например про плагины, это по сути ведь паттерн декоратор получается?
+1
oddy #
Да, именно он. Там сплошные паттерны. Взять те же createInterceptor и createSequence, например.
0
KAndy #
А почему в коде
#, tbar: new Ext.Toolbar({
#, items: [{
#, text: «Add»
#, handler: this.entityAdd.createDelegate(this) // [3]
# },{
используется createDelegate(this) а не
#{
#, text: «Add»
#, handler: this.entityAdd // [3]
#, scope: this // < —
# }
0
oddy #
Ничто не мешает использовать scope в конфигурации, если вам дополнительная гибкость не нужна. Дело в том, что createDelegate позволяет передавать в функцию дополнительные аргументы (это его огромнейшее преимущество).
0
kirill533 #
Что-то я не догоняю.
А чем отличается запись
Ext.apply(this, {
   store: new Ext.data.JsonStore({/* Настройки хранилища */}); // [1]
});
от
this.store = new Ext.data.JsonStore({/* Настройки хранилища */});
?
Если ни чем, то по-моему однострочный вариант лучше.

И по поводу
При проектировании больших приложений привяжитесь к одной версии Ext и не меняйте её.
Я бы такого не советовал, так как на моем опыте переход с ветки 2.1 на 3.0 прошел почти безболезненно. Были парочка багов, я даже писал на форум про баги. В версии 3.0.1 их уже не было.
Но я бы не советовал «хачить» Extjs. То-есть не использовать свойства, которых нет в API, что б не получить неработающий код после обновлений.
Ну и тесты использовать нужно в больших проектах.

Но вообще статья доходчивая, спасибо! Не знал, что плагины можно регить ( Ext.preg ).
0
oddy #
Да, можно вообще Ext.apply не использовать, так как в данном контексте он ничего не даёт.

Касательно привязки привязки к версии — у нас всего лишь был разный опыт «общения» с фреймворком. У меня при апдейтах из репозитория, равно как и при официальном апдейте с 3.0.0 на 3.0.1 лезли пренеприятнейшие, непонятные ошибки. А я недокументированных возможностей, помимо конфигураций графиков, нигде не использовал (графики слабо документированы, приходилось смотреть их исходные коды). Таким образом, если есть даже малейший шанс заполучить хотя бы одну из тех ошибок, связанных с обратной совместимостью — лучше этого избежать. Я трижды откатывался на прежнюю версию, прежде чем окончательно забросить попытки мигрировать в середине проекта на пропатченый Екст. И теперь жалею, что вообще пытался. Если перефразировать Кнута: корень всех бед в миграции без веских причин.

И спасибо за ваш вердикт касательно статьи :)
0
Cancel #
А как же баги? В 3.0.0 я уже наткнулся на два крайне критичных для меня бага (не работал скроллинг в каком-то виджете и ещё что-то), пришлось перелазить. Плюс если не пользоваться хаками, то вполне всё перетаскиваемо (правда, не пользоваться хаками невозможно :( ).
0
oddy #
Смотря что вы подразумеваете под хаками. Баги в 3ей ветке, конечно, есть, но я ещё не сталкивался с критическими. Разве что в релиз кандидате, где просто не работал аккордеон в ie
0
Cancel #
Ну вот типичный хак. Нужно выровнять по вертикали размер элемента формы, то есть чтобы HtmlEditor (например) укладывался в остаток вертикального пространства окна. Используем anchor, всё хоккей. Но как только динамически скрываем-удаляем другой элемент (тоже хаком, ибо метод hide() работает совершенно парадоксально), получаем пустое место, ибо размер HtmlEditor'а не изменился. А динамически менять anchor нельзя без хака.

Или вот как поменять url у объекта Store? Хаком. А потом оказывается, что если к элементу прикручен пейджинг, то при пролистываении/обновлении всё равно открывается старый урл. Опять хак.
+1
oddy #
Я вам не подскажу на счёт выравнивания эдитора, но подскажу, как справиться с динамической сменой url в Store.

Copy Source | Copy HTML
  1. var store = new Ext.data.JsonStore({
  2.       remoteSort: true
  3.     , reader: new Ext.data.JsonReader()
  4.     , proxy: new Ext.data.HttpProxy({
  5.         api : {
  6.             read : {
  7.                   url : "/someurl1"
  8.                 , method: "GET"
  9.             }
  10.         }
  11.     })
  12.     /* Другие настройки */
  13. });
  14.  
  15. store.proxy.setApi(Ext.data.Api.actions.read, {method: "GET", url: "/someurl2"});
  16.  
0
Cancel #
Спасибо, так правильнее, я же выловил отладчиком проблему и сделал примерно так:

mp.LList.store.proxy.api.read.url = url;
0
Cancel #
А ну самое главное забыл. Хаки — это когда ковыряешься в исходниках Ext и потом дёргаешь приватные функции из своего кода, ибо по-другому никак. Или долго и упорно ковыряешься в отладчике, вылавливая странное поведение, чтобы потом хаком это поведение поправить.
0
Etherial #
В этом случае нужно создавать свои компоненты на основе существующих, где и дописывать нужное вам поведение. Уж не знаю считать ли это хаком или просто наследованием :)
Дергать приватные функции вне методов компонента конечно не надо. А странное поведение не всегда является багом, часто это фича ;)
0
Cancel #
Тогда придётся дёргать приватные методы из отнаследованного класса. Другого пути нет. В Ext вообще слишком много шаманства, уж очень сложный продукт.

Кстати, вот на днях в интернетах появилась книга «Ext 3.0 Cookbook», весьма достойное чтиво.
0
oddy #
Ext не безгрешен. Об этом говорят их сабмиты в свн. А хакать всё-таки приходится. Иногда (я описал случай в статье) это очень даже быстрый способ реализации той или иной функциональности, так что лучше предусмотреть возможность хака при проектировании. Есть альтернативный вариант — писать в сапорт. Они оперативно фиксят баги. В течении недели, если баг достаточно критичен. Но по моему опыту, во-первых, неделя — это очень долгий срок почти для любого проекта и, во-вторых, это означает что вы будете регулярно переходить с версии на версию. А это, опять же, огромный риск.
0
oddy #
Да, соглашусь, приходится. Но даже подобное можно сделать красиво. Так, чтоб самого не воротило (а то и чтоб гордиться можно было). А это очень важно.
0
demongloom #
А я по другому описываю построение своих мета обьектов. Вместо наследования/расширения всего и вся, я строю функцию которая возвращает мне метаобьект/апи для работы с ним, а также настраивает взаимосвязи между внутренними базисными кирпичами.

MyProject.widgets.ProductsGrid = function(config) {

var me = {};

me.observer = new Ext.util.Observable;
me.observer.addEvents({
«addproduct»: true
});

me.store = new Ext.data.JsonStore({...})
me.grid = new Ext.grid({
store: me.store,
region: Ext.value(config.region, «center»)

})

me.addProduct = function(product) {
me.store.add(product);
me.observer.fireEvent(«addproduct», product);
}

return me;

}

Такой подход мне позволяет:
1. Отделить апи конкретного виджета с конкретным функционалом от внутренностей extjs.
2. С другой стороны, я могу грид или панели интегрировать и настраивать туда, куда захочу, через геттеры типа getGridPanel и config параметры.

Таким методом я написал для своего проекта код который позволяет мне создать динамически грид:
1. Обычный или Иерархический (maximgb treegrid), причем если я передаю параметр иерархический, то датастор уже автоматически содержит в себе описание необходимых для иерархии полей.
2. С плагином поиска, фильтрации колонок, с автоподсветкой результатов поиска
3. При дополнительных параметрах грид можно пересортировывать, через кнопки в тулбаре или драг-н-дроп.
4. Грид знает как взаимодействовать с соседским гридом, для переноса данных из одного списка в другой.
И т.д.

Конечно многое пришлось в самом ехт патчить ручками, сам до сих пор на 2.3.0 (в целом доволен) и пока не вижу возможностей и веских причин для миграции.
0
yroman #
Скажите, а какой подход вы используете для локализации приложения (если вообще используете)?
0
Etherial #
Собственно подход к локализации в ExtJS уже представлен. Смотрите файлы в source/locale в дистрибутиве ExtJS. Подобный подход + текстовые константы вполне решают проблему локализации.
0
yroman #
Я уже перекопал половину сорсов экста, так что про этот подход я в курсе. Я думал, что может вы предложите что-то более оригинальное, ибо не всегда хочется наследоваться от компонентов, вполне достаточно просто составить конфиг и скормить его кому нужно. А здесь уже подход, который используется в эксте, не подходит.

0
oddy #
К сожалению, если необходимо избежать перезагрузки приложения, приходится использовать предложенный разработчиками Ext'а стандартный метод локализации с перегрузкой объектов. Это очень неудобный метод, но о лучшем я ничего не знаю. Было бы замечательно, если бы кто-то написал статью на эту тему, так как предложенный метод локализации достаточно сложен.
0
vita1ik #
а как осуществляется уничтожение объекта в фреймворке? можна ли его удалить програмно, когда объект считается удаленным? можно ли как-то контролировать занимаемую память?
спасибо.
0
oddy #
Обычно фреймворк сам регулирует жизненный цикл объектов, но в некоторых случаях необходимо самому их уничтожать. Делается это методом destroy, который определён в Ext.Component (базовый класс для всех виджетов). При этом можно отлавливать события элементов beforedestroy и destroy. Объект считается удалённым, если его свойство isDestroyed вернёт true.
0
zokotuhaFly #
Подробно. Аккуратно. Внимательно. То что нужно! Спасибо.
0
Radik_Wind #
Спасибо за интересную статью!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.