Как то поздним вечерком мне пришла мысль изучить Backbone.js и привязать его к уже написанному на jQuery сервису. Сервис уже серьёзно расширился и меня достало это нагромождение обработчиков кликов, запросов и логики. Поэтому, я как усердный школьник полез в официальную документацию. Но либо я тупой, либо мой английский меня подкачал, либо то и другое вместе, но я не черта не понял. Я прочитал уже второй раз, внимательно, и для особо одарённых мест использовал google translate. Прочитал также и пример ToDo List. Всё показалось понятно, ровно до той поры пока я не стал писать. После чего я взял всё что нашел по этой библиотеке, как на английском так и переводы . Прочтя кипу документации я решил, что сейчас вроде всё понял.Я напрягся, но… Не вышел каменный цветок у мастера Данилы, т.е. вышло, но это явно был не цветок, и камень как то неправильно пах. Тогда, как прилежный ученик, я решил написать «Hello, KittyWorld» с нуля. Попутно комментируя и сохраняя шаги в hg, у меня получилось введение в backbone.js framework для таких как я, особо одарённых.

Задача.

Выберем простую задачу. Написать Hello, World? Слишком просто, также как и написать Hello, <имярёк>. Может напишем клиент GTD с авторизацией и оффлайн хранилищем? Такое уже есть и оно не помогает понять нашу “хребтовую кость”.Сделаем проще. Создадим страницу с 3 состояниями. В первом состоянии человек вводит имя пользователя, во втором состоянии его поздравляют, если введённое имя найдено, в третьем состоянии огорчают, если имя не найдено. По-моему, данная задача учебней и проще некуда, да и в общем позволит посмотреть и проверить почти всё что есть в backbone.

Все шаги сохраним через mercurial . Поэтому, читая какой либо шаг вы можете распаковать zip архив (+ dropbox , если на народе удалят), зайти в каталог и перейти на нужную ревизию при помощи команды

hg update --rev <номер ревизии>

После чего посмотреть на код и понять то, что вам не понятно 🙂

Шаг 0. Структура и шаблон (rev 0 )

структура
Структуру будем использовать академическую, такую как на картинке. Сознаюсь, я не знаю кто автор этой священной пули, у кого я слизал эту корову, но использую во всех заготовках. Один файл index.html. В папке css лежат стили, в папке i лежат картинки, в js — скрипты. Одновременно закинем в скрипты jquery, underscore и backbone к скриптам.
Шаблон html — пустая страничка. Т.е. страничка с пустым body и подключенными скриптами и стилем.
Т.е. как вы видите, в отличии от некоторых современных javascript mvc framework проект не требует особого подготовления, поэтому уже существующий проект может быть “переписан” на backbone.

Шаг 1. Начальная вёрстка (rev 1 )

Наша страница, в соответствии с задачей, должна иметь 3 состояния: ввод имени пользователя, состояние при удачном сравнении, состояние при неудачном сравнении. Для начала сверстаем 3 дива, каждому состоянию по своему месту.

<div id="start">
    <div>
        <label for="username">Имя пользователя: </label>
        <input type="text" id="username" />
    </div>
    <div>
        <input type="button" value="Проверить" />
    </div>
</div>
<div id="error">
    Ошибка такой пользователь не найден.
</div>
<div id="success">
    Пользователь найден.
</div>

Блоки #error и #success скроем от глаз подальше при помощи CSS.

#error, #success
{
    ...
    display: none;
}

На этом шаге мы полностью подготовили всё для внедрения backbone. Эти шаги идентичные для многих реализаций одностраничных сайтов.

Шаг 2. Внедряем Router (rev 2 )

Раньше до 0.5.0 этот класс звали Controller. Его назначение обработка хеш навигации в приложении. Т.е. он никогда не был в полном понимании контроллером, просто хеш навигация это контроллер приложения. Видно логика разработчиков взяла верх и теперь мы имеем класс Router.
Что такое location.hash для чего он используется, и как его использовать правильно вы можете прочитать на хабре (тут тут или тут ).

Для начала, на время создадим импровизированное меню в index.html

<div id="menu"> <!-- Блок меню -->
    <ul>
        <li><a href="#!/">Start</a></li>
        <li><a href="#!/success">Success</a></li>
        <li><a href="#!/error">Error</a></li>
    </ul>
</div>

А потом легким движением руки добавляем работу роутинга в пример:

var Controller = Backbone.Router.extend({
    routes: {
        "": "start", // Пустой hash-тэг
        "!/": "start", // Начальная страница
        "!/success": "success", // Блок удачи
        "!/error": "error" // Блок ошибки
    },

    start: function () {
        $(".block").hide(); // Прячем все блоки
        $("#start").show(); // Показываем нужный
    },

    success: function () {
        $(".block").hide();
        $("#success").show();
    },

    error: function () {
        $(".block").hide();
        $("#error").show();
    }
});

var controller = new Controller(); // Создаём контроллер

Backbone.history.start();  // Запускаем HTML5 History push    

Вот таким простым кодом мы создали простейший tab-орентированный сайт с возможностью делать закладки на страницы.

Шаг 3. Простейшее View (rev 3 )

View в backbone это смесь контроллера и View из стандартной MVC модели.Можно сказать проще, View тут это widget/component на странице, который умеет себя отображать, реагировать на события и создавать события. Будем наедятся, что создатели задумаются и переименуют View в Widget (или component), как ранее они сделали с Router.
У нас есть сформировавшийся widget проверки имени пользователя, это блок start.Давайте сделаем так, что если введено имя “test”, то перейдём на хеш тег #!/success, который покажет блок success. А если введено что-то иное, то перейдём на хеш тег #!/error, который покажет, соответственно, блок error.Кстати, заодним уберем меню, оно нам больше не понадобится.

var Start = Backbone.View.extend({
    el: $("#start"), // DOM элемент widget'а
    events: {
        "click input:button": "check" // Обработчик клика на кнопке "Проверить"
    },
    check: function () {
        if (this.el.find("input:text").val() == "test") // Проверка текста
            controller.navigate("success", true); // переход на страницу success
        else
            controller.navigate("error", true); // переход на страницу error
    }
});

var start = new Start();

Кстати, вы заметили, что после того, как вы перешли на страницу результата, вы можете спокойно вернуться назад нажав кнопку Backspace? Это магия хеш навигации.

Ремарка. JQuery way (rev 4 )

Вы заметили сколько кода мы уже написали? Я так и думаю, многие уже из тех кто дочитал до этого предложения получат коньяк воскликнули: “На jQuery это делается быстрее и проще”. Не спорю. Код который надо написать на начальную вёрстку очень прост:

    $("#start input:button").click(function () { // Обработчик нажатия кнопки
        var username = $("#username").val(); // Получаем значение введенное пользователем
        $("#start").hide(); // Скрываем основной экран
        if (username == "test")
            $("#success").show(); // Показываем успех
        else
            $("#error").show(); // Показываем ошибку
    });

Но… Данный код не поддерживает хеш навигацию, плохо расширяется и очень плохо поддерживается.
Не для кого не секрет, что программист в своей работе занимается созданием новых приложений всего 20% времени. 80% же своего времени он состыкует модули, занимается исправлением ошибок и расширяет функционал уже созданных проектов. А поддержка jQuery лапши может очень дорого стоить. Очевидный способ избежать геморроя на пальцах, это заняться декомпозицией проектов, для чего в основном изобретают велосипеды. Backbone уже готовый велосипед. Зачем придумывать что то новое, когда за вас это сделал добрый дядя?

Шаг 4. Работа со View через Template (rev 5 )

View было бы не View, а контролером, если бы не умела себя отображать. В backbone нет своего механизма для этого. Смешно? Нисколько… Его назначение не давать инструмент для создания приложения, а дать шаблон, используя который можно было бы создать максимально поддерживаемую систему. Поэтому в backbone можно использовать различные template движки. Например, встроенный в underscore.js движок от John Resig. Или подключить Microsoft Template. А если хитрожопно извернуться то можно реализовать всё через Knockout.js (хотя меня напрягает его свалка логики и шаблонов)
Мы не будем напрягаться и просто используем _.template из underscore.js для реализации своих идей. Для этого создадим один пустой блок на странице, а все “наполнители” вынесем в шаблоны. Соответственно изменятся и стили страницы.

<div id="block">
</div>

<!-- Блок ввода имени пользователя -->
<script type="text/template" id="start"> 
    <div> 
        <div>
            <label for="username">Имя пользователя: </label>
            <input type="text" id="username" />
        </div>
        <div>
            <input type="button" value="Проверить" />
        </div>
    </div>
</script>

<!-- Блок ошибки -->
<script type="text/template" id="error">
    <div>
        Ошибка. Пользователь  <%= username %> не найден.
        <a href="#!/">Go back</a>
    </div>
</script>

<!-- Блок удачи -->
<script type="text/template" id="success">
    <div>
        Пользователь <%= username %> найден.
        <a href="#!/">Go back</a>
    </div>        
</script>

Для того чтобы показать динамику, мы добавили в шаблоны результатов имя пользователя.
Хранить имя пользователя и передавать его в шаблон мы будем в переменной AppState

var AppState = {
    username: ""
}

Напишем View для каждого шаблона.

var Views = { };    

var Start = Backbone.View.extend({
    el: $("#block"), // DOM элемент widget'а

    template: _.template($('#start').html()),

    events: {
        "click input:button": "check" // Обработчик клика на кнопке "Проверить"
    },

    check: function () {
        AppState.username = this.el.find("input:text").val(); // Сохранение имени пользователя
        if (AppState.username == "test") // Проверка имени пользователя
            controller.navigate("success", true); // переход на страницу success
        else
            controller.navigate("error", true); // переход на страницу error
    },

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

var Success = Backbone.View.extend({
    el: $("#block"), // DOM элемент widget'а

    template: _.template($('#success').html()),

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

var Error = Backbone.View.extend({
    el: $("#block"), // DOM элемент widget'а

    template: _.template($('#error').html()),

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

Views = { 
            start: new Start(),
            success: new Success(),
            error: new Error()
        };

Замечание. У нас 3 View сылаются на один и тот же DOM элемент. В реальности такого быть не должно. Логически, это должен быть один widget. Я сознательно неверно спроектировал данный шаг, для того чтобы показать возможность работы нескольких View. Позднее я покажу как избежать данный прокол.

Контроллер тоже претерпит небольшие изменения

var Controller = Backbone.Router.extend({
    routes: {
        "": "start", // Пустой hash-тэг
        "!/": "start", // Начальная страница
        "!/success": "success", // Блок удачи
        "!/error": "error" // Блок ошибки
    },

    start: function () {
        if (Views.start != null) {
            Views.start.render();
        }
    },

    success: function () {
        if (Views.success != null) {
            Views.success.render();
        }
    },

    error: function () {
        if (Views.error != null) {
            Views.error.render();
        }
    }
});

Вот таким образом мы добавили динамики к нашему приложению.

Шаг 5. Проверка на несколько пользователей (rev 6 )

Самым простым способом проверки не только на test, но и на других пользователей, это проверка на нахождения имени в массиве пользователей.
Создадим массив Family, в который и забьем все имена.

var Family = ["Саша", "Юля", "Елизар"]; // Моя семья

А проверку сделаем в коде вьюшки Start. Т.к. underscore уже включено в приложение, сделаем через _.detect

...
if (_.detect(Family, function (elem) { return elem == AppState.username })) // Проверка имени пользователя
...

Какие проблемы есть у данного решения? Основная проблема в том, что если завтра нам нужно будет сменить физическое расположение массива пользователей (сервер, localstore и т.д.), то нам придётся менять логику работы View. Т.е. View настолько завязана на метод доступа к данным, что придётся менять его код при малейшем чихе.

Ремарка 2. jQuery way. Продолжение (rev 7 )

А намного ли кода нам пришлосб добавить в jQuery код чтобы поддерживать нескольких пользователей? Очень мало:

$(function () {

    var Family = ["Саша", "Юля", "Елизар"];

    $("#start input:button").click(function () { // Обработчик нажатия кнопки
        var username = $("#username").val(); // Получаем значение введенное пользователем
        $("span.username").text(username);
        $("#start").hide(); // Скрываем основной экран
        $("#" + ($.inArray(username, Family) != -1 ? "success" : "error")).show();
    });

    $("#error a, #success a").click(function () {
        $(".block").hide();
        $("#start").show();
    });

});

Ну и соответственно подправить вёрстку макета:

        ...        
        <div id="error"> <!-- Блок ошибки -->
            Ошибка. Пользователь <span></span> не найден.
            <a href="javascript:void(0);">Go back</a>
        </div>
        <div id="success"> <!-- Блок удачи -->
            Пользователь <span></span> найден.
            <a href="javascript:void(0);">Go back</a>
        </div>
        ...

Помните я говорил, что то про поддержку, 80/20% и прочую муть? Дак вот. Забудьте. Для данного приложения нет ничего постыдного написать код в стиле jQuery way. Вы потратите времени в 10-20 раз меньше, чем писать это всё через Backbone. А размеры кода позволяют поддерживать это приложение хоть ночью после пол-литры. Нет ничего постыдного писать таким способом и зарабатывать свои $5. Кто не согласен, пусть засунет своё мнение в комментарий.
Я люблю повторять фразу, что все framework’и служат 2 целям, делать из миллиардного проекта, проект на миллион, и из проекта за $100 — проект на пару миллионов. Пользуетесь тем что эффективнее сэкономит ваше время и деньги.

Шаг 6. Контроллер приложения через Модель (rev 8 )

Замечательная функция Backbone это возможность связать модель и представление. Если создать представление с параметром model, то в методе initialize представления можно подписаться на возникновение события изменения модели. После чего, View будет получать сообщения, при каждом изменении модели либо ее части. И уже на это сообщение привязать определенное поведение предоставления, например полную либо частичную его перерисовку.
Помните я говорил, что некрасиво, когда один и тот же блок обрабатывается несколькими View. Попробуем отвязаться от этого засилья обработчиков.
Для начала, из объекта AppState создадим модель, которая будет содержать имя пользователя и состояние приложения:

var AppState = Backbone.Model.extend({
    defaults: {
        username: "",
        state: "start"
    }
});

var appState = new AppState();

Вторым шагом, удалим вьюшки Success и Error, а view Start переименуем в Block, т.к. она будет обрабатывать несколько состояний, а не только стартовое. Во оставшимся view переименуем поле template в templates в котором будут храниться все шаблоны для различных состояний

    var Block = Backbone.View.extend({

        templates: { // Шаблоны на разное состояние
            "start": _.template($('#start').html()),
            "success": _.template($('#success').html()),
            "error": _.template($('#error').html())
        },

В инициализаторе представления подпишемся на событие изменения модели. На данное событие повесим перерисовку блока.

        initialize: function () { // Подписка на событие модели
            this.model.bind('change', this.render, this);
        },

Функция перерисовки (render) будет “отрисовывать” нашу главную модель соответствующим шаблоном, зависящим от поля state модели:

        render: function () {
            var state = this.model.get("state");
            $(this.el).html(this.templates[state](this.model.toJSON()));
            return this;
        }

Изменится также функция check. Она будет устанавливать соответствующие поля модели:

        check: function () {
            var username = this.el.find("input:text").val();
            var find = (_.detect(Family, function (elem) { return elem == username })); // Проверка имени пользователя
            appState.set({ // Сохранение имени пользователя и состояния
                "state": find ? "success" : "error",
                "username": username
            }); 
        },

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

    var block = new Block({ model: appState });
    appState.trigger("change");

Если бы у нас не было бы хеш навигации, я бы закончил этот шаг. Но у нас полетела навигация. Восстановим её. Для этого перепишим код роутера.

    var Controller = Backbone.Router.extend({
        routes: {
            "": "start", // Пустой hash-тэг
            "!/": "start", // Начальная страница
            "!/success": "success", // Блок удачи
            "!/error": "error" // Блок ошибки
        },

        start: function () {
            appState.set({ state: "start" });
        },

        success: function () {
            appState.set({ state: "success" });
        },

        error: function () {
            appState.set({ state: "error" });
        }
    });

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

    appState.bind("change:state", function () { // подписка на смену состояния для контроллера
        var state = this.get("state");
        if (state == "start")
            controller.navigate("!/", false); // false потому, что нам не надо 
                                              // вызывать обработчик у Router
        else
            controller.navigate("!/" + state, false);
    });

События это отдельная песня в backbone. Простейшие события, DOM-события и события изменения модели или коллекции, могут переплетаться с событиями описанными пользователем, образуя чудесный винтаж объектно-орентированного и событийно-орентированного программирования. Советую изучить их прежде чем начинать использовать Backbone.js в своём проекте.

Вот и всё с самым большим рефакторингом в этом маленьком проекте. И на будущее, начинайте проектирование системы с моделей, а не с View как это сделал я и ваши волосы будут, просто будут.

Шаг 7. Проверка на несколько пользователей через коллекцию (rev 9 )

То что мы реализовали на 5 шаге имеет свой недостаток. Мы смешали логику отображения с логикой управления данными. Мы не сможем сейчас просто, не перестраивая логику работы View, заменить наш массив на обращение к сервису.Именно для этих целей в Backbone используются коллекции.
Коллекция в данном framework’е это сортированный набор моделей, который умеет обращаться с этими моделями, фильтровать или сортировать их. Также коллекции умеют из коробки работать с сервисами по REST интерфейсу.Фактически это прослойка между widget’ом и способами доступа к базе данных.
Вернёмся от рассуждений к нашей задаче. Создадим модель UserNameModel. Единственным обязательным полем данной модели будет поле Name, которое по умолчанию имеет пустое значение.

    var UserNameModel = Backbone.Model.extend({ // Модель пользователя
        defaults: {
            "Name": ""
        }
    });

Создадим коллекцию Family из моделей UserNameModel

    var Family = Backbone.Collection.extend({ // Коллекция пользователей
        model: UserNameModel,
    });

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

        checkUser: function (username) { // Проверка пользователя
            var findResult = this.find(function (user) { return user.get("Name") == username })
            return findResult != null;
        }

Создадим экземпляр коллекции Family

    var MyFamily = new Family([ // Моя семья
                { Name: "Саша" },
                { Name: "Юля" },
                { Name: "Елизар" }
            ]);

После чего проверка пользователя во View сокращается до вызова метода проверки в экземпляре MyFamily

var find = MyFamily.checkUser(username); // Проверка имени пользователя

Вывод

В процессе статьи мы создали учебный проект, который ни фига не делает, но который не мой взгляд всесторонне охватывает данный framework. В результирующем файле примерно 200 строк кода. Это больше чем в варианте с jQuery, но это хорошие легко расширяемые строки. Объекты знают о друг друге необходимый минимум и не больше того.
Backbone на удивление оказался хорошим продуктом позволяющим построить хребет для своего сервиса. Он даёт платформу для создания одностраничных сервисов и различных крупных динамических приложений. Как уже показано в ремарках, использовать его иногда, на маленьких проектах, бывает невыгодно. Но как только мы разрастаемся, и сложность поддержки нашего приложение растёт по экспоненте, то используя backbone можно значительно сократить трудозатраты на поддержку, оставляя время для наработку нового функционала.

По материалам http://habrahabr.ru/post/127049/