Pull to refresh

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

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


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



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

Copy Source | Copy HTML
  1. /**<br/>  * somefile.js<br/>  * @copyright (c) 2009, by someone<br/>  * @date       20 November 2009<br/>  *<br/>  */
  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.

На этом я статью закончу. Надеюсь, кому-то она поможет.
Tags:
Hubs:
+19
Comments 25
Comments Comments 25

Articles