Pull to refresh

Написание сложных интерфейсов с Spine.js

Reading time 11 min
Views 8.6K

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

Использованные материалы:
Tags:
Hubs:
+35
Comments 17
Comments Comments 17

Articles