Введение в Spine.js
Spine представляет собой небольшой фрэймворк, который позволяет работать по схеме MVC, создавая приложения непосредственно на языке JavaScript, что обеспечивает логическое разделение кода, наследование моделей через классы и расширения. Также во многом этот инструмент базируется на Backbone.js API, так что те разработчики, которые имели дело с данным фрэймворком, без труда разберутся и в Spine (однако существует целый ряд существенных различий). Spine.js может работать совместно с HTML5 и асинхронными запросами сервера.
С каждым днем JavaScript фрэймворков появляется все больше и больше. Так что же делает Spine.js таким особенным?
- Простая реализация контроллера (на основе Backbone's API)
- Поддержка Ajax и HTML5 Local Storage
- Асинхронная связь с сервером
- Работает во всех современных браузерах (Chrome, Safari, Firefox, IE> = 7)
- Spine Mobile extension
- Простой и легкий
- Хорошая документация
Взгляните на исходный код примера приложения, и решите для себя.
Классы Spine, модели и представления
Официальная документация Spine содержит самое подробное руководство из всех, что я видел. Туда включено очень много вещей: работа с валидацией, сериализацией и ещё целая куча фишек. Однако целью данного урока является знакомство с тремя самыми крупными фичами: классами, моделями и представлениями.

Классы
В самом сердце Spine, используется эмулированние Object.create для уверенности в том, что объекты создаются динамически и могут быть использованы во время работы скрипта. Использование подобных классов можно увидеть в следующем примере:
var twitterClient = Spine.Class.create();
//or
var twitterClientWithArgs = Spine.Class.create({
testMessage: "If it weren't for WebMD I would have never known what symptoms to mimic so I could get all these prescriptions from my doctor."
});
Для инициализации классов используется метод init(). Разработчики Spine приняли решение не эксплуатировать функцию конструктора, поскольку использование ключевого слова «new» может вызвать некоторые проблемы при создании экземпляров класса.
var twitterClient = Spine.Class.create({
testMessage: "Hello world"
});
var myclient = twitterClient.init();Все параметры, которые вы хотите использовать при инициализации объекта, следует передать через метод init(). Пример:
var twitterClient = Spine.Class.create({
init:function(testMessage){
this.testMessage = testMessage;
}
});Модели
В Spine модели используются для хранения данных приложения, а также для любой другой логики, связанной с этими данными. Следует придерживаться этой идеи, т.к. она является одним из требований в приложении, которое строится на MVC. Данные, связанные с моделями, хранятся в записи Model.records, и могут быть созданы с помощью функции Spine setup().
В следующем примере задается название модели и набор атрибутов в метод setup():
var Tweets = Spine.Model.setup("Tweet", ["username","tweet_message"]);Функциональность моделей можно расширять, используя свойства того или иного класса следующим образом:
Tweets.include({
toTweetString: function(){
return("@" + this.username + " " + this.tweet_message);
}
});Создание модели осуществляется всё тем же простым методом .init ():
var mytweets = Tweets.init({username: "addyosmani", tweet_message: "hai twitter"});Spine и Backbone имеют различные возможности рендеринга шаблонов и встраивания их в DOM модель.
Контроллеры
Spine контроллеры расширяют Spine.Class, а также наследуют все его свойства. Пример создания контроллера в Spine:
var TweetController = Spine.Controller.create({
init:function(){
//initial logic on instantiation
}
})Инициализация контроллеров происходит следующим образом:
var myTweetController = TweetController.init(); Каждому контроллеру соответствует специальный элемент — 'el', который может быть передан через свойство экземпляра. Пример:
var myTweetController = TweetController.init({el: $('#tweets')}); Документация
Более подробно о классах, моделях и контроллерах вы можете почитать в документации к Spine.
Основные различия между Spine и Backbone
Разработчики, которые прочитали документацию к Backbone и Spine, в первые минуты не смогут найти принципиальных отличий. Однако в реальном проекте эти отличия могут появиться сами собой.
1. Представления в Backbone по своему применению больше похожи на традиционные контроллеры, а Backbone контроллеры больше ответственны за обработку маршрутизации URL. В Spine поддержка маршрутизации была добавлена совсем недавно (потому как является очень необходимым элементом), а контроллеры очень похожи на представления в Backbone.
2. Backbone использует функции конструктора и прототипы, в то время как Spine использует эмулированную версию Object.create и моделируемую систему классов – что позволяет добиться того же самого эффекта наследования и на самом деле является очень интересным приёмом. Это одно из принципиальных отличий от Backbone. Оба подхода имеют право на существование.
3. Немало разработчиков обращают внимание на разницу в размере файлов той или другой библиотеки: в этом плане можно отметить тот факт, что в Spine не включает в себя маппинг, фильтрацию, и многие другие функции, которые включены в Backbone. Если для вас размер имеет значение, то вам, безусловно, нужно выбирать Spine, т.к. в этом плане он выигрывает по всем показателям.
Spine.js на практике
Пример: Bit.ly клиент


Когда вы работаете над SPA, много времени уходит на работу и взаимодействие с внешними данными (это могут быть ваши собственные данные или данные, полученные от какого-то API). Вам также хотелось бы использовать маршрутизацию, чтобы можно было сохранить состояние приложения. Для этого возможно придётся использовать localStorage, а также обработать запросы ajax.
Учитывая всё вышеперечисленное, мы собираемся создать bit.ly клиент, который позволит вам:
- создавать привлекательные в размере URL непосредственно из вашего браузера;
- архивировать свои bit.ly URL так, чтобы вы могли легко получить доступ к ним в любой момент;
- предоставление статистики кликов (это будет реализовано через дополнительное 'представление', для демонстрации роутинга).
Предпосылки
Создание bit.ly плагина
Прежде чем мы начнем, нам необходимо найти хороший способ получения доступа к службам bit.ly: 1. сокращённому URL и 2. статистике кликов. Вместо того, чтобы мучиться с обычным JavaScript, мы будем использовать jQuery для того, чтобы более удобным и быстрым способом работать с ajax запросами. Этот подход также позволит нам написать более читабельное и понятное приложение.
Дополнительная поддержка store.js
По умолчанию Spine ориентирован на современные браузеры и именно по этой причине такие вещи как localStorage, не будут одинаково работать во всех браузерах, т��к что если вам нужна кроссбраузерность, то вам следует воспользоваться более старыми инструментами.
Однако, эту проблему можно решить применением store.js (и то, на чем он основывается: json2.js). Ниже представлено содержание файла spine.model.local.js, который вы можете обновить, чтобы использовать хранилище, комментируя строки, которые отмечены ниже и заменив их своими.
Spine.Model.Local = {
extended: function(){
this.sync(this.proxy(this.saveLocal));
this.fetch(this.proxy(this.loadLocal));
},
saveLocal: function(){
var result = JSON.stringify(this);
//localStorage[this.name] = result;
store.set(this.name, result);
},
loadLocal: function(){
//var result = localStorage[this.name];
var result = store.get(this.name);
if ( !result ) return;
var result = JSON.parse(result);
this.refresh(result);
}
}; Обработка jQuery шаблона
Spine и Backbone фрэймворки могут взаимодействовать с несколькими подходами (микрошаблонная обработка, mustache.js и так далее). Какой использовать, выбирать вам. В примере использован плагин jQuery tmpl, чтобы представить наши сокращенные записи URL и статистику кликов, используя шаблоны.
Разработка
Список того, что необходимо реализовать:
- Модель, чтобы представить данные, которые будут содержаться в каждом сокращённом URL (Модель Url);
- Контроллер, чтобы представить отдельные записи и действия, которые могут быть выполнены приложением (exports.URL);
- Контроллер для вывода представления, ответственного за ввод новой записи bit.ly (exports.UrlsList);
- Контроллер, чтобы вывести представление, ответственное за статистику кликов по той или иной записи (exports.Stats);
- Универсальный контроллер, который будет обрабатывать маршрутизацию приложения (exports. UrlApp).
В примере используется jQuery, поскольку он идеально подходит для работы с шаблоном и плагином, но Spine так же может работать с Zepto или другими JavaScript библиотеками. Теперь давайте рассмотрим код нашего приложения:
Начальное кэширование
var exports = this;Простой jQuery плагин
$.fn.toggleDisplay = function(bool){
if ( typeof bool == "undefined" ) {
bool = !$(this).filter(":first:visible")[0];
}
return $(this)[bool ? "show" : "hide"]();
}; Url модели:
var Url = Spine.Model.setup("Url", ["short_url", "long_url", "stats"]);
Url.extend(Spine.Model.Local);
Url.include({
validate: function(){
if ( !this.long_url )
return "long_url required"
if ( !this.long_url.match(/:\/\//))
this.long_url = "http://" + this.long_url
},
fetchUrl: function(){
if ( !this.short_url )
$.bitly(this.long_url, this.proxy(function(result){
this.updateAttributes({short_url: result});
}));
},
fetchStats: function(){
if ( !this.short_url ) return;
$.bitly.stats(this.short_url, this.proxy(function(result){
this.updateAttributes({stats: result});
}));
}
});
Url.bind("create", function(rec){
rec.fetchUrl();
}); Контроллер exports.Urls:
exports.Urls = Spine.Controller.create({
events: {
"click .destroy": "destroy",
"click .toggleStats": "toggleStats"
},
proxied: ["render", "remove"],
template: function(items){
return $("#urlTemplate").tmpl(items);
},
init: function(){
this.item.bind("update", this.render);
this.item.bind("destroy", this.remove);
},
render: function(){
this.el.html(this.template(this.item));
return this;
},
toggleStats: function(){
this.navigate("/stats", this.item.id, true);
},
remove: function(){
this.el.remove();
},
destroy: function(){
this.item.destroy();
}
}); Контроллер exports.UrlsList:
exports.UrlsList = Spine.Controller.create({
elements: {
".items": "items",
"form": "form",
"input": "input"
},
events: {
"submit form": "create",
},
proxied: ["render", "addAll", "addOne"],
init: function(){
Url.bind("create", this.addOne);
Url.bind("refresh", this.addAll);
},
addOne: function(url){
var view = Urls.init({item: url});
this.items.append(view.render().el);
},
addAll: function(){
Url.each(this.addOne);
},
create: function(e){
e.preventDefault();
var value = this.input.val();
if (value)
Url.create({long_url: value});
this.input.val("");
this.input.focus();
}
}); Контроллер exports.Stats:
exports.Stats = Spine.Controller.create({
events: {
"click .back": "back"
},
proxied: ["change", "render"],
init: function(){
Url.bind("update", this.render);
},
template: function(items){
return $("#statsTemplate").tmpl(items);
},
render: function(){
if ( !this.item ) return;
this.el.html(this.template(this.item));
},
change: function(item){
this.item = item;
this.navigate("/stats", item.id);
this.item.fetchStats();
this.render();
this.active();
},
back: function(){
this.navigate("/list", true);
}
});Контроллер exports.UrlApp:
exports.UrlApp = Spine.Controller.create({
el: $("body"),
elements: {
"#urls": "urlsEl",
"#stats": "statsEl"
},
init: function(){
this.list = UrlsList.init({el: this.urlsEl});
this.stats = Stats.init({el: this.statsEl});
this.manager = Spine.Controller.Manager.init();
this.manager.addAll(this.list, this.stats);
this.routes({
"": function(){ this.list.active() },
"/list": function(){ this.list.active() },
"/stats/:id": function(id){ this.stats.change(Url.find(id)) }
});
Url.fetch();
Spine.Route.setup();
}
}); Наконец, для того чтобы завершить инициализацию нашего контроллера 'app':
exports.App = UrlApp.init();Код для сокращения URL и статистики кликов для Bit.ly
(function($){
var defaults = {
version: "3.0",
login: "legacye",
apiKey: "R_32f60d09cccde1f266bcba8c242bfb5a",
history: "0",
format: "json"
};
$.bitly = function( url, callback, params ) {
if ( !url || !callback ) throw("url and callback required");
var params = $.extend( defaults, params );
params.longUrl = url;
return $.getJSON("http://api.bit.ly/shorten?callback=?", params, function(data, status, xhr){
callback(data.results[params.longUrl].shortUrl, data.results[params.longUrl], data);
});
};
$.bitly.stats = function( url, callback, params ) {
if ( !url || !callback ) throw("url and callback required");
var params = $.extend( defaults, params );
params.shortUrl = url;
return $.getJSON("http://api.bitly.com/v3/clicks?callback=?", params, function(data, status, xhr){
callback(data.data.clicks[0], data);
});
};
})(jQuery); Application Index/HTML:
Для управления приложением используется LABjs, однако вы легко можете заменить это на то, с чем привыкли работать.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/application.css" type="text/css" charset="utf-8">
<script src="lib/LAB.min.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
$LAB
.script("lib/json.js")
.script("lib/jquery.js")
.script("lib/jquery.tmpl.js")
.script("lib/jquery.bitly.js")
.script("lib/store.min.js")
.script("lib/spine.js")
.script("lib/spine.model.local.js")
.script("lib/spine.controller.manager.js")
.script("lib/spine.route.js")
.script("app/models/url.js")
.script("app/application.js");
</script>
<script type="text/x-jquery-tmpl" id="urlTemplate">
<div class="item">
<div class="show">
<span class="short">
${long_url}
</span>
<span class="long">
{{if short_url}}
<a href="${short_url}">${short_url}</a>
{{else}}
Generating...
{{/if}}
</span>
<a class="toggleStats"></a>
<a class="destroy"></a>
</div>
</div>
</script>
<script type="text/x-jquery-tmpl" id="statsTemplate">
<div class="stats">
<a class="back">Back</a>
<h1>Click Statistics</h1>
<h1 class="longUrl">${long_url}</h1>
<p>Short URL:
{{if short_url}}
<a href="${short_url}">${short_url}</a>
{{else}}
Generating...
{{/if}}
</p>
{{if stats}}
<p>Global clicks: ${stats.global_clicks}</p>
<p>User clicks: ${stats.user_clicks}</p>
{{else}}
Fetching...
{{/if}}
</div>
</script>
</head>
<body>
<div id="views">
<div id="urls">
<h1>Bit.ly Client</h1>
<form>
<input type="text" placeholder="Enter a URL">
</form>
<div class="items"></div>
</div>
<div id="stats">
</div>
</div>
</body>
</html>
Примечание:
- Для кроссбраузерной совместимости данный пример должен быть запущен на живом или локальном веб сервере. Используйте MAMP/WAMP если необходимости;
- Для проверки статистики кликов я рекомендую использовать URL сайтов, которые являются наиболее популярными. Например, информация о сайте www.google.com наверняка присутствует в базе данных Bit.ly;
- Демо пример использует мои собственные ключи API Bit.ly, которые должны быть заменены.
- Круговые диаграммы сгенерированы при помощи Google Chart API. Для того чтобы не усложнять и так новый для вас подход, я сам выбирал изменение изображения диаграммы, но вы в любой момент можете легко переключиться на Visualization API;
- Структура каталогов приложения — это полностью ваше дело. Некоторые разработчики предпочитают общую структуру — model / view / controller, а другие предпочитают иметь универсальную папку приложения, где все содержется в единственном файле. В примере использована структура папок, к которой я привык.
- Если вы хотите сохранить уникальные «представления» для контента (например, одно представление для #ui/dashboard, а другое для #ui/stats), то вам необходимо разобрать работу spine.controller.manager.js, т.к. в этом файле есть решение данной задачи.
Вот и всё!
Результат
Исходники
Заключение
Spine — это хорошая альтернатива Backbone. Документация является довольно-таки неплохой, чтобы продолжить самостоятельное изучение.
Использованные материалы:
