Як відобразити та додати підпогляди у Backbone.js


133

У мене є вкладений режим перегляду, який може дещо заглибитися в моє додаток. Існує маса способів ініціалізувати, відображати та додавати підпогляди, але мені цікаво, що таке звичайна практика.

Ось пара, про яку я думав:

initialize : function () {

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

    this.$el.html(this.template());

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Плюси: Вам не потрібно турбуватися про підтримку правильного замовлення DOM із додаванням. Погляди ініціалізуються на ранніх стадіях, тому в функції візуалізації не так багато, щоб зробити все одночасно.

Мінуси: Ви змушені повторно делегуватиEvents (), що може дорого коштувати? Функція візуалізації батьківського перегляду захаращена всіма рендерингами субпрогляду, які мають відбутися? Ви не маєте можливості встановлювати tagNameелементи, тому шаблон повинен підтримувати правильні тегиNames.

Інший спосіб:

initialize : function () {

},

render : function () {

    this.$el.empty();

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});

    this.$el.append(this.subView1.render().el, this.subView2.render().el);
}

Плюси: не потрібно повторно делегувати події. Вам не потрібен шаблон, який містить просто порожні заповнювачі, і ваш тегName повернеться до визначення перегляду.

Мінуси: Тепер вам потрібно обов’язково додати речі в потрібному порядку. Відображення батьківського подання все ще захаращене поданням підвідного перегляду.

З onRenderподією:

initialize : function () {
    this.on('render', this.onRender);
    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

    this.$el.html(this.template);

    //other stuff

    return this.trigger('render');
},

onRender : function () {

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Плюси: логіка перегляду тепер відокремлена від render()методу подання .

З onRenderподією:

initialize : function () {
    this.on('render', this.onRender);
},

render : function () {

    this.$el.html(this.template);

    //other stuff

    return this.trigger('render');
},

onRender : function () {
    this.subView1 = new Subview();
    this.subView2 = new Subview();
    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Я наче змішував і поєднував безліч різних практик у всіх цих прикладах (так шкода цього), але які з них ви б зберегли чи додали? а що б ти не робив?

Короткий огляд практик:

  • Ігнорувати підпогляди в initializeчи в render?
  • Виконувати всю логіку візуалізації під-виду в renderчи в onRender?
  • Використовувати setElementабо append/appendTo?

Я був би обережний щодо нового без видалення, у вас там просочилася пам'ять.
vimdude

1
Не хвилюйтесь, у мене є closeметод і спосіб, onCloseякий очищає дітей, але мені просто цікаво, як інстанціювати і робити їх в першу чергу.
Ян Шторм Тейлор

3
@abdelsaid: У JavaScript GC обробляє операції з розміщенням пам'яті. deleteв JS не є тим самим, що і deleteвід C ++. Це дуже погано назване ключове слово, якщо ви запитаєте мене.
Майк Бейлі

@MikeBantegui отримав це, але це те саме, що і в Java, за винятком того, що в JS для звільнення пам'яті вам просто потрібно призначити null. Щоб уточнити, що я маю на увазі, спробуйте створити цикл з новим об'єктом всередині і контролювати пам'ять. Звичайно, GC дістанеться до нього, але ви втратите пам’ять, перш ніж він потрапить до нього. У цьому випадку Візуалізація, яку можна викликати багато разів.
vimdude

3
Я початківець розробник хребта. Чи може хто-небудь пояснити, чому приклад 1 змушує нас повторно делегувати події? (Або я повинен це задати у власному питанні?) Спасибі
pilau

Відповіді:


58

Я зазвичай бачив / використовував пару різних рішень:

Рішення 1

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.$el.append(this.inner.$el);
        this.inner.render();
    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);
        this.delegateEvents();
    }
});

Це схоже на ваш перший приклад, з кількома змінами:

  1. Має значення порядок внесення додаткових елементів
  2. Зовнішній вигляд не містить елементів html, які слід встановити у внутрішніх поданнях (тобто ви все ще можете вказати tagName у внутрішньому вікні)
  3. render()називається ПІСЛЯ, коли елемент внутрішнього перегляду був розміщений у DOM, що корисно, якщо render()метод вашого внутрішнього перегляду розміщує / розміщує себе на сторінці на основі положення / розміру інших елементів (що, на мій досвід, є звичайним випадком використання)

Рішення 2

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.inner = new InnerView();
        this.$el.append(this.inner.$el);
    }
});

var InnerView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

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

Рішення 2 може виглядати більш чистим, але воно спричинило деякі дивні речі в моєму досвіді і негативно вплинуло на продуктивність.

Зазвичай я використовую Рішення 1 з кількох причин:

  1. Багато моїх поглядів покладаються на те, що вже перебуваю в DOM у своїх render() методі
  2. Коли зовнішній вигляд буде відтворений, представлення даних не потрібно повторно ініціалізувати, що може повторно ініціалізувати витік пам'яті, а також викликати нечіткі проблеми з існуючими прив’язками

Майте на увазі, що якщо ви ініціалізуєте new View()кожен раз, коли render()викликається, ця ініціалізація delegateEvents()все одно зателефонує . Отже, це не обов'язково повинно бути "кон", як ви висловили.


1
Жодне з цих рішень не опрацьовує дерево перегляду під назвою View.remove, що може бути життєво важливим при здійсненні спеціального очищення у режимі перегляду, що інакше перешкоджатиме збору сміття
Домінік

31

Це багаторічна проблема з хребтом, і, на моєму досвіді, насправді немає задоволеної відповіді на це питання. Я поділяю ваше розчарування, тим більше, що настільки мало вказівок, незважаючи на те, наскільки поширений цей випадок використання. Однак, я зазвичай йду з чимось схожим на ваш другий приклад.

Перш за все, я б відмовив від руки все, що вимагає від вас повторного делегування подій. Модель перегляду Backbone, орієнтована на події, є одним з найважливіших компонентів, і втратити цю функціональність просто тому, що ваша програма нетривіальна, залишить поганий смак у роті будь-якого програміста. Тож нуля номер один.

Щодо вашого третього прикладу, я думаю, що це лише кінцевий крок навколо звичайної практики надання та не додає великого значення. Можливо, якщо ви робите фактичну активізацію події (тобто не надуманої onRenderподії " ", варто було б просто пов'язати ці події із renderсобою. Якщо знайдешrender виявитеся непростими і складними, у вас занадто мало підзаписів.

Повернімось до вашого другого прикладу, який, мабуть, менший із трьох злих. Ось приклад коду, знятого з " Рецепти із основою" , знайдений на сторінці 42 мого PDF-видання:

...
render: function() {
    $(this.el).html(this.template());
    this.addAll();
    return this;
},
  addAll: function() {
    this.collection.each(this.addOne);
},
  addOne: function(model) {
    view = new Views.Appointment({model: model});
    view.render();
    $(this.el).append(view.el);
    model.bind('remove', view.remove);
}

Це лише дещо складніші налаштування, ніж ваш другий приклад: вони задають набір функцій addAllтаaddOne , що робити брудну роботу. Я вважаю, що такий підхід є працездатним (і я його неодмінно використовую); але це все ще залишає химерний післясмак. (Вибачте всі ці метафори язика.)

До вашого питання про додавання в правильному порядку: якщо ви суворо додаєте, впевнено, це обмеження. Але обов’язково врахуйте всі можливі схеми шаблонування. Можливо, вам дійсно сподобається елемент-заповнювач (наприклад, порожній divабо ul), що ви можете потім replaceWithстворити новий (DOM) елемент, який містить відповідні підгляди. Звернення не є єдиним рішенням, і ви, звичайно, можете обійти проблему з замовленням, якщо вам це дуже важливо, але я б подумав, що у вас є проблема з дизайном, якщо вона вас відключає. Пам'ятайте, що в субпреглядах можуть бути підзагляди, і вони повинні, якщо це доречно. Таким чином, у вас є досить деревоподібна структура, що є досить приємним: кожен підпогляд додає всі свої підгляди для того, щоб батьківський вид додав інший тощо.

На жаль, рішення №2 - це, мабуть, найкраще, на що ви можете сподіватися для використання нестандартної магістралі. Якщо ви зацікавлені у перегляді сторонніх бібліотек, одна з них, яку я розглядав (але насправді ще не мав часу з нею грати), є Backbone.LayoutManager , який, здається, має більш здоровий метод додавання підпрезентацій. Однак навіть у них були останні дискусії щодо подібних питань.


4
Передостанній рядок - model.bind('remove', view.remove);- чи не варто ви просто робити це у функції ініціалізації призначення, щоб вони залишалися окремими?
atp

2
А як бути, коли представлення даних не може бути повторно встановлене кожного разу, коли це відображається з боку батьків, оскільки воно зберігає стан?
mor

Припиніть всю цю божевільність і просто використовуйте плагін Backbone.subviews !
Хоробрий Дейв

6

Дивно, що про це ще не було сказано, але я серйозно подумав би про використання Маріонет .

Він нав'язує трохи більше структури для Backbone додатків, включаючи типи зору конкретних ( ListView, ItemView, Regionі Layout), додаючи власні ControllerS і багато іншого.

Ось проект про Github та чудовий путівник Адді Османі в книзі Основи хребта, щоб розпочати роботу.


3
Це не дає відповіді на запитання.
Ceasar Bautista

2
@CeasarBautista Я не розглядаю, як використовувати Маріонетку для цього, але Маріонетка справді вирішує вищезазначену проблему
Дана Вудман

4

Я маю, як я вважаю, цілком комплексне рішення цієї проблеми. Це дозволяє моделі в колекції змінюватися і мати лише її представлення (а не всю колекцію). Він також обробляє видалення зомбі поглядів методами close ().

var SubView = Backbone.View.extend({
    // tagName: must be implemented
    // className: must be implemented
    // template: must be implemented

    initialize: function() {
        this.model.on("change", this.render, this);
        this.model.on("close", this.close, this);
    },

    render: function(options) {
        console.log("rendering subview for",this.model.get("name"));
        var defaultOptions = {};
        options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
        this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
        return this;
    },

    close: function() {
        console.log("closing subview for",this.model.get("name"));
        this.model.off("change", this.render, this);
        this.model.off("close", this.close, this);
        this.remove();
    }
});
var ViewCollection = Backbone.View.extend({
    // el: must be implemented
    // subViewClass: must be implemented

    initialize: function() {
        var self = this;
        self.collection.on("add", self.addSubView, self);
        self.collection.on("remove", self.removeSubView, self);
        self.collection.on("reset", self.reset, self);
        self.collection.on("closeAll", self.closeAll, self);
        self.collection.reset = function(models, options) {
            self.closeAll();
            Backbone.Collection.prototype.reset.call(this, models, options);
        };
        self.reset();
    },

    reset: function() {
        this.$el.empty();
        this.render();
    },

    render: function() {
        console.log("rendering viewcollection for",this.collection.models);
        var self = this;
        self.collection.each(function(model) {
            self.addSubView(model);
        });
        return self;
    },

    addSubView: function(model) {
        var sv = new this.subViewClass({model: model});
        this.$el.append(sv.render().el);
    },

    removeSubView: function(model) {
        model.trigger("close");
    },

    closeAll: function() {
        this.collection.each(function(model) {
            model.trigger("close");
        });
    }
});

Використання:

var PartView = SubView.extend({
    tagName: "tr",
    className: "part",
    template: _.template($("#part-row-template").html())
});

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});

2

Ознайомтеся з цим міксинсом для створення та відображення підвидів:

https://github.com/rotundasoftware/backbone.subviews

Це мінімалістичне рішення, яке вирішує багато питань, що обговорюються в цій темі, включаючи порядок надання, відсутність повторного делегування подій тощо. підвід) - інша тема. Найкраще загальне рішення, яке мені відомо в цьому випадку, є CollectionView в Маріонетті .


0

Мені не дуже подобається жодне з перерахованих вище рішень. Я віддаю перевагу цій конфігурації над кожним видом, що потребує роботи вручну в методі візуалізації.

  • views може бути функцією або об'єктом, що повертає об'єкт визначень перегляду
  • Коли викликається батько, .removeслід викликати .removeвкладених дітей із нижнього порядку вгору (аж до представлень суб-під-підрядів)
  • За замовчуванням батьківський вигляд передає власну модель та колекцію, але параметри можна додавати та змінювати.

Ось приклад:

views: {
    '.js-toolbar-left': CancelBtnView, // shorthand
    '.js-toolbar-right': {
        view: DoneBtnView,
        append: true
    },
    '.js-notification': {
        view: Notification.View,
        options: function() { // Options passed when instantiating
            return {
                message: this.state.get('notificationMessage'),
                state: 'information'
            };
        }
    }
}

0

Магістраль була навмисно побудована таким чином, що не було «загальної» практики щодо цього та багатьох інших питань. Це покликане бути максимально ненав'язливим. Теоретично вам навіть не доведеться використовувати шаблони з Backbone. Ви можете використовувати javascript / jquery у renderфункції перегляду, щоб вручну змінити всі дані у поданні. Щоб зробити це більш екстремальним, вам навіть не потрібна одна конкретна renderфункція. У вас може бути функція, renderFirstNameяка називається, яка оновлює перше ім’я в домі і renderLastNameяка оновлює прізвище в домі. Якщо ви скористалися таким підходом, це було б набагато краще з точки зору продуктивності, і вам більше ніколи не доведеться вручну делегувати події. Код також мав би повний сенс для того, хто його читає (хоча це був би довший / Messier-код).

Однак, як правило, немає недоліків у використанні шаблонів, а просто руйнування та відновлення всього перегляду, і це підпрограми для кожного виклику візуалізації, оскільки запитувачеві навіть не спало на думку робити щось інакше. Тож саме це і робить для більшості людей кожну ситуацію, в яку вони стикаються. І саме тому вигадливі рамки просто роблять це поведінкою за замовчуванням.


0

Ви також можете вводити відредаговані підгляди як змінні в основний шаблон як змінні.

спочатку надайте субпрегляди та перетворіть їх у HTML у такий спосіб:

var subview1 = $(subview1.render.el).html(); var subview2 = $(subview2.render.el).html();

(таким чином ви також могли б динамічно рядок об'єднати представлення, наприклад, subview1 + subview2коли використовується в циклі), а потім передати його до головного шаблону, який виглядає приблизно так: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

і введіть його нарешті так:

this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));

Щодо подій у межах підзаписів: вони, швидше за все, повинні бути зв'язані в батьківському (masterView) такому підході, а не в підпоглядах.


0

Мені подобається використовувати наступний підхід, який також обов'язково належним чином видаляє дитячі погляди. Ось приклад з книги Адді Османі.

Backbone.View.prototype.close = function() {
    if (this.onClose) {
        this.onClose();
    }
    this.remove(); };

NewView = Backbone.View.extend({
    initialize: function() {
       this.childViews = [];
    },
    renderChildren: function(item) {
        var itemView = new NewChildView({ model: item });
        $(this.el).prepend(itemView.render());
        this.childViews.push(itemView);
    },
    onClose: function() {
      _(this.childViews).each(function(view) {
        view.close();
      });
    } });

NewChildView = Backbone.View.extend({
    tagName: 'li',
    render: function() {
    } });

0

Не потрібно повторно делегувати події, оскільки це дорого. Дивись нижче:

    var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        // first detach subviews            
        this.inner.$el.detach(); 

        // now can set html without affecting subview element's events
        this.$el.html(template);

        // now render and attach subview OR can even replace placeholder 
        // elements in template with the rendered subview element
        this.$el.append(this.inner.render().el);

    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);            
    }
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.