React - Backbone - Browserify : Que du bonheur!

J’ai eu beau essayer d’autres frameworks (Angular, Polymer, Ember 
), j’en reviens toujours Ă  Backbone : son modĂšle objet, ses modĂšles et collections, associĂ©s Ă  de la lĂ©gĂšretĂ© et de la simplicitĂ©, je n’arrive pas Ă  m’en passer. Par contre, je trouve lourds les “Backbone Views” et les templates (Mustache et les autres), et probablement encore plus lourd, le systĂšme de gestion de dĂ©pendances et de module RequireJS.

Remplacer Backbone.View

Il y a peu j’ai dĂ©couvert React qui permet de se substituer aux “Views” Backbone d’une façon tout Ă  fait “concurrentielle”. J’y gagne en visibilitĂ© (facilitĂ© de dĂ©veloppement et de maintenance) et en puissance (plus rapide, plus lĂ©ger). (cf. mon post “React : La RĂ©volution des Views (?) (cĂŽtĂ© front)” pour une initiation rapide).

Remplacer RequireJS

J’ai mis un moment Ă  l’accepter, mais pour dĂ©velopper de “grosses” application javascript (surtout si l’on bosse en Ă©quipe), un gestionnaire de modules et de dĂ©pendances est OBLIGATOIRE!. Mais ĂȘtre obligĂ© de dĂ©clarer l’ensemble des dĂ©pendances dans un fichier (avec le risque dans oublier si on ajoute une librairie), la notation utilisĂ©e dans chacun des modules (avec le risque d’oublier une dĂ©pendance lĂ  aussi), 
 tout ça me “pompe l’air”! J’aime bien me simplifier la vie, et j’ai fini par tester quelque chose que j’avais mis de cĂŽtĂ© depuis un moment : Browserify. Pour faire court, cela permet d’avoir un systĂšme de gestion de module cĂŽtĂ© front identique Ă  celui de Nodejs et donc d’utiliser npm pour tĂ©lĂ©charger vos librairies javascript prĂ©fĂ©rĂ©es. Par exemple pour “rĂ©cupĂ©rer” Backbone, prĂ©fĂ©rez un npm install backbone Ă  un bower install backbone. Ensuite, lorsque vous aurez besoin de Backbone dans un module, il suffira d’écrire var Backbone = require("backbone"); dans votre fichier javascript. Vous n’aurez pas de tag <script></script> Ă  ajouter dans votre page html, puisque la commande browserify something.js -o bundle.js permettra de crĂ©er un fichier javascript unique avec toutes les dĂ©pendances “mergĂ©e”.

Oui, et comment fait-on?

Le but de cet article n’est pas de vous expliquer de A Ă  Z comment construire l’ensemble de la stack et des Ă©lĂ©ments nĂ©cessaires pour faire une 1Ăšre application, mais de vous permettre de dĂ©couvrir simplement et facilement comment tout ceci fonctionne. Pour ce faire j’ai crĂ©Ă© un gĂ©nĂ©rateur pour Yeoman (1) qui va vous permettre facilement (et automatiquement) de crĂ©er un projet avec toutes les dĂ©pendances nĂ©cessaires pour commencer Ă  jouer avec :

  • React
  • Backbone
  • Browserify
  • Bootstrap
  • et accessoirement Express, Mongoose (et donc MongoDb que vousdevrez installer)

Ce gĂ©nĂ©rateur s’appelle generator-react-app, vous pouvez le trouver ici : https://www.npmjs.org/package/generator-react-app, il est “accompagnĂ©â€ de quelque “subs-generators” permettant de gĂ©nĂ©rer des bouts de code automatiquement (ie: des modĂšles, des composants backbones, 
), mais aussi de quelques mĂ©caniques Grunt (grunt-react, grunt-browserify, grunt-watch) permettant de transformer automatiquement vos composants React en javascript mais aussi de gĂ©nĂ©rer le “bundle browserifiy javascript final”.

Tout ceci peut paraĂźtre un peu abstrait, donc passons directement Ă  la pratique.

Installer generator-react-app

Bien sûr vous avez besoin de Yeoman (et donc node et npm).

Dans un terminal, tapez sudo npm install -g generator-react-app

Créer le squelette de votre projet

CrĂ©ez un rĂ©pertoire : mkdir humans-demo, puis “allez” dans le rĂ©pertoire : cd humans-demo et enfin lancez “mon killer generator” (2) : yo react-app et donnez un nom Ă  votre application et Ă  votre base de donnĂ©es :

 _____             _       _____
| __  |___ ___ ___| |_ ___|  _  |___ ___
|    -| -_| .'|  _|  _|___|     | . | . |
|__|__|___|__,|___|_|     |__|__|  _|  _|
                                |_| |_|
Hi! This is a React-Express-Mongoose Generator :) Enjoy!
[?] Application name? HumansDemo
[?] DataBase name? DemoDb

attendez : le générateur va créer la structure de votre projet et télécharger via npm et bower toutes les dépendances nécessaires à votre projet.

Lancez votre application (pour voir)

Vous devez maintenant avoir l’arborescence suivante :

Alt "000.png"

  • lancez MongoDb : dans un terminal tapez mongod
  • lancez votre application : node app.js (un conseil installez nodemon https://github.com/remy/nodemon cela permet d’écouter les changements des fichiers et de re-dĂ©marrer node Ă  chaque changement)
  • lancez grunt browserify (cela va crĂ©er un fichier public/js/app.built.js)
  • lancez grunt-watch pour que Grunt Ă©coute les changements
  • allez Ă  http://localhost:3000

Si tout va bien vous devriez obtenir ceci :

Alt "001.png"

Si tout va mal, “pinguez” moi 


Créez des services CRUD pour Express (cÎté back)

Dans un terminal, tapez : yo react-app:mgroutes Human et répondez aux questions :

[?] mongoose schema (ie: name: String, remark: String)? firstName: String, lastName: String
[?] url? humans
   create models/Human.js
   create routes/Humans.routes.js
   create controllers/HumansCtrl.js

Nous venons de renseigner le schema Mongoose : firstName: String, lastName: String, l’url (gardez la valeur par dĂ©faut) de base des routes sera /humans et 3 fichiers ont Ă©tĂ© crĂ©Ă©s automatiquement :

models/Human.js

var mongoose = require('mongoose');

var HumanModel = function() {

  var HumanSchema = mongoose.Schema({
    firstName: String, lastName: String
  });

  return mongoose.model('Human', HumanSchema);
}

module.exports = HumanModel;

controllers/HumansCtrl.js

var Human = require("../models/Human")();

var HumansCtrl = {
  create : function(req, res) {
    var human = new Human(req.body)
      human.save(function (err, human) {
      res.send(human);
    });
  },
  fetchAll : function(req, res) {
    Human.find(function (err, humans) {
      res.send(humans);
    });
  },
  fetch : function(req, res) {
    Human.find({_id:req.params.id}, function (err, humans) {
      res.send(humans[0]);
    });
  },
  update : function(req, res) {
    delete req.body._id
    Human.update({_id:req.params.id}, req.body, function (err, human) {
      res.send(human);
    });
  },
  delete : function(req, res) {
    Human.findOneAndRemove({_id:req.params.id}, function (err, human) {
      res.send(human);
    });
  }
}

module.exports = HumansCtrl;

routes/Humans.routes.js

var HumansCtrl = require("../controllers/HumansCtrl");

var HumansRoutes = function(app) {

  app.post("/humans", function(req, res) {
    HumansCtrl.create(req, res);
  });

  app.get("/humans", function(req, res) {
    HumansCtrl.fetchAll(req, res);
  });

  app.get("/humans/:id", function(req, res) { //try findById
    HumansCtrl.fetch(req, res);
  });

  app.put("/humans/:id", function(req, res) {
    HumansCtrl.update(req, res);
  });

  app.delete("/humans/:id", function(req, res) {
    HumansCtrl.delete(req, res);
  });

}

module.exports = HumansRoutes;

Je vous ai déjà fait gagner beaucoup de temps non ? ;)

Créez les modÚles et collections Backbone (cÎté front)

Dans un terminal, tapez : yo react-app:bbmc Human et répondez aux questions : (gardez les valeurs par défaut quand elles sont proposées)

[?] model name (ie: Book) Human
[?] defaults (ie: name: 'John Doe', remark: 'N/A')? firstName: "John", lastName: "Doe"
[?] url? humans
   create public/js/modules/models/HumanModel.js
   create public/js/modules/models/HumansCollection.js

Nous avons donc obtenu toujours automatiquement un modĂšle et une collection.

public/js/modules/models/HumanModel.js

var Backbone = require("backbone");

var HumanModel = Backbone.Model.extend({
  defaults : function (){
    return {
      firstName: "John", lastName: "Doe"
    }
  },
  urlRoot : "humans",
  idAttribute: "_id"
});

module.exports = HumanModel;

public/js/modules/models/HumansCollection.js

var Backbone = require("backbone");
var HumanModel = require("./HumanModel");

var HumansCollection = Backbone.Collection.extend({
  url : "humans",
  model: HumanModel
});

module.exports = HumansCollection;

Vous avez vu ?!

Nous sommes cĂŽtĂ© client, et nous dĂ©clarons les dĂ©pendances comme avec Node : var HumanModel = require("./HumanModel");, c’est tout de mĂȘme plus simple qu’avec Require, c’est la magie de Browserify!

Il ne faut pas oublier d’exporter chacun des modules pour pouvoir les utiliser : module.exports = HumanModel; et module.exports = HumansCollection;.

Passons à l’IHM avec React ou comment remplacer les “Views” Backbone

Nous voulons pouvoir saisir des informations et les afficher, nous allons donc créer un formulaire et une table.

Dans un terminal, tapez : yo react-app:formbb HumanForm Human et répondez aux questions : (gardez les valeurs par défaut quand elles sont proposées)

[?] model name (ie: Book) Human
[?] fields (for UI) (ie : title, author)? firstName, lastName
[?] url? humans
   create public/js/react_components/HumanForm.js

Ensuite, yo react-app:tablebb HumansTable Human

[?] model name (ie: Book) Human
[?] fields (for UI) (ie : title, author)? firstName, lastName
[?] url? humans
   create public/js/react_components/HumansTable.js

Nous avons donc maintenant 2 composants React (toujours automatiquement)

public/js/react_components/HumanForm.js

/** @jsx React.DOM */

var React = require('react')
  , HumanModel = require("../modules/models/HumanModel");

var HumanForm = React.createClass({

  getInitialState: function() {
    return {data : [], message : ""};
  },

  render: function() {
    return (
      <form role="form" className="form-horizontal" onSubmit={this.handleSubmit}>
        <div className="form-group">
            <input className="form-control" type="text" placeholder="firstName" ref="firstName"/>
        </div>
        <div className="form-group">
            <input className="form-control" type="text" placeholder="lastName" ref="lastName"/>
        </div>
        
        <div className="form-group">
          <input className="btn btn-primary" type="submit" value="Add Human" />
        </div>
        <div className="form-group"><strong>{this.state.message}</strong></div>
      </form>
    );
  },
  handleSubmit : function() {
    var firstName = this.refs.firstName.getDOMNode().value.trim();
    var lastName = this.refs.lastName.getDOMNode().value.trim();
    
    if (!firstName) {return false;}
    if (!lastName) {return false;}
    
    var data = {};
    data.firstName = firstName;
    data.lastName = lastName;
    

    var human= new HumanModel(data);

    human.save()
      .done(function(data) {
        this.setState({
          message : human.get("_id") + " added!"
        });
        this.refs.firstName.getDOMNode().value = '';
        this.refs.lastName.getDOMNode().value = '';
        
        this.refs.firstName.getDOMNode().focus();
      }.bind(this))
      .fail(function(err) {
        this.setState({
          message  : err.responseText + " " + err.statusText
        });
      }.bind(this));

    return false;
  }

});

module.exports = HumanForm;

public/js/react_components/HumansTable.js

/** @jsx React.DOM */

var React = require('react')
  , Backbone = require("backbone")
  , HumanModel = require("../modules/models/HumanModel")
  , HumansCollection = require("../modules/models/HumansCollection");

var HumansTable = React.createClass({

  getInitialState: function() {
    return {data : [], message : ""};
  },

  render: function() {

    var humansRows = this.state.data.map(function(human){
      var deleteLink = "# delete_human/" + human._id;

      return (
        <tr>
          <td>{human.firstName}</td>
          <td>{human.lastName}</td>
          
          <td><a href={deleteLink}>delete{" "}{human._id}</a></td>
        </tr>
      );
    });

    return (
      <div className="table-responsive">
        <strong>{this.state.message}</strong>
        <table className="table table-striped table-bordered table-hover" >
          <thead>
            <tr>
              <th>firstName</th><th>lastName</th>
              <th>_id</th>
            </tr>
          </thead>
          <tbody>
            {humansRows}
          </tbody>
        </table>
      </div>
    );
  },  

  getHumans : function() {

    var humans = new HumansCollection();

    humans.fetch()
      .done(function(data){
        this.setState({data : humans.toJSON(), message : Date()});
      }.bind(this))
      .fail(function(err){
        this.setState({
          message  : err.responseText + " " + err.statusText
        });
      }.bind(this))
  },
  
  componentWillMount: function() {
    this.getHumans();
    setInterval(this.getHumans, this.props.pollInterval);
  },

  componentDidMount: function() {
    var Router = Backbone.Router.extend({
      routes : {
        "delete_human/:id" : "human"
      },
      initialize : function() {
        console.log("Initialize router of HumansTable component");
      },
      human : function(id){
        console.log("=== delete human ===", id);
        new HumanModel({_id:id}).destroy();
        this.navigate('/');
      }
    });
    this.router = new Router()
  }

});

module.exports = HumansTable;

Vous avez vu là aussi 
 ?!

Nous dĂ©clarons lĂ  aussi les dĂ©pendances de la mĂȘme maniĂšre que pour les modĂšles et collection Backbone :

var React = require('react')
  , Backbone = require("backbone")
  , HumanModel = require("../modules/models/HumanModel")
  , HumansCollection = require("../modules/models/HumansCollection");

Et surtout ne pas oublier : /** @jsx React.DOM */ pour que les composants soit bien transformés par grunt-react.

One more thing!

Une derniĂšre petite remarque, dans la mĂ©thode componentDidMount de mon composant React, j’ai pu crĂ©er un “Router” Backbone complĂštement associĂ© au composant.

Il ne nous reste plus qu’à afficher tout ça

Modifiez public/js/modules/main.js`

Ouvrez le fichier public/js/modules/main.js et remplacez son contenu par :

/** @jsx React.DOM */
var React   = require('react');
var Backbone = require("backbone");

var HumanForm = require('../react_components/HumanForm');
var HumansTable = require('../react_components/HumansTable');

Backbone.history.start();

React.renderComponent(
  <HumanForm/>,
  document.querySelector('HumanForm')
);

React.renderComponent(
  <HumansTable pollInterval={500}/>,
  document.querySelector('HumansTable')
);

Modifiez public/index.html

Modifiez le contenu du fichier index.html de la maniĂšre suivante :

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>HumansDemo</title>
  <meta name="description" content="react application">
  <meta name="author" content="John Doe">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <link rel="stylesheet" href="js/bower_components/bootstrap/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="js/bower_components/bootstrap/dist/css/bootstrap-theme.min.css">
</head>
<body>
  <div class="container">
    <h1>HumansDemo</h1>
  </div>
  <div class="container">
    <div class="row">
      <div class="col-md-6">
        <HumanForm/>
      </div>
      <div class="col-md-6">
        <HumansTable/>
      </div>
    </div>
  </div>

  <script src="js/app.built.js"></script>

</body>
</html>

Vous pouvez remarquer que l’on n’insùre qu’un seul script js/app.built.js qui est construit et mis à jour au fur et à mesure que l’on travail grñce aux tñches Grunt.

Et vous pouvez rafraĂźchir la page de votre navigateur et jouez avec :

Alt "002.png"

Je joue avec tout cela depuis quelques jours et je trouve que marier React et Browserify simplifie le code, et en prime je peux conserver ce que je préfÚre dans Backbone : les modÚles et les collections.

A l’usage, je trouve que l’utilisation des tĂąches grunt-react et grunt-browserify lancĂ©es via grunt-watch me permet de dĂ©tecter rapidement (et de maniĂšre assez explicite) certaine erreurs.

Si vous avez des idĂ©es d’amĂ©liorations (notamment pour la partie Grunt) ou d’ajouts, n’hĂ©sitez pas Ă  contribuer Ă  mon gĂ©nĂ©rateur : https://github.com/k33g/generator-react-app. (PS: j’ai aussi en projet d’en faire une version pour PlayFramework dĂšs que j’ai un peu de temps).

Tous les retours sont bienvenus :)

(1): je sais que j’avais dit que je trouvais Yeoman “pas trĂšs lĂ©ger”, mais une fois que l’on a fait son premier gĂ©nĂ©rateur, on s’aperçoit qu’il est diablement pratique. (2) un peu d’autosatisfaction n’a jamais fait de mal.

blog comments powered by Disqus

Related posts