Pull to refresh

Cоздание прототипа социальной сети на ExtJS. Первые и не последние проблемы с ExtJS 4

Reading time 5 min
Views 5.6K
Постоянно меняющиеся требования и сжатые сроки подтолкнули нас к использованию ExtJS 4 для создания прототипа.

Проблемы в ExtJS, с которыми мы столкнулись при разработке, едва ли превысили опыт, который мы получили при препарировании ExtJS.

В интернете много статей про ExtJS 4 уровня basic. Например очень порадовала эта статья http://css.dzone.com/articles/how-use-extjs-4-jquery. А вот серьезных “не кассовых” статей по решению (или созданию) каких-то проблем с помощью ExtJS не много.

Предлагаю вашему внимаю первую статью в рамках ExtJS patch lab… Ну и немного о самой соц. сети… совсем немного.



Для ленты постов решено было использовать Ext.panel.Grid с Ext.data.Store. Можно сказать это те фундаментальные вещи, ради которых и выбрали использование ExtJS.

Почему именно grid, а не dataView (в последствии конечное пришлось отказаться от грида в пользу вьюшки). У грида понравился визуальный режим сортировки и фильтрации данных, “бесконечный” скролл. Радость от предоставленных возможностей просто переполняла.

Ext.data.Store


Данные приходят от сервера с помощью API вызова, далее в сыром виде передаются в метод loadData класса Store. Использовать встроеный в Store прокси не стали из-за наличия у нас API как у Foursqure.

var myStore = Ext.create('Ext.data.Store', {
    model: 'Post',
    filterOnLoad: true,
    idProperty: 'id',
    filters: new Ext.util.Filter({
        filterFn: function (item) {
            return item.get("status") > 0;
        }
    })
});
…
API.Events.get().done(function(posts){
   myStore.loadData(posts);
});

Модель Post из себя представляет наследника класса Ext.data.Model.

Ext.define('Post', {
    extend: 'Ext.data.Model',
    idProperty: 'id',

    constructor: function (raw) {
        return this.callParent([this.prepare(raw), raw[this.idProperty]]);
    },
    prepare: function (raw) {
        …
    }
});

В модели Post происходит подготовка сырых данных через метод prepare. В конструктор родительского класса данные приходят уже в обработанном виде.

Что происходит в методе prepare:
Например приведение даты поста из строки к объекту Date

this.data = new Date(this.data);

…получение строкового значения наименования поста из typeId через хэш таблицу соответствий

this.typeName = Dict.post.types[this.typeId];

И так далее...

Store хранит внутри себя данные в виде инстанцированных моделей, в методе loadData происходит проверка, если данные еще сырые, то передаем их в конструктор модели, и записываем ее у себя, и так с каждым элементом массива.

Приятная полезность #1


Таким образом loadData принимает как массив сырых объектов, так и уже инстанцированных моделей.

Особенность #1


При передачи в loadData единичного объекта произойдет ошибка, метод ожидает только массива, пусть даже из одного элемента.

Особенность #2


Объекты в javascript передаются по ссылке, поэтому переданные сырые данные в loadData станут массивом моделей.

API.Events.get().done(function(posts){
    //posts is array of objects
    myStore.loadData(posts);
    //posts is array of Models
});

Если вам нужны исходные данные после загрузки в неизменном виде, используйте метод Ext.Array.clone(), но этот метод не поддерживает глубокого клонирования, как jQuery.extend.

Проблема #1


У метода loadData есть второй параметр append (append True to add the records to the existing records in the store, false to remove the old ones first). Т.е. от сервера приходят новые посты, мы их должны добавить к уже имеющимся. Для этого выставляем второй параметр true.

В нашем случае хранилище использует фильтрацию по загрузке (filterOnLoad = true)

Как работает фильтрация:

Store хранит все записи в двух коллекциях, основной – data, и в скрытой – snapshot. Ключ “filterOnLoad: true” в Store заставляет хранилище при загрузке данных любым способом проверять их на фильтрах и отбрасывать отфильтрованные в свое внутреннее хранилище snapshot.

Использование фильтров в Store и метод loadData несовместимы.

Листинг из файла ext-all-debug.js (Метод loadRecords используется loadData… и не только)

loadRecords: function(records, options) {
    …
    if (!options.addRecords) {
        delete me.snapshot;
        me.clearData();
    }

    me.data.addAll(records);
    …
}

Вот здесь и кроется проблема, options.addRecords в нашем случае true, удаление snapshot не происходит, данные попадают только в основную коллекцию.

API.Posts.get().done(function(posts){
    myStore.loadData(posts); //posts.length 8, filtered 2 entries
});
…
API.Posts.get().done(function(posts){
    myStore.loadData(posts, true); //posts.length 10, filtered all entries
});

В этоге при первой загрузке прошли только 2 записи, а во втором все.

me.data содержит 2 + 10 записей, а me.snapshot только 8, т.к. данные из второго вызова не попали в скрытое хранилище.

Для того, чтобы пересортировать нашу коллекцию, нам нужно очистить старые фильтры и применить новые, нельзя просто “перечитать” фильтры (что очень и очень странно).

Метод mystore.clearFilter() убивает 10 последних записей, в итоге в хранилище у нас 8 записей.

Решение


loadRecords: function(records, options) {
    …
    if (!options.addRecords) {
        delete me.snapshot;
        me.clearData();
    }
    me.data.addAll(records);
    //add records in snapshot too.
    if (options.addRecords && me.snapshot) {
        me.snapshot.addAll(records)
    }
    …
}

Проблема #2


Ключ idProperty не используется по назначению.

Поле idProperty это аналог первичного ключа в БД. В нашем случае данные имеют уникальный ключ ID, по которому свежие посты должны добавляться в хранилище. В случае изменения какого-то поста, например добавления к нему комментария мы должны обновить эту запись в хранилище. Метод loadData в режиме append с выставленным ключом idProperty = «id» должен решить эту проблему. Но на самом деле этот ключ не используется для этого, и мы не можем сделать объединение данных автоматом.

Покопавшись в исходникам я обнаружил что такой функционал присутствует, но попросту не используется. Метод me.data.addAll(records) может принимать не только массивы, но и объекты ключ-значение. Нужно лишь слегка модифицировать тот же метод loadRecords чтобы использовать эту возможность.

Решение


loadRecords: function(records, options) {
    …
    if (!options.addRecords) {
        delete me.snapshot;
        me.clearData();
    }
    me.data.addAll(records);

    if (this.idProperty) { 
        var indexRecord = {}; 
        for (i = 0; i < length; i++) { 
            indexRecord[records[i].data[this.idProperty]] = records[i]; 
        } 
        me.data.addAll(indexRecord); 
        if (options.addRecords && me.snapshot) { 
            me.snapshot.addAll(indexRecord); 
        } 
        records = []; 
        for (key in indexRecord) { 
            records.push(indexRecord[key]); 
        } 
        length = records.length; 
    } else { 
        me.data.addAll(records);
        if (options.addRecords && me.snapshot) { 
            me.snapshot.addAll(records); 
        } 
    }
    …
}

После этим изменений в качестве ключа мы имеем значения id, и можем добавлять новые данные, они не будут дублироваться…

P.S. Не забываем, что лезть в исходники со своими правками плохо, используйте Ext.override. Таким образом вы избавляетесь от проблемы при обновлении версии фреймворка, вы просто пишите свой патч файл, и при исправлении в новой версии старой проблемы – вы удаляете свое решение из патча.

На горизонте рассмотрение проблемы синхронизации Store с его представлением, и что делать если представлений несколько.

Принимаются заявки, быть может есть какие-то более интересные проблемы и их решения.
Tags:
Hubs:
+31
Comments 23
Comments Comments 23

Articles