Backbone, quelques tips à connaître

Publié le 7 août 2013 par Alexandre Salaun | front

Cet article est publié sous licence CC BY-NC-SA

Après des débuts assez chaotiques avec Backbone, je me suis aperçu que certaines choses qui me semblent être essentielles ne sont pas si simples et la documentation ou les exemples présents sur le net sont assez légers. Je vais donc partager dans cet article quelques tips qui pourront vous servir pour vos futurs développements avec Backbone, et qui, en tout cas, nous ont servi.

Passage de paramètres à une vue

Il est possible dans Backbone de passer des paramètres à une vue depuis votre router. Pour cela, rien de plus simple, lors de l’instanciation de la vue, vous passez les paramètres comme dans l’exemple suivant :

my_action: ->
  # je crée un tableau que je veux utiliser dans ma vue mais aussi dans le router
  @categories = ["test", "foo", "bar"]
  # j'instance ma vue en lui passant en paramètre mes catégories
  @view = new MyApp.Views.Homepage(categories: @categories)
  $("#container").html(@view.render().el)

Les catégories sont maintenant accessibles dans la vue, il suffit d’en faire une variable de classe pour y accéder partout dans votre vue :

class MyApp.Views.Homepage extends Backbone.View

  initialize: (options) ->
    @categories = options.categories

  render: ->
    # je peux afficher mes catégories pour vérifier qu'elles sont bien présentes
    console.log @categories

    # comme pour passer des paramètres du router à la vue, je les passe au *template* afin de pouvoir les utiliser dans ce dernier
    @$el.html(@template(categories: @categories))

    return this

Les paramètres passés depuis le router sont donc accessibles dans la méthode initialize de la classe, ils sont passés dans un objet les regroupant (ici options). Attention, les paramètres nommés model et collection ne figurent pas dans cet objet (tout comme el, id, className, tagName et attributes), ce sont des cas spéciaux que nous allons aborder par la suite.

Dans l’exemple ci-dessus, l’affichage à proprement parler se fait via l’appel à la ligne suivante du router :

$("#container").html(@view.render().el)

C’est cette dernière qui va afficher le contenu du template dans l’élément du DOM ayant l’id “container”. Sans cette ligne, rien ne sera affiché.

Affichage suite à une chargement de collection ou de model

Comme nous venons de le voir, il est possible de passer des paramètres aux vues. Cela est nécessaire pour les vues correspondant à un model ou à une collection. Pour ces cas précis, le fonctionnement est légèrement différent. Dans la documentation que j’ai pu trouver, la plupart du temps, les routers ne correspondent qu’à un seul model et la collection correspondante est passée en paramètre lors de l’instanciation du router comme dans l’exemple suivant (qui est une vue Rails) :

#container

:javascript
  $(function() {
    window.router = new MyApp.Routers.ApplicationRouter({
      categories: #{Category.order("name DESC").to_json}
    });
    Backbone.history.start({pushState: true});
  });

Dans ce cas, il est possible d’obtenir toutes les catégories dans le router comme suit :

class MyApp.Routers.ApplicationRouter extends Backbone.Router
  initialize: (options) ->
    @categories = new MyApp.Collections.ProfilesCollection()
    @categories.reset options.categories

    ...

    super options

Les catégories sont donc chargées depuis Rails, passées au router Backbone et il n’est donc pas nécessaire de le refaire à chaque action. Il suffit de passer les catégories lorsque c’est nécessaire :

class MyApp.Routers.ApplicationRouter extends Backbone.Router

  ...

  index: ->
    @view = new MyApp.Views.IndexView(collection: @categories)
    $("#container").html(@view.render().el)

  show: (id) ->
    category = @categories.get(id)
    @view = new MyApp.Views.ShowView(model: category)
    $("#container").html(@view.render().el)

Ensuite dans les vues :

# index
class MyApp.Views.IndexView extends Backbone.View

  render: ->
    @$el.html(@template(categories: @collection))
    return this

# show
class MyApp.Views.ShowView extends Backbone.View

  render: ->
    @$el.html(@template(category: @model))
    return this

Voilà donc comment avoir une vue pour l’index et une pour le show d’un model donné. Le fonctionnement, lorsque la collection d’objets est passée lors de l’instanciation du router, est identique à celui vu précédemment avec le passage de paramètre entre router et view. Maintenant, si la collection ou le model ne sont pas présents lors de l’instanciation, il faut faire une requête AJAX pour les récupérer et, dans ce cas, le fonctionnement varie.

Dans votre action de router, il faut instancier la collection ou le model, donner l’URL à attaquer et ensuite le transmettre à la vue :

class MyApp.Routers.ApplicationRouter extends Backbone.Router

  ...

  users_index: ->
    users = new MyApp.Collections.UsersCollection()
    users.url = "/users/"
    users.fetch()

    @view = new MyApp.Views.IndexView(collection: users, el: $("#container"))

Comme vous pouvez le voir, l’affichage du template ne se fait plus dans le router. Dans la vue aussi le fonctionnement change :

class MyApp.Views.Contents.IndexView extends Backbone.View

  initialize: (options) ->
    @collection.on("sync", @render, this)

  render: ->
    @$el.html(@template(users: @collection.toJSON()))
    return this

L’affichage du template se fait à chaque synchronisation de la collection. C’est pour cela que l’affichage du template n’est plus géré dans le router. Il se fait une fois que l’appel AJAX est terminé et que les objets ont été retournés en JSON. Vous pouvez donc afficher vos users dans votre template. Le principe est le même pour le chargement d’un model en lieu et place d’une collection.

Si vous retirez l’initializer de la vue et que vous continuez à appeler la méthode d’affichage du template dans le router, il n’y aura aucun élément dans votre collection car l’appel AJAX n’est pas fait au moment où le template est appelé.

Vous pouvez donc, dans votre router, passer des collections directement lors de l’instanciation ou effectuer des appels AJAX. Cependant, il vaut mieux éviter d’avoir des routers trop gros.

Découper le router

Pour une application assez importante, il est très facile de se retrouver avec un router de plusieurs dizaines (voir centaines) de lignes, qui, par conséquent, devient illisible. Dans les différents exemples que j’ai pu trouver au début de mes recherches sur Backbone, un seul model était utilisé, par conséquent, le router restait assez simple.

Prenons maintenant pour exemple une application multi-modèles. Dans ce cas, le router grossit à vue d’œil et le plus judicieux est de le découper en plusieurs parties, une par model par exemple (à chacun de faire le découpage qu’il juge pertinent). Il y aura donc plusieurs routers dans votre application :

Nous avons donc ici deux routers. Il peut y en avoir autant que vous le souhaitez.

Il est nécessaire de créer un router “principal” pour votre application. Il va contenir les méthodes qui seront partagées entre les routers. Dans notre cas, disons que c’est l’ApplicationRouter qui est le principal. Il sera conservé dans une variable globale lors de l’instanciation pour pouvoir être appelé par les autres routers :

#container

:javascript
  $(function() {
    window.router = new MyApp.Routers.ApplicationRouter({
      categories: #{Category.order("name DESC").to_json}
    });

    new MyApp.Routers.UsersRouter;

    Backbone.history.start({pushState: true});
  });

Vous pouvez donc instancier autant de routers que vous en avez afin de séparer les actions pour une meilleure lisibilité. Il est également possible de passer des paramètres à chacun d’eux.

On ajoute au router principal une méthode permettant de garder l’historique des pages par exemple :

class MyApp.Routers.ApplicationRouter extends Backbone.Router
  initialize: (options) ->
    @on "all", @storeRoute

    @history = []

    ...

    super options

  routes:
    ""        : "home"
    "sign_up" : "newRegistration"
    "sign_in" : "newSession"
    ".*"      : "home"

  storeRoute: ->
    window.router.history.push Backbone.history.fragment
    # window.router.history représente la même variable que @history mais il est défini comme cela pour être explicite dans les autres routers et qu'il n'y ait pas de confusions

La méthode storeRoute est appelée à chaque changement d’URL correspondant à ce router ce qui permet de stocker toutes les URLs parcourues. Si maintenant on souhaite aussi que cette méthode soit appelée à chaque changement d’URL de notre autre router, il suffit de l’appeler comme suit :

class MyApp.Routers.UsersRouter extends Backbone.Router
  initialize: (options) ->
    @on "all", window.router.storeRoute
    ...
    super options

Voici donc comment vous pouvez découper votre router en plusieurs parties afin de faciliter la lisibilité et éviter d’avoir des fichiers interminables.

Conclusion

Backbone permet de structurer son code JavaScript afin de créer une single-page-application. Cependant, un certain nombre de points peuvent sembler difficiles à appréhender au départ. Il faut du temps et de la pratique pour maitriser cet outil mais c’est le cas pour tout. J’espère vous avoir aidés avec ces quelques tips qui, bien sûr, ne sont pas les seuls. Je suis d’ailleurs curieux de savoir quels cas particuliers vous avez pu rencontrer et comment vous les avez traités.

L’équipe Synbioz.

Libres d’être ensemble.