Pull to refresh

Принципы написания приложений на ExtJS 2.x/3.x

Reading time6 min
Views9.2K
Каркас для кроссбраузерной разработки ExtJS сейчас очень популярен. Это поистине грандиозный (и монструозный) набор компонентов, классов, функций, хелперов и т. п., которые могут как облегчить жизнь разработчика, так и усложнить ее. Говоря вообще, ExtJS (до 4-й версии) не устанавливает никаких «правил игры» для конечного разработчика: формально нет никаких требований и рекомендаций по проектированию и написанию надежных приложений.
Удивительно, что до сих пор качество пособий и туториалов для новичков ExtJS, мягко говоря, оставляет желать лучшего. Как и стандартная справка по ExtJS API, впрочем [1].
Цель данной статьи — показать, как писать приложения на базе ExtJS так, чтобы человеку, который будет поддерживать ваш код, не хотелось рвать волосы, а вам просто не было стыдно. А если серьезно, то в данной статье я предложу простой и короткий набор правил проектирования и написания приложений применительно к данному фреймворку.

Откажитесь от жесткой привязки к DOM id


В примерах по ExtJS часто применяют методику жесткого задания id компоненту, после чего выполняется быстрое обращение к нему через вызов Ext.getCmp(id). В примерах уровня «Hello world» все выглядит чудесно, но как только проект становится крупнее, вместо кода обнаруживается каша.
Второй важный минус такого подхода заключается в том, что id обязан быть уникальным в рамках всего документа (дерева DOM).

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


Все очень просто. Инстанциирование стандартных компонентов мы заменяем наследованием. Даже если на данном этапе видится просто панелька с определенным border'ом и текстом внутри — наследуйтесь от Ext.Panel и определяйте свойства в самом классе.
Не превращайте код в последовательность вида «объявить объект — добавить в контейнер».
Иными словами, избегайте кода, аналогичного следующему:
var p = new Ext.Panel({
    region: 'north',
    height: 30,
    html: '<b>Test</b>'
});

var mainPanel = new Ext.Panel({
   layout: 'border',
   items: [
      p, {
           region: 'center'
           title: 'Center'
      }
   ]
});

Очень быстро такой код превратится в кашу, в которой не разберется даже ее создатель. Кроме этого, рост зависимостей между такими элементами в коде практически не контролируем.

Вложенные config-объекты: помним о scope


Не забывайте, что любая функция в JS может вызываться от имени любого объекта. Иными словами, this внутри функции может указывать вообще говоря куда угодно. В случаях, когда мы сами вызываем функцию, синтаксически мы можем проконтролировать эту ситуацию. В случаях же, когда config-объект содержит на разных уровнях вложенности функции (обработчики событий), проблема области видимости (scope) для них становится очевидной. В большинстве случаев this является своего рода «точкой отсчета» для логики обработчиков. Потеряли this — и совсем непонятно, как добраться до соседней кнопочки, чтобы ее спрятать/задисейблить и т. д., и т. п.
Язык javascript предоставляет как минимум следующие методы объекта Function: apply() и call() [2].
Ядро ExtJS дополняет прототип Function методом createDelegate(scope). Эта функция возвращает функцию-обертку вокруг исходной, так что ваша исходная функция гарантированно будет вызвана в области видимости scope.
Для слушателей и итераторов ExtJS всегда можно задать явно, с каким scope следует вызвать указанную функцию [3].

Обращение ко вложенным компонентам


Вложенные компоненты всегда располагаются в коллекции items владельца. При этом есть как минимум три варианта, как добраться до дочернего элемента:
  1. Обращение к элементу коллекции по индексу
    • неочевидный код; зубодробительные конструкции вида return this.get(0).get(3) или еще хуже return this.items.items[0].items.items[3]
    • поменялся порядок следования компонентов в items — все поломалось
    • items до отрисовки родителя в DOM документа является просто массивом (см. ниже).
  2. Обращение к элементу коллекции по itemId. Такой идентификатор не привязан к dom id, но позволяет сделать обращение к элементу более осмысленным. Кроме этого такой способ не зависит от порядка объявления компонентов в родителе.
  3. Обращение через ref. Ref — это способ указать, как будет называться ссылка на данный элемент, когда он будет создан, и на каком уровне эта ссылка будет помещена.
Я рекомендую использовать ref всегда, где это возможно (код короче, понятнее, нет итераций по коллекциям, нет лишних вызовов функций).

Пример использования ref и itemId можно увидеть в следующем примере:
Ext.ns('Util');

Util.MyPanel = Ext.extend(Ext.Panel, {
  
    constructor: function (config) {
      config = config || {};
      
      Ext.apply(config, {
          border: false,
          layout: 'card',
          activeItem: 0,
          items: [{
              xtype: 'panel',
              itemId: 'mainScreen',
              layout: 'border',
              items: [{
		  //...skipped...
		}
              ]
            }, {
              xtype: 'editpanel',
              itemId: 'editScreen',
              listeners: {
                scope: this,
                cancel: this.showMainScreen,
                finish: this.onRuleSaved
              }
            }
          ],
	  buttons: [{
	      ref: '../editBtn',
	      handler: this.onEditBtn,
	      scope: this
	  }]
        }
      );
        
      
      Util.MyPanel.superclass.constructor.call(this, config);
    },
    
    showMainScreen: function () {
      this.getLayout().setActiveItem('mainScreen');
      this.editBtn.enable();
    },
    
    showEditScreen: function () {
      this.getLayout().setActiveItem('editScreen');
    },
    
    //... other methods 
  }
);    

Ext.reg('mypanel', Util.MyPanel);


Регистрируйте собственные события


События в ExtJS — основной механизм взаимодействия виджетов. Это своего рода способ для компонента сообщить внешнему миру о своем состоянии. Каждый виджет может быть слушателем чужих событий, а так же быть источником собственных. При этом у компонента может быть произвольное количество слушателей, все они будут уведомлены о событии. Порядок уведомления слушателей вообще говоря не определен.
События подразумевают минимум предположений о получателях и их количестве, что значительно уменьшает связность кода. А это в свою очередь делает компонент переносимым и обеспечивает возможность его переиспользования в других комбинациях.
Если ваш компонент имеет состояние, о котором могут захотеть узнать объекты извне — регистрируйте собственное событие.
Если вы создаете композитный виджет и внутренний компонент должен что-то сообщить внешнему — используйте собственное событие.

Объект до и после отрисовки. Когда нужен afterrender


Каждый визуальный компонент базируется на элементах DOM страницы браузера. При этом у компонента есть состояние, когда как объект он уже создан, но еще не был «отрисован» в дереве DOM. В таком состоянии любые обращения к внутренней DOM-структуре невозможны. Объект в таком состоянии полуфункционален: формально у него есть все заявленные методы, но нет гарантии, что они будут корректно работать.
Как только компонент фиксирует себя в структуре DOM, он генерирует событие render и затем — afterrender. Честно скажу, что особого различия между этими событиями нет, но как правило событие render используется самим компонентом для внутренних целей и велика вероятность случайно перекрыть обработчик такого события своим методом, скажем, назвав его onRender(). Отловить такую неочевидную проблему будет не так уж легко, а «падает» ExtJS, как известно, всегда молча.
Следует помнить, что компонент сообщает, что он уже отрисован, до того, как отрисованы его дочерние компоненты.
Хорошим тоном считается корректно обрабатывать любые вызовы к компоненту, требующие существования DOM-элементов. При этом, в случае если компонент не отрисован, эти операции следует «отложить» до момента его отрисовки — в этом случае просто повесьте динамически обработчик на свой же afterrender.
Ниже приведен пример функции, корректно обрабатывающей ситуацию, когда вызов происходит до отрисовки компонента в DOM:
    //...other property definitions...

    showDocFeedOptions: function (show) {
      var fn = function () {
        this.feedDocsFieldSet.setVisible(!!show);
      };
      if (this.rendered) {
        fn.apply(this);
      }
      else {
        this.on('afterrender', fn, this);
      }
    }



Ленивая инициализация. Xtype


Волшебное свойство (очередное «хитросвойство») xtype конфигурационного объекта однозначно определяет тип конструируемого объекта. Это означает, что если передать такой конфигурационный объект универсальной фабрике Ext.ComponentMgr.create() (а она вызывается автоматически для каждого вложенного config object'а), то на выходе мы получим экземпляр соответствующего класса.
Такой подход дает возможность:
  • создавать вложенные конфигурационные объекты, в которых видна структура виджета
  • избежать последовательности ручного создания объектов и позднего добавления в контейнеры
  • создавать компоненты по мере надобности (что в свою очередь увеличит скорость отрисовки вашего приложения)

Вместо заключения — немного о проектировании


Как правило, на практике приходится иметь дело с достаточно сложными контролами и компонентами. В этом смысле полезно их разбивать на логические блоки (виджеты), которые являются простыми или композитными компонентами. Очень важно продумать структуру и схему передачи управления между элементами композитного виджета. В своей практике я использую следующий подход:
  1. Внешний компонент (назовем его «родитель») знает о вложенных (дочерних) элементах. Обратное неверно.
  2. Родитель может обращаться напрямую к дочерним элементам.
  3. Дочерние элементы сообщают информацию «наверх» только через механизм событий. Дочерний элемент не должен делать никаких предположений о родителе и наборе его методов.
  4. «Горизонтальные» связи между дочерними элементами (вызовы методы друг друга напрямую) строго запрещены. Только родитель занимается синхронизацией «дочек».
  5. Обращение через константный DOM id (Ext.getCmp()) строго запрещено.

Полезные ссылки:

  1. ExtJS 3 API
  2. Ключевое слово «this» в деталях
  3. Four Tips for Staying on Track With Scope in ExtJS
Tags:
Hubs:
Total votes 49: ↑46 and ↓3+43
Comments22

Articles