Обзор ExtJS 4. Опыт портирования со старой версии

ExtJS/Sencha
Несмотря на то, что четвертая версия каркаса ExtJS вышла уже достаточно давно, материалов по этой версии на хабре не слишком много. А если учесть, что в четвертой версии существенно переработан API, структура классов и предлагаемая архитектура приложения, то, как мне кажется, тут есть, о чем поговорить.

В данной статье я попробую рассказать свои впечатления от фреймворка в контексте портирования существующей кодовой базы на новую версию; постараюсь так же не забыть о подводных камнях и ссылках на полезные (и не очень) страницы справки.


Ext4, ugh. Not backward compatible with Ext3, and imposing all sort of new methodologies to make it «easier». The Windows Vista of Javascript frameworks
(один из комментариев на stackoverflow.com )

ExtJS 4 несовместим с предыдущими версиями фреймворка.
Это означает, что ваше приложение, написанное под любую другую версию каркаса, работать не будет (за исключением тривиальных случаев). Произойдет это в силу следующих причин:
  1. Изменились требования к архитектуре приложения
  2. Изменилась структура классов
  3. Частично изменилась семантика API


Архитектура приложения


Самое существенное изменение касается архитектуры: ExtJS4 теперь проповедует шаблон MVC на клиентской стороне. Вся «бизнес-логика» вашего компонента должна быть вынесена в сущность «Контроллер», а то, что предоставляет визуальный интерфейс — сделать «Представлением» (view); логика работы с данными должна быть вынесена в слой «Модели» (к которой относятся непосредственно сами Ext.data.Model, известные ранее как Ext.data.Record, и хранилища Ext.data.Store).

По факту все визуальные компоненты — панели, таблицы, деревья и т.п. — это претенденты на то, чтобы быть представлением или его частью. Возможность вложения (агрегации) визуальных компонентов теперь возможна только для уровня view: контроллеры существуют, по сути, в плоском слое.

Представления сообщают «наверх» об изменении своего состояния через механизм событий.
Важные с точки зрения «бизнес-задачи» события должны быть обработаны в контроллере. Для этого у каждого контроллера есть волшебный метод control(), с помощью которого он подписывается на события, пришедшие от какой-то части представления. Контроллер сообщает, от кого он хочет получать события с помощью CSS-like синтаксиса, который называется ComponentQuery:
Ext.define('My.controller.Header', {
    extend: 'Ext.app.Controller',
    //...
    init: function() {
      this.control({
          'button[action=help-toggle]': { 
            scope: this,
            click: this.toggleHelp
          },  
          'mainmenuview': { 
            scope: this,
            afterrender: this.menuRendered
          }   
        }   
      );  
    },  

    toggleHelp: function () {
      //some actions
    },

    menuRendered: function () {
      //some other actions
    }
  }
); 

Здесь важно понимать следующие вещи:
  1. Каждый контроллер «видит» весь доступный viewport, поэтому часто нужно точно указать, что вам нужна вполне определенная панелька или вполне себе уникальная кнопка, и это не всегда легко.
  2. ComponentQuery, написанный в параметре к control(), это правило отбора событий, и действует оно динамически. Это означает, что для любого события от view приложение решает, можно ли вызвать данный обработчик в контроллере или нет.

Таким образом то, что раньше было инкапсулировано в одном компоненте, который можно было легко переиспользовать, теперь оказывается разбитым на несколько разрозненных частей: набор «бездумных» представлений, набор моделей и набор контроллеров. Это создает преимущество для небольших приложений со статически заданными элементами и серьезные трудности — для страниц с динамическим созданием компонентов (о такой проблеме можно почитать, например, здесь).

Классы и компоненты


В четвертой версии каркаса серьезно переработана структура стандартных классов. Сами имена классов стали лаконичнее, разделение обязанностей между классами — более продуманным.
Порадовало, например, что логика по чтению/записи данных для Store теперь действительно инкапсулирована в Ext.data.proxy.Proxy (ранее, чтобы, например, переопределить работу с RESTful-сервером требовалось перекрыть как соответствующие методы в Proxy, так и в самом хранилище — в методах onWrite, onRead и onUpdate).
Кроме этого, больше нет той странной ситуации с GroupingStore, когда приходилось выбирать, будет ли Store работать с данными JSON (JsonStore), XML (XmlStore) или он будет уметь группировать данные (GroupingStore) для таблицы, и все эти 3 класса были на одном уровне иерархии.

В некоторых случаях поведение объектов стало более предсказуемым. Так, теперь до отрисовки компонента (до события 'render') его коллекция items имеет тип MixedCollection, а не массив, как раньше (мелочь, а приятно), а таблица Ext.grid.Panel теперь самостоятельно отрабатывает потерю selection после перезагрузки хранилища (раньше эту ситуацию нужно было описывать вручную).

ExtJS 4 не изменяет прототипы стандартных яваскриптовых классов.
Так, теперь утилитарные функции, которыми ранее дополнялись прототипы (Function.createDelegate(), String.format(), Array.indexOf()) вынесены в отдельные объекты-синлетоны (Ext.Function.bind(), Ext.String.format() и Ext.Array.indexOf() соответственно).

Изменения в API


В большинстве случаев имена методов тоже стали лаконичнее, но как факт — они изменились. Соответственно, вам потребуется потратить время на то, чтобы убедиться все ли в вашем приложении работает корректно. Например, метод selectRow() у SelectionModel теперь соответствует новому select().

Из неприятного: сильно изменилась семантика работы с хранилищами, работающими с удаленными данными. Я был удивлен, когда выяснилось, что у Store больше нет «ручек» для того, чтобы добавить дополнительный набор параметров для запроса к серверу. Никаких аналогов setBaseParam() или load(params) больше не предоставляется.
Нижележащий класс Proxy может работать с любым набором параметров, как и раньше, но на уровне Store есть строго определенное множество ключей, которые отдаются Proxy.

UPD В комментариях подсказывают, что передать дополнительные параметры на самом деле можно (спасибо MrSLonoed за дополнение).

Очевидно в качестве альтернативы setBaseParam() хранилища поддерживают так называемые фильтры и сортировщики (Filters и Sorters). Эти сущности могут использоваться как локально (работая над имеющимся набором данных), так и удаленно (будучи отправленными на сервер в качестве параметров). Что характерно: для фильтров и сортировщиков нет гибкой возможности сериализации в HTTP-параметры. Так, фильтры всегда будут сериализованы в следующем виде
mywebserver?{otherparams}&filters={your serialized filters}
Управлять сериализацией фильтров можно только в пределах строки {your serialized filters}, при этом от параметра filter избавиться стандартными средствами не удастся.

Вот пример того, как фильтры могут быть использованы для добавления дополнительных параметров в запрос:

Ext.define('My.proxy.Ajax', {
    extend: 'Ext.data.proxy.Ajax',
    alias: 'proxy.myajax',

    filterParam: '', 

    getParams: function (params, operation) {
      params = this.callParent(arguments);
      var filters = operation.filters;
      if (this.filterParam === '' && filters && filters.length) {
        Ext.apply(params, this.encodeFilters(filters))
      }   

      return params;
    },

    encodeFilters: function (filters) {
      var f, po = {}, i;
      for (i = 0; i < filters.length; ++i) {
        f = filters[i];
        po[f.property] = f.value;
      }
      return po;
    }
  }
);

UPD: Обратите внимание на важную деталь: по умолчанию добавленные фильтры будут работать как удаленно, так и локально. Это значит, что если вы добавите фильтр «q=myValue», этот фильтр будет сериализован в соответствующий HTTP-параметр (что хорошо), но и затем будет применен к Store после загрузки. И отлавливать причину пропадания записей, когда они все были получены, можно тоже долго. Поэтому присмотритесь к параметру Store.filterOnLoad.

Второй неприятный момент так же связан с AJAX. По какой-то причине, в ExtJS4 отсутствует простой случай Writer'а, который отправляет объект как множество HTTP параметров «ключ-значение». Реализация его тривиальна, но «осадок остался»:
Ext.define('My.data.writer.Http', {
    extend: 'Ext.data.writer.Writer',
    alias : 'writer.http',

    writeRecords: function(request, data) {
        if (Ext.isArray(data)) {
          data = data[0];
        }
        Ext.apply(request.params, data);
        return request;
    }
  }
);


Динамическая загрузка


Новая версия фреймворка предоставляет возможность динамической загрузки JS-ресурсов (в том числе кроссдоменной и даже для протокола file:// — так, например справку можно открыть просто как html-файл из файловой системы). В связи с этим рекомендуется в каждом своем классе явно прописывать имена классов или пакетов, от которых данный класс зависит.
Делается это с помощью вызова Ext.require() или с помощью одноименной секции в теле объявления класса.

При включенной подгрузке исходников ExtJS гарантирует корректную подгрузку графа классов таким образом, чтобы все зависимости были корректно учтены.
С этой же особенностью связано новое требование, касающееся расположения исходных кодов в файловой системе: полное имя класса должно однозначно определять место файла в файловой системе (как это принято, например, в Java или в Zend Framework в PHP).

Исходников ExtJS теперь тоже несколько вариантов:
  • ext-all-debug.js
  • ext-all-debug-w-comments.js
  • ext-all-dev.js
  • ext-all.js
  • ext-debug.js
  • ext-dev.js
  • ext.js


Файлы ext*.js, не содержащие в имени суффикса all — это исходники с включенным режимом динамической загрузки. Такие ресурсы стоит использовать только в режиме отладки.
К слову, «случайно» включенная динамическая подгрузка ресурсов — это еще один источник «молчаливых» падений ExtJS, если подгружаемый ресурс содержит синтаксическую ошибку. Впрочем, JSLint еще никто не отменял.

Подробнее о целях каждого из вариантов «линковки» ExtJS можно прочитать вот здесь.

Читайте исходники и будьте хорошими программистами!
Спасибо за внимание
Tags:javascriptextjs4портирование
Hubs: ExtJS/Sencha
+32
5.7k 61
Comments 22

Popular right now