Backbone.js est un cadre applicatif pour les applications à forte teneur en javascript.
Backbone.js peut se définir comme un framework MVC mais pas au sens classique du terme. Ici le C représente des Collections d’objets. Le M et le V désignent respectivement les modèles et les vues.
Backbone.js ne possède qu’une dépendance ferme, il s’agit de underscore.js, qui est maintenu par la même équipe.
Le rôle d’underscore.js est de vous apporter tout un tas de méthodes pour vous simplifier la vie (enumerable, binding, template, comparaison…) sans faire 500ko.
Généralement jQuery vient compléter la liste des dépendances finales de l’application, car il y a fort à parier que vous aurez à minima besoin de manipuler le DOM.
Backbone.js est un excellent choix pour les applications dites «single page application», c’est à dire une page principale avec un nombre important d’interactions utilisateur.
Plutôt que d’avoir une navigation classique par page (ou le serveur envoie toute la page à chaque URL), la navigation, l’envoi de formulaire et toutes les actions classiques se gèrent en javascript.
Avant, quand on souhaitait faire ce genre de choses, on écrivait son propre javascript, sans aucune convention, ầ grand coup d’AJAX et on se retrouvait vite avec du spaghetti code, complexe à maintenir et à tester.
Backbone.js nous permet de cadrer cela en définissant modèle, vue et collections. On va pouvoir définir des événements sur des changements de valeurs de nos modèles et ainsi rafraichir automatiquement nos vues.
Pour cela Backbone.js possède son propre routeur. On peut faire correspondre des actions à des URL.
Le code créé avec Backbone.js peut être testé via jasmine.
Backbone.js est uniquement une couche client, il n’a aucune vocation à gérer la persistence de vos données sur un serveur.
Mais rassurez vous cela se fait très bien, il suffit d’indiquer à nos modèles une URL et d’avoir une API JSON Restful pour être capable de réaliser du CRUD très simplement.
L’objectif de cette première partie est de créer une application qui va lister les catégories récupérées depuis le serveur.
L’application backbone.js est disponible sur github.
Le tag 0.1 correspond à cette partie.
Pour la persistence des données nous utilisons une application Ruby on Rails qui agit uniquement comme une API.
Nous avons une migration pour créer des catégories qui ont un nom, ainsi que quelques seeds. Une fois l’application clonée il vous reste à configurer votre base de données (database.yml) et lancer le chargement:
bin/rake db:setup
Notre contrôleur categories renvoie uniquement du JSON.
class CategoriesController < ApplicationController
respond_to :json
def index
@categories = Category.all
respond_with(@categories)
end
end
On va donc pouvoir récupérer nos catégories sur /categories.json
Pour son côté pratique et maintenant par habitude, le code est en coffeescript. Jetez un œil à notre introduction à coffeescript si vous ne connaissez pas encore. Vous ne serez pas trop dépaysés.
Le fichier principal est app.coffee, il est généré dans app.js Si vous souhaitez faire des changements en local pour tester, vous pouvez utiliser le Guardfile fournit via:
bin/guard
Il s’occupera de compiler votre coffeescript à la volée.
Logiquement notre application devrait être découplée en plusieurs fichiers, en séparant modèle, vue etc…
Ici la simplicité de l’application fait que tout est en un seul fichier. Commençons à décortiquer le code.
@app = window.app ? {}
Afin que les fichiers javascript puissent avoir connaissance les uns des autres on utilise l’objet global windows et le namespace app.
_.templateSettings = {
interpolate: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\{(.+?)\}\}/g
};
Underscore.js possède un système de template, dont la syntaxe par défaut est la même que celle des templates ERB.
Nous changeons cela pour qu’ERB n’essaie pas de les interpréter, ce sera le rôle de Backbone.js.
Pour cela nous modifions les paramètres du template pour avoir une syntaxe proche de celle de mustache.
On va donc passer de <% %> à .
class Category extends Backbone.Model
Notre catégorie va simplement étendre le modèle de base de Backbone.js pour hériter de toutes ces méthodes (save…).
Ici on utilise directement le mécanisme de coffeescript. Par convention le modèle est au singulier.
En javascript on aurait plutôt:
var Category = Backbone.Model.extend({})
Une fois notre classe créée nous la mettons dans le namespace. Nous le ferons systématiquement pour les autres classes, quelque soit leur fonction.
class Categories extends Backbone.Collection
url: "/categories"
model: app.Category
@app.Categories = Categories
La collection se trouve à mi chemin entre la vue et le modèle. Elle concerne un modèle dont elle contiendra une liste, dont le but sera d’aller peupler une vue.
Par convention la collection est au pluriel.
Partant de là nous sommes déjà en mesure de récupérer les données de l’API. Vous pouvez tester depuis la console de votre navigateur:
categories = new window.app.Categories()
categories.fetch()
categories.models
# [Category, Category]
Regardons comment définir un template dans nos vues HTML.
<script type="text/template" id="category">
</script>
Les templates sont définis dans le head de notre HTML, de base le navigateur n’interprétera rien. Comme mentionné plus tôt nous utilisons la syntaxe de mustache.
Le but pour ces templates est d’être remplacé par le HTML correspondant à ce qu’on souhaite afficher par Backbone.js.
Par exemple #category représentera un élément LI contenant le nom de la catégorie.
Voyons comment est définie notre classe du côté de Backbone.js:
class CategoryView extends Backbone.View
tagName: 'li'
template: _.template($('#category').html())
initialize: ->
_.bindAll(@, 'render')
render: ->
@.$el.html @template(@model.toJSON())
@
@app.CategoryView = CategoryView
Nous spécifions donc le tagName qui servira à l’enrobage HTML. Il existe d’autres options comme la classe CSS…
Ensuite nous indiquons quel est le template à utiliser (#category). C’est cette définition qui justifie l’enrobage $(document).ready, afin que le DOM soit prêt pour l’initialisation des templates.
Autrement on peut toujours les initialiser dans le constructeur de la vue mais cela impliquera un calcul à chaque instanciation d’objet, donc ce n’est pas recommandé.
Dans le constructeur nous allons nous assurer que le rendu de la vue se fera toujours dans le contexte de cet objet (this).
En effet le render pourra être appelé depuis d’autres endroits, notamment le modèle.
Dans ce cas nous voulons nous assurer que le this courant s’apparentera toujours à cette vue. Nous utilisons pour cela bindAll.
Enfin la méthode render va se servir du modèle que nous lui aurons passé pour générer notre LI.
Plutôt que d’appeler $(@el) on utilise @.$el qui permet d’éviter d’accéder à chaque fois au DOM en stockant une référence à l’élément.
Render doit toujours retourner this afin de pouvoir chainer les appels.
Un petit test en console:
// creation de la categorie
var category = new window.app.Category({ name: "Synbioz" })
// creation de la vue
var view = new window.app.CategoryView({ model: category })
var content = view.render()
content.el
// <li>Synbioz</li>
Regardons maintenant la vue qui listera l’ensemble des catégories:
class CategoriesView extends Backbone.View
tagName: 'ul'
template: _.template($('#categories').html())
initialize: ->
_.bindAll(@, 'render')
@collection.bind 'reset', @render
render: ->
@.$el.html(@template())
node = @.$('.categories')
collection = @collection
@collection.each (category) ->
view = new CategoryView
model: category
collection: collection
node.append view.render().el
@
Avec le binding du reset sur la collection nous introduisons toute la puissance de Backbone.js. Ce code va permettre de surveiller la collection, et de faire automatiquement un render de la vue associée.
Dans la méthode render nous allons utiliser la vue précédente pour générer chaque catégorie et l’ajouter à la liste.
Plutôt que de rechercher .categories dans tout le DOM nous limitons la portée au template, ce qui est nettement plus efficace.
Le routeur va nous permettre de définir les routes de notre application. Il définit les routes en GET en faisant matcher l’URL courante sur une action.
class MetricsRouter extends Backbone.Router
routes:
'': 'dashboard'
initialize: ->
window.categories = new app.Categories()
@categoriesView = new app.CategoriesView
collection: categories
dashboard: ->
window.categories.fetch()
$("#container").html(@categoriesView.el);
@app.MetricsRouter = new MetricsRouter()
Dès que nous serons sur la home, la méthode dashboard sera appelée. Dans celle ci on récupère les catégories, ce qui exécute directement un rendering de la vue associée.
Il ne reste qu’à l’ajouter dans le DOM.
Maintenant que tout est en place, lançons l’application:
Backbone.history.start()
Nous n’avons pas encore de navigation mais cela n’en demeure pas moins indispensable.
Par défaut les URL sont gérées comme celles de twitter, avec un #. Mais il existe une option (pushState) qui permet d’utiliser les possibilités d’HTML5 pour mettre à jour l’historique en javascript.
Ainsi pour votre navigateur c’est complètement transparent, la navigation s’apparente à une navigation classique.
Le rapport puissance / taille du code du Backbone.js est assez impressionnant. Il permet de créer des applications plus fluides et réactives en limitant les aller / retour avec le serveur.
Ce cadre applicatif permet d’avoir du code bien organisé, qui plus est testable avec jasmine.
Dans la prochaine partie nous allons gérer toute la partie événementielle et formulaire afin de pouvoir interagir avec la page et naviguer comme dans une application classique.
L’équipe Synbioz.
Libres d’être ensemble.