Pull to refresh

Backbone.js для «чайников»

JavaScript
Backbone.js для чайников
Как то поздним вечерком мне пришла мысль изучить 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" class="block">
	<div class="userplace">
	    <label for="username">Имя пользователя: </label>
	    <input type="text" id="username" />
	</div>
	<div class="buttonplace">
	    <input type="button" value="Проверить" />
	</div>
</div>
<div id="error" class="block">
	Ошибка такой пользователь не найден.
</div>
<div id="success" class="block">
	Пользователь найден.
</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" class="block">
</div>

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

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

<!-- Блок удачи -->
<script type="text/template" id="success">
    <div class="success">
        Пользователь <%= 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" class="block"> <!-- Блок ошибки -->
            Ошибка. Пользователь <span class="username"></span> не найден.
            <a href="javascript:void(0);">Go back</a>
        </div>
        <div id="success" class="block"> <!-- Блок удачи -->
            Пользователь <span class="username"></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 можно значительно сократить трудозатраты на поддержку, оставляя время для наработку нового функционала.
Tags:backbonejavascriptдля чайников
Hubs: JavaScript
Total votes 113: ↑110 and ↓3+107
Views278K

Popular right now

Top of the last 24 hours