Введение в 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. Документация является довольно-таки неплохой, чтобы продолжить самостоятельное изучение.

Использованные материалы: