Data binding for Backbone.js

Обзор существующих решений

Автор Константин Цареградский

Backbone.js - мощный фреймворк, но...

очень часто приходиться решать простейшие задачи не самым простым образом.

Задача

Есть форма редактирования имени и фамилии пользователя, из них генерируется полное имя.

 

Vanilla Backbone.js


First Name:

Last Name:

Full Name: <%- firstName %> <%- lastName %>


var FullNameView = Backbone.View.extend({
    template: _.template($("script[type='text/template']").html()),
    events: {
        "change #firstName": "updateFirstName",
        "change #lastName": "updateLastName"
    },
    initialize: function () {
        this.listenTo(this.model, "change:firstName change:lastName", this.render);
    },
    render: function () {
        this.$el.html(this.template(model.toJSON()));
        return this;
    },
    updateFirstName: function () {
        this.model.set("firstName", this.$("#firstName").val());
    },
    updateLastName: function () {
        this.model.set("lastName", this.$("#lastName").val());
    }
});

var model = new Backbone.Model({firstName: "Luke", lastName: "Skywalker"}),
    view = new FullNameView({model: model, el: $("#container")});

view.render();

Vanilla Backbone.js

+-
    • Много кода для такой простой задачи
    • Ре-рендеринг всей вьюхи каждый раз
    • В перспективе - причина создания собственного велосипеда

    Есть решение проще!

    Data Binding

    Data Binding

    Процесс связывания пользовательского интерфейса приложения с бизнес-логикой.

    Эта концепция активно используется в таких популярных фреймворках как AngularJS, KnockoutJS и многих других.

    KnockoutJS

    
    

    First Name:

    Last Name:

    Full Name:

    
    var ViewModel = function(first, last) {
        this.firstName = ko.observable(first);
        this.lastName = ko.observable(last);
    };
    
    ko.applyBindings(new ViewModel("Luke", "Skywalker"));

    AngularJS

    
    

    First Name:

    Last Name:

    Full Name: {{firstName}} {{lastName}}

    
    function ctrlName($scope) {
        $scope.firstName = "Luke";
        $scope.lastName = "Skywalker";
    }

    К сожалению, из коробки Backbone не предоставляет такой функциональности. Однако существует множество сторонних решений:

    Backbone.ModelBinding

    
    

    First Name:

    Last Name:

    Full Name:

    
    var ModelBindingView = Backbone.View.extend({
        template: $("script[type='text/template']").html(),
    
        render: function() {
            this.$el.html(this.template);
            Backbone.ModelBinding.bind(this);
            return this;
        }
    });
    
    var model = new Backbone.Model({ firstName: "Luke", lastName: "Skywalker" }),
        view = new ModelBindingView({ model: model, el: $("#container") });
    
    view.render();

    Backbone.ModelBinding

    +-
    • По сути, первый data binding для Backbone. Интересен для истории.
    • Ограниченое число binding'ов
    • Разработка остановлена
    • Новые версии Backbone официально не поддерживаются

    Backbone.ModelBinder

    
    

    First Name:

    Last Name:

    Full Name:

    
    var ModelBinderView = Backbone.View.extend({
        template: $("script[type='text/template']").html(),
    
        initialize: function() {
            this.modelBinder = new Backbone.ModelBinder();
        },
    
        render: function() {
            this.$el.html(this.template);
            this.modelBinder.bind(this.model, this.el);
            return this;
        }
    });
    
    var model = new Backbone.Model({ firstName: "Luke", lastName: "Skywalker" }),
        view = new ModelBinderView({ model: model, el: $("#container") });
    
    view.render();

    Backbone.ModelBinder

    +-
    • Гибкость в настройке
    • Есть возможность привязаться к любому аттрибуту DOM элемента
    • Поддержка конвертеров и форматтеров
    • Поддержка коллекций с помощью Backbone.CollectionBinder
    • Сложный в освоении из-за большого количества настроек и параметров
    • Проект не активен, minor версия 1.0.6 от 5 ноября 2014, до этого коммиты были только в 2013

    Backbone.DataBinding

    
    

    First Name:

    Last Name:

    Full Name:

    
    var DataBindingView = Backbone.View.extend({
        template: $("script[type='text/template']").html(),
    
        render: function() {
            this.$el.html(this.template);
            return this;
        }
    });
    
    var model = new Backbone.Model({ firstName: "Luke", lastName: "Skywalker" }),
        view = new DataBindingView({ model: model, el: $("#container") }),
        modelBinder = new Backbone.ModelBinder(view, model);
    
    modelBinder.watch('value: firstName', { selector: '[name="firstName"]' });
    modelBinder.watch('text: firstName', { selector: '#firstName' });
    modelBinder.watch('value: lastName', { selector: '[name="lastName"]' });
    modelBinder.watch('text: lastName', { selector: '#lastName' });
    
    view.render();

    Backbone.DataBinding

    +-
    • Есть возможность связать модель и представление извне, не трогая оригинальный код
    • Поддержка коллекций
    • Автор из Украины
    • Отсутсвтие какого-либо community - 2 контрибьютора, 0 форков
    • Много ручной работы
    • Последний коммит - год назад

    Backbone.Stickit

    
    

    First Name:

    Last Name:

    Full Name:

    
    var StickitView = Backbone.View.extend({
        template: $("script[type='text/template']").html(),
        bindings: {
            '[name=firstName]': 'firstName',
            '[name=lastName]': 'lastName',
            '#fullName': {
                observe: ['firstName', 'lastName'],
                onGet: function(values) { return values[0] + ' ' + values[1]; }
            }
        },
        render: function() {
            this.$el.html(this.template);
            this.stickit();
            return this;
        }
    });
    
    var model = new Backbone.Model({ firstName: "Luke", lastName: "Skywalker" }),
        view = new StickitView({ model: model, el: $("#container") });
    
    view.render();

    Backbone.Stickit

    +-
    • Активно развивается, последний коммит - в начале ноября 2014
    • Большое community, серьезный автор - NY Times
    • Поддержка contenteditable DOM элементов
    • Computed свойства
    • Огромное количество настроек
    • Возможность форматирования и конвертирования
    • Data binding описывается в JavaScript коде, нет привязки к темплейтам
    • Доволен сложен в освоении и, как следствие, возможно, overkill для простых задач

    Epoxy.js

    
    

    First Name:

    Last Name:

    Full Name:

    
    var EpoxyView = Backbone.Epoxy.View.extend({
        template: $("script[type='text/template']").html(),
    
        computeds: {
            fullNameDisplay: function() {
                return this.getBinding("firstName") + " " + this.getBinding("lastName");
            }
        },
    
        render: function() {
            this.$el.html(this.template);
            this.applyBindings();
            return this;
        }
    });
    
    var model = new Backbone.Model({ firstName: "Luke", lastName: "Skywalker" }),
        view = new EpoxyView({ model: model, el: $("#container") });
    
    view.render();

    Epoxy.js

    +-
    • Активно развивается, последний коммит - 7 дней назад
    • Computed свойства как в модели, так и во view
    • Декларировать binding'и можно как во view, так и в темплейтах
    • Возможность форматирования и конвертирования
    • Поддержка коллекций
    • Простота создания кастомных binding'ов
    • Нужно обязательно наследоваться от Backbone.Epoxy.View. Если нужна поддержка computed свойств в модели то нужно наследоваться от Backbone.Epoxy.Model

    Rivets.js

    
    

    First Name:

    Last Name:

    Full Name:

    
    var RivetsView = Backbone.View.extend({
        template: $("script[type='text/template']").html(),
        fullName: function() {
            return this.model.get("firstName") + " " + this.model.get("lastName");
        },
        render: function () {
            this.$el.html(this.template);
            rivets.bind(this.$el, { model: this.model, view: this });
            return this;
        }
    });
    
    var model = new Backbone.Model({firstName: "Luke", lastName: "Skywalker"}),
        view = new RivetsView({model: model, el: $("#container")});
    
    view.render();

    Rivets.js

    
    rivets.adapters[':'] = {
        observe: function(obj, keypath, callback) {
            obj.on('change:' + keypath, callback)
        },
        unobserve: function(obj, keypath, callback) {
            obj.off('change:' + keypath, callback)
        },
        get: function(obj, keypath) {
            return obj.get(keypath)
        },
        set: function(obj, keypath, value) {
            obj.set(keypath, value)
        }
    };

    Rivets.js

    + -
    • Активно развивается, последний коммит - 5 дней назад
    • Может использоваться как с event-driven моделями других JavaScript фреймворками (например, Stapes.js), так и с обычными JavaScript объектами
    • Computed свойства
    • Возможность форматирования и конвертирования
    • Поддержка коллекций
    • Прост в освоении
    • Для поддержки моделей и коллекций из определенного фреймворка необходимо написать адаптер
    • Дополнительная прослойка Sightglass для поддержки Observable
    • Все кастомные биндинги можно объявлять только глобально
    • IE8 не поддерживается

    Knockback.js

    
    

    First Name:

    Last Name:

    Full Name:

    
    var KnockbackViewModel = function(model) {
        this.firstName = kb.observable(model, 'firstName');
        this.lastName = kb.observable(model, 'lastName');
        this.fullName = ko.computed((function() {
            return "" + (this.firstName()) + " " + (this.lastName());
        }), this);
    };
    
    var model = new Backbone.Model({ firstName: "Luke", lastName: "Skywalker" }),
        viewModel = new KnockbackViewModel(model);
    
    $('#container').html($("script[type='text/template']").html());
    ko.applyBindings(viewModel, $('#container')[0]);

    Knockback.js

    +-
    • Активно развивается, последний коммит - 15 дней назад
    • Умеет все, что умеет Knockout.js
    • Поддержка различных Backbone.js плагинов для nested моделей (BackboneORM, Backbone-Relational etc.)
    • От самого Backbone.js остаются только Model и Router
    • Зависимость сразу от двух больших фреймворков

    Выводы

    • Если хорошо знаете/нравиться Knockout.js, но нужны Backbone модели, и вы не боитесь зависимости сразу от двух больших фреймворков - ваш выбор Knockback.js
    • Если нет окончательной уверенности, что Backbone - ваш финальный выбор, то стоит обратить внимание на Rivets.js
    • Во всех остальных случаях - Epoxy.js или Backbone.Stickit

    Вопросы?

    Эта презентация доступна по адресу - http://tsareg.github.io/Data-binding-for-Backbone.js/, sources - https://github.com/tsareg/Data-binding-for-Backbone.js

     

    Рабочие примеры находятся тут - http://tsareg.github.io/Data-binding-for-Backbone.js/samples/, sources - https://github.com/tsareg/Data-binding-for-Backbone.js/tree/master/samples

     

    Презентация создана с помощью Reveal.js

    Спасибо за внимание