Подтвердить что ты не робот

Проблемы рендеринга BackboneJS

В течение последних шести месяцев я работал с Backbone. Первые два месяца возились, изучали и выясняли, как я хочу структурировать свой код вокруг него. Следующие 4 месяца избивали производственное приложение. Не поймите меня неправильно, Backbone спас меня от многопользовательского беспорядка клиентского кода, который раньше был стандартом, но он позволил мне сделать больше грандиозных вещей за меньшее время, открыв полный новый стек проблем. По всем вопросам, которые я поднимаю здесь, есть простые решения, которые выглядят как хаки или просто ошибаются. Я обещаю награду в размере 300 очков за потрясающее решение. Здесь:

  • Загрузка. Для нашего случая использования (панель администратора) пессимистическая синхронизация плохая. Для некоторых вещей мне нужно проверить действия на сервере, прежде чем принимать их. Мы начали, прежде чем событие sync было объединено с Backbone,

и мы использовали этот маленький код для имитации события загрузки:

window.old_sync = Backbone.sync

# Add a loading event to backbone.sync
Backbone.sync = (method, model, options) ->
  old_sync(method, model, options)
  model.trigger("loading")

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

Теперь для жестких:

  1. Слишком много вещей слишком много делают.. Скажем, у нашего приложения есть вкладки. Каждая вкладка управляет коллекцией. С левой стороны вы получаете коллекцию. Вы щелкните модель, чтобы начать редактирование в центре. Вы меняете свое имя и нажимаете вкладку, чтобы перейти к следующему элементу формы. Теперь ваше приложение "в реальном времени что-то", которое замечает разницу, запускает проверки и автоматически синхронизирует изменения с сервером, не требуется кнопка сохранения! Отлично, но H2 в начале формы совпадает с именем на входе - вам нужно его обновить. О, и вам нужно обновить имя в списке в сторону. OH, и список сортируется по именам!

Вот еще один пример: вы хотите создать новый элемент в коллекции. Вы нажимаете кнопку "новый", и вы начинаете заполнять форму. Вы сразу добавляете элемент в коллекцию? Но что произойдет, если вы решите отказаться от него? Или если вы сохраните всю коллекцию на другой вкладке? И там есть загрузка файла. Вам нужно сохранить и синхронизировать модель, прежде чем вы сможете начать загрузку файла (чтобы вы могли прикрепить файл к модели). Итак, все начинается с дрожания: вы сохраняете модель и список, а форма снова отображается - теперь она синхронизируется, поэтому вы получаете новую кнопку удаления, и она отображается в списке, - но теперь загрузка файла завершена, поэтому все снова начинает рендеринг.

Добавить подвид в микс, и все начинает выглядеть как фильм Феллини.

  1. Он находится на всем пути вниз - Вот хорошая статья об этом материале. Я не мог, ради любви ко всему, что свято, найти правильный способ подключить плагины jQuery или события DOM к любому представлению, имеющему subviews. Ад приходит незамедлительно. Всплывающие подсказки слышат, что рендер придет долго и начинает волноваться, subviews становятся похожими на зомби или не реагируют. Это основные моменты боли, так как здесь присутствуют настоящие ошибки, но у меня все еще нет всеобъемлющего решения.

  2. Мерцание - рендеринг выполняется быстро. На самом деле, это так быстро, что мой экран выглядит так, будто у него был захват. Иногда это изображения, которые должны снова загружаться (с другим вызовом сервера!), Поэтому html минимизирует и затем снова резко увеличивается - ширина css + высота для этого элемента исправляет это. иногда мы можем решить это с помощью fadeIn и fadeOut - которые являются болью в заднице, потому что иногда мы повторно используем представление и иногда создаем его заново.

TL; DR. У меня проблемы с представлениями и подзаголовками в Backbone. Он отображает слишком много раз, он мерцает, когда он отображает, subviews отделяет мои события DOM и питает мои мозги.

Спасибо!

Более подробная информация: BackboneJS с Ruby on Rails Gem. Шаблоны с использованием шаблонов UnderscoreJS.

4b9b3361

Ответ 1

Частичное отображение представлений

Чтобы свести к минимуму полную рендеринг вашей иерархии DOM, вы можете настроить специальные узлы в DOM, которые будут отражать обновления для данного свойства.

Позвольте использовать этот простой шаблон подчёркивания, список имен:

<ul>
  <% _(children).each(function(model) { %>
    <li>
        <span class='model-<%= model.cid %>-name'><%= model.name %></span> :
        <span class='model-<%= model.cid %>-name'><%= model.name %></span>
    </li>
  <% }); %>
</ul>

Обратите внимание на класс model-<%= model.cid %>-name, это будет наша точка инъекции.

Затем мы можем определить базовый вид (или изменить Backbone.View), чтобы заполнить эти узлы соответствующими значениями при их обновлении:

var V = Backbone.View.extend({
    initialize: function () {
        // bind all changes to the models in the collection
        this.collection.on('change', this.autoupdate, this);
    },

    // grab the changes and fill any zone set to receive the values
    autoupdate: function (model) {
        var _this = this,
            changes = model.changedAttributes(),
            attrs = _.keys(changes);

        _.each(attrs, function (attr) {
            _this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));
        });
    },

    // render the complete template
    // should only happen when there really is a dramatic change to the view
    render: function () {
        var data, html;

        // build the data to render the template
        // this.collection.toJSON() with the cid added, in fact
        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {cid: model.cid});
        });

        html = template({children: data});
        this.$el.html(html);

        return this;
    }
});

Код будет немного отличаться для размещения модели вместо коллекции. Скрипт для игры с http://jsfiddle.net/nikoshr/cfcDX/

Ограничение манипуляций DOM

Делегирование рендеринга в subviews может быть дорогостоящим, их фрагменты HTML должны быть вставлены в DOM родителя. Посмотрите на этот тест jsperf, сравнивающий различные методы рендеринга

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

<script id="tpl-table" type="text/template">
    <table>
        <thead>
            <tr>
                <th>Row</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
        <% _(children).each(function(model) { %>
            <tr id='<%= model.cid %>'>
                <td><%= model.row %></td>
                <td><%= model.name %></td>
            </tr>
        <% }); %>
        </tbody>
     </table>
</script>
var ItemView = Backbone.View.extend({
});

var ListView = Backbone.View.extend({
    render: function () {
        var data, html, $table, template = this.options.template;

        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {
                cid: model.cid
            });
        });

        html = this.options.template({
            children: data
        });

        $table = $(html);

        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
        });

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});


var view = new ListView({
    template: _.template($('#tpl-table').html()),
    collection: new Backbone.Collection(data)
});

http://jsfiddle.net/nikoshr/UeefE/

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

В соответствующей заметке не работайте над узлами, прикрепленными к DOM, это приведет к ненужным перерасчетам. Либо создайте новый DOM, либо отсоедините node, прежде чем манипулировать им.

Сквотирование зомби

Дерик Бэйли написал прекрасную статью на тему уничтожение представлений зомби

В принципе, вы должны помнить, что когда вы отбрасываете представление, вы должны развязать все прослушиватели и выполнить любую дополнительную очистку, например, уничтожить экземпляры плагина jQuery. То, что я использую, представляет собой комбинацию методов, аналогичных тем, что использует Дерик в Backbone.Marionette:

var BaseView = Backbone.View.extend({

    initialize: function () {
        // list of subviews
        this.views = [];
    },

    // handle the subviews
    // override to destroy jQuery plugin instances
    unstage: function () {
        if (!this.views) {
            return;
        }

        var i, l = this.views.length;

        for (i = 0; i < l; i = i + 1) {
            this.views[i].destroy();
        }
        this.views = [];
    },

    // override to setup jQuery plugin instances
    stage: function () {
    },

    // destroy the view
    destroy: function () {
        this.unstage();
        this.remove();
        this.off();

        if (this.collection) {
            this.collection.off(null, null, this);
        }
        if (this.model) {
            this.model.off(null, null, this);
        }
    }
});

Обновление моего предыдущего примера, чтобы дать строкам перетаскиваемое поведение будет выглядеть так:

var ItemView = BaseView.extend({
    stage: function () {
        this.$el.draggable({
            revert: "invalid",
            helper: "clone"
        });
    },

    unstage: function () {
        this.$el.draggable('destroy');
        BaseView.prototype.unstage.call(this);
    }
});

var ListView = BaseView.extend({

    render: function () {
       //same as before

        this.unstage();
        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
            subview.stage();
            this.views.push(subview);
        }, this);
        this.stage();

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});

http://jsfiddle.net/nikoshr/yL7g6/

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

NB: извините за код JS, я недостаточно хорошо знаком с Coffeescript для предоставления точных фрагментов.

Ответ 2

Хорошо, в порядке..:)

  • Загрузка...

Если вы хотите проверить данные, хранящиеся на сервере, хорошая практика делает это на стороне сервера. Если проверка на сервере будет безуспешной, сервер должен отправить не 200 HTTP-код, поэтому сохранить метод Backbone.Model приведет к ошибке.

Другая сторона, для базы данных валидации не реализован метод validate. Я предполагаю, что это правильный выбор, чтобы реализовать и использовать его. Но имейте в виду, что validate вызывается перед установкой и сохранением, и если validate возвращает ошибку, установка и сохранение не будут продолжены, и атрибуты модели не будут изменены. Неверные проверки инициируют событие с ошибкой.

Другой способ, когда мы вызываем тихий набор (с параметром {silent: true}), мы должны вызвать метод isValid вручную для проверки данных.

  1. Слишком много вещей слишком много делают.

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

Отлично, но H2 в начале формы имеет то же имя, что и в input - вам нужно его обновить. О, и вам нужно обновить имя на список сбоку.

вы можете использовать метод JQuery on для реализации обратного вызова, который отправляет значение для отображения. Пример:

//Container view
init: function() {
    this.collection = new Backbone.Collection({
        url: 'http://mybestpage.com/collection'
    });
    this.collection.bind('change', this.render, this);
    this.collection.fetch();
},
render: function() {

    _.each(this.collection.models, function(model) {
         var newView = new myItemView({
              model: model,
              name: 'view' + model.id
         });
         this.$('#my-collection').append(newView.render().$el);
         view.on('viewEdit', this.displayValue);
    }, this);
},
...
displayValue: function(value) {
    //method 1
    this.displayView.setText(value); //we can create little inner view before, 
                                     //for text displaying. Сonvenient at times.
    this.displayView.render();
    //method 2
    $(this.el).find('#display').html(value);
}

//View from collection
myItemView = Backbone.View.extend({
events: {
    'click #edit': 'edit'
},
init: function(options) {
    this.name = options.name;
},
...
edit: function() {
    this.trigger('viewEdit', this.name, this);
}

OH, и список сортируется по именам!

Вы можете использовать sort метод для базовых коллекций. Но (!) Вызов сортировки вызывает событие коллекции reset. Пройдите {silent: true}, чтобы этого избежать. Как

Вот еще один пример: вы хотите создать новый элемент в Коллекция...

Когда мы нажимаем кнопку "Создать", нам нужно создать новую модель, но только тогда, когда метод .save() приведет к успеху, мы должны подтолкнуть эту модель к коллекции. В другом случае мы должны отобразить сообщение об ошибке. Конечно, у нас нет причин добавлять новую модель в нашу коллекцию, пока она не будет проверена и сохранена на сервере.

  1. Он подматривает весь путь вниз... subviews становятся похожими на зомби или не отвечают.

когда вы (или любая модель) вызываете метод render, все элементы внутри него будут воссозданы. Поэтому в случае, если у вас есть subviews, вы должны вызвать subView.delegateEvents(subView.events); для всех подзаголовков; Вероятно, этот метод - небольшой трюк, но он работает.

  1. Мерцающий..

Использование миниатюр для больших и средних изображений будет минимизировать мерцание во многих случаях. Другой способ: вы можете разделить рендеринг изображения на изображения и другой контент.

Пример:

var smartView = Backbone.View.extend({
  initialize: function(){
    this.model.on( "imageUpdate", this.imageUpdate, this );
    this.model.on( "contentUpdate", this.contentUpdate, this );
  },

  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  },

  imageUpdate: function(){
    this.$el.find('#image').attr('src', this.model.get('imageUrl'));
  },
  contentUpdate: function(){
    this.$el.find('#content').html(this.model.get('content'));
  }
})

Я надеюсь, что это поможет кому угодно. Извините за ошибки грамматики, если они есть:)

Ответ 3

Загрузка...

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

Мой любимый способ загрузки с помощью Backbone-relational. Если я организую свое приложение иерархически. Рассмотрим это:

Organization model
|--> Event model
|--> News model
   |--> Comment model

Поэтому, когда пользователь просматривает organization, я могу загружать эту организацию events и news. И когда пользователь просматривает статью news, я хочу загрузить эту статью comments.

Backbone-relational предоставляет отличный интерфейс для запроса связанных записей с сервера.

Слишком много вещей слишком много делают...

Backbone-relational тоже помогает! Backbone-relational предоставляет глобальное хранилище записей, которое оказывается очень полезным. Таким образом, вы можете передавать идентификаторы и извлекать одну и ту же модель в другом месте. Если вы обновите его в одном месте, его можно будет найти в другом.

a_model_instance = Model.findOrCreate({id: 1})

Другим инструментом здесь является Backbone.ModelBinder. Backbone.ModelBinder позволяет создавать ваши шаблоны и забывать о прикреплении к просмотру изменений. Поэтому в вашем примере сбора информации и отображения ее в заголовке просто скажите Backbone.ModelBinder, чтобы посмотреть ОБОИХ из этих элементов и на входе change, ваша модель будет обновлена, а на модели change вы будете обновлены, так что теперь заголовок будет обновлен.

Он подчиняется всему пути вниз... subviews становятся похожими на зомби или не отвечают...

Мне действительно нравится Backbone.Marionette. Он обрабатывает большую очистку для вас и добавляет обратный вызов onShow, который может быть полезен при временном удалении представлений из DOM.

Это также облегчает установку плагинов jQuery. Метод onShow вызывается после визуализации представления и добавляется в DOM, так что код плагина jQuery может нормально функционировать.

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

Мерцающие

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