HTML5-sovellusten kehittämiseen on ainakin kaksi hyväksi havaittua JavaScript-frameworkia: Backbone ja Spine. Käyn tässä artikkelissa läpi näissä havaitsemiani eroja tietomallien osalta. Suurin tekninen eroavuus on siinä, että Backbone on kehitetty JavaScriptillä ja Spine CoffeeScriptillä. Tämä ei estä käyttämästä kumpaakin ristiin kumman tahansa kielen kanssa, mutta Spine hyödyntää enemmän CoffeeScriptin ominaisuuksia.

CoffeeScriptissä on oma luokkasyntaksinsa, jolla tietomallit luodaan näin:

class Article extends Spine.Model

Backbonessa vastaava tehdään JavaScriptillä perinteisempään tapaan:

var Article = Backbone.Model.extend({});

Tietomallien määrittely

Spine ja Backbone suhtautuvat tietomalleihin hieman eri lähtökohdista. Spine lähtee siitä, että tietokentät määritellään mallin yhteydessä. Niitä voidaan käsitellä tavallisina muuttujina (propertyinä). Lisäksi tietomalli koostuu vain yhdestä Model-luokasta. Sille voidaan määritellä REST-rajapinnan URL, josta tiedot ladataan JSON-muodossa. Luokkaa ei tarvitse erikseen instantioida, vaan sitä käytetään staattisia metodeita kutsuen samaan tapaan kuin Ruby on Railsissa.

# Spine data model
class Article extends Spine.Model
  @configure 'Article', 'id', 'created', 'published', 'title', 'body'
  @extend Spine.Model.Ajax
  @url: '/api/blog/article/'

Article.bind 'refresh', -> console.log('Articles loaded: ' + Article.count()) Article.fetch()

Backbonessa taas tietokenttiä ei tarvitse määritellä etukäteen, mutta niiden käsittelyn täytyy tapahtua get()- ja set()-metodien kautta. Lisäksi Backbone erottelee Model- ja Collection-luokat siten, että yhtä tietomallia kohden tarvitaan molemmat. Collection-luokka täytyy vielä erikseen instantioida ennen kuin sen sisältö voidaan ladata. Tästä seuraa melko paljon turhaa "javamaista" boilerplate-koodia.

// Backbone data model

var Article = Backbone.Model.extend({ });

var Articles = Backbone.Collection.extend({ model: Article, url: '/api/blog/article/' });

var articles = new Articles(); articles.fetch({ success: function() { console.log('Articles loaded: ' + articles.length); } });

JSON-rajapinnan kustomointi

Sekä Spine että Backbone olettavat palvelimen palauttavan Ajax-pyynnöillä noudetun datan tietyssä JSON-muodossa. Usein kuitenkin käy niin, että palvelimen API on valmiiksi määritelty, eikä sitä haluta muuttaa. Näin on esimerkiksi siinä tilanteessa, että Djangossa käytetään valmista Tastypie-rajapintaa.

Spinessä rajapinnan kustomointi onnistuu määrittelemällä tietomallille oman @fromJSON-funktion. Funktion voi määritellä suoraan tietomallin luokkaan, tai sitten sen voi erottaa omaksi moduulikseen, jolloin sitä on helppo käyttää missä tahansa tietomallissa.

Tässä esimerkki jälkimmäisestä tavasta, jossa Article-malli käyttää yksinkertaista TastypieAdapter-moduulia.

TastypieAdapter =
  fromJSON: (data) -> if data.objects then (new @(obj) for obj in data.objects) else new @(data)

class Article extends Spine.Model @configure 'Article', 'created', 'slug', 'short_url', 'title', 'rendered_body', 'original_image', 'thumb_image', 'comment_count' @extend Spine.Model.Ajax @extend TastypieAdapter @url: '/api/blog/article/'

Backbonen puolella käytetään vastaavanlaista parse-metodia, joka muuntaa palvelimen palauttaman datan sopivaan muotoon. Se on yksinkertaisempi, mutta toisaalta parse-metodi on lisättävä erikseen sekä Model- että Collection-luokkiin, jos molempia tarvitsee kustomoida. Spinessä taas yhdistetty @fromJSON-metodi voi luoda vapaasti Model-instansseja sen mukaan, mitä dataa palvelin sattui palauttamaan.

var Article = Backbone.Model.extend({
  parse: function(response) {
    return response;
  }
});

var Articles = Backbone.Collection.extend({ model: Article, url: '/api/blog/article/' parse: function(response) { return response.objects; } });

Eventit ja tiedon päivittyminen

Sekä Spine että Backbone lähettävät erilaisia eventtejä sitä mukaa, kun tietomallin sisältämät dataobjektit päivittyvät. Suurimpana erona on se, että Backbone osaa tarkkailla yksittäisten tietokenttien päivittymistä, kun taas Spine seuraa vain kokonaisten dataobjektien muuttumista. Spinessä dataobjektia muutetaan asettamalla uudet attribuutit ja kutsumalla sitten save()-metodia:

article.title = 'Hello World'
article.save()

Backbonessa taas riittää, että dataobjektin attribuuttia päivittää set()-metodilla. Sen kutsuminen on toisaalta hieman kömpelöä:

article.set({title: 'Hello World'})

Backbonessakin on save()-metodi, mutta sitä tarvitsee kutsua vain siinä tapauksessa, että muutetut tiedot halutaan tallentaa palvelimelle. Spine puolestaan tallentaa tiedot palvelimelle aina, ellei tätä ole erikseen disabloitu.

Tässä vielä lyhyt yhteenveto tietomalleihin liittyvistä eventeistä. Ne eivät vastaa suoraan toisiaan, mutta kattavat pääosin samat tilanteet.

SpineBackbone
create - record was createdadd - when a model is added to a collection.
update - record was updatedchange - when a model's attributes have changed.
change:[attribute] - when a specific attribute has been updated.
save - record was saved (either created/updated)
destroy - record was destroyeddestroy - when a model is destroyed.
remove - when a model is removed from a collection.
change - any of the above, record was created/updated/destroyed
refresh - all records invalidated and replacedreset - when the collection's entire contents have been replaced.
error - validation failederror - when a model's validation fails, or a save call fails on the server.
"all" — this special event fires for any triggered event, passing the event name as the first argument.

Kyselyt ja tiedon suodattaminen

Spinessä tietomallin sisältämiä dataobjekteja voidaan kysellä Ruby on Railsin ActiveRecordia muistuttavalla rajapinnalla. Kyselyt kohdistuvat niihin objekteihin, jotka on ensin noudettu palvelimelta selaimen muistiin. Tässä muutama esimerkki.

# Yksittäinen artikkeli ID:n perusteella
Article.find('4efc88a454e41e31b7000000')
# Yksittäinen artikkeli attribuutin perusteella
Article.findByAttribute('slug', 'tietomallit-html5-sovelluksissa-backbone-vs-spine')
# Useampi artikkeli attribuutin perusteella
Article.findAllByAttribute('published', true)

Backbonesta löytyy vastaavanlaiset hakuominaisuudet, mutta se käyttää Underscore-kirjaston find- ja filter-metodeja etsimiseen. Tämä tekee kyselyistä melko paljon työläämpiä kirjoittaa, joskin ne ovat joustavampia. Toisaalta näitä Underscoren collection-metodeita voi halutessaan käyttää Spinenkin kanssa.

// Yksittäinen artikkeli ID:n perusteella
articles.get('4efc88a454e41e31b7000000')
// Yksittäinen artikkeli Client side ID:n perusteella (Backbonen itse generoima tunniste)
articles.getByCid(otherArticle.cid))
// Yksittäinen artikkeli attribuutin perusteella
articles.find(function(article) { return article.get('slug') == 'tietomallit-html5-sovelluksissa-backbone-vs-spine'); });
// Useampi artikkeli attribuutin perusteella
articles.filter(function(article) { return article.get('published') == true; });

Yhteenveto

Yhteenvetona voisin todeta, että Spinen ja CoffeeScriptin käyttäminen johtaa pääsääntöisesti siistimpään ja tiiviimpään koodiin. Molemmissa näkyy Rubyn ja Pythonin ideologia, jossa pyritään yksinkertaisuuteen ja kaikki tarpeeton eliminoidaan.

Disclaimer: Tässä artikkelissa olevia koodinpätkiä ei ole kokonaan testattu. Olen kuitenkin kehittänyt molemmilla frameworkeilla pieniä HTML5-sovelluksia, joissa näitä toimintoja on käytetty. Omalla blogisaitillani on käytetty Spineä, CoffeeScriptiä ja Djangon Tastypie-rajapintaa.