React.js, Grunt, browserify et CDN

Lorsqu’on attaque le développement d’une application frontend en Javascript, plusieurs questions se posent immédiatement pour le développeur :

  • Quels frameworks et librairies (Angular, Ember, React et Flux, …)
  • Comment organiser les fichiers sources (organisation technique, organisation fonctionnelle, …)
  • Comment gérer les modules (AMD compliant, CommonJS compliant, …)
  • Quel outil de build (Grunt, Gulp, …)

Dans cet article, nous allons voir comment créer un HelloWorld avec ReactJs, en utilisant Grunt pour construire l’application à partir des sources et browserify pour gérer les dépendances entre nos modules front et libs externes.

Vous pouvez retrouver le code final et les différentes étapes dans ces quelques commits poussés sur Github.

Sur la documentation du site officiel de React, un HelloWorld ressemble typiquement à ça :

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.2/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.2/JSXTransformer.js"></script>
  </head>
  <body>
    <div id="content"></div>

    <script type="text/jsx">
      /** @jsx React.DOM */
      var hello = React.createClass({
        render: function() {
          return <div>Hello, {this.props.name}!</div>
        }
      });

      React.renderComponent(
        <hello name="World" />,
        document.getElementById('content')
      );
    </script>

  </body>
</html>

Notre composant React.js est implémenté en plein milieu de notre code HTML, le JSX est transformé en JS à la volée par JSXTransformer au sein du navigateur et le  tout, dans un seul fichier index.html. Pas terrible évidemment dans le cadre d’une « vrai » application.

Intégration de Grunt avec React

Facebook fournit un outil en ligne de commande très bien fait pour transformer côté serveur le JSX en JS.

npm install -g react-tools
jsx --watch src/ build/

Mais dès que l’application grossit, qu’on ajoute des assets, qu’on veut compiler des fichiers LESS ou SASS, minifier nos sources et autres joyeusetés, l’utilisation d’un outil de build tel que Grunt ou Gulp devient vite indispensable. Il y a beaucoup d’exemples d’intégration de React avec Gulp sur le net donc pour faire original (ou old school), nous allons utiliser Grunt.

npm install -g grunt-cli
npm install react --save-dev
npm install grunt --save-dev

# pour copier nos assets :
npm install grunt-contrib-copy --save-dev

Pour rappel, React utilise une extension au Javascript appelée JSX dont l’objectif est de faciliter l’implémentation d’un composent UI (~HTML) au sein de code javascript. Facebook a même proposé un draft de spécification de la syntaxe JSX :

JSX is a XML-like syntax extension to ECMAScript without any defined semantics. It’s NOT intended to be implemented by engines or browsers. It’s NOT a proposal to incorporate JSX into the ECMAScript spec itself. It’s intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript.

Différentes approches sont possibles pour intégrer Grunt avec React. Il existe un plugin grunt-react très bien fait pour transpiler automatiquement les JSX en JS.

Ici, nous allons utiliser une autre approche en gérant nos dépendances entre modules avec Browserify et en intégrant la traduction des JSX via un transformer browserify.

Intégration de Browserify

Browserify est une lib de gestion de modules clients (browser) compatible CommonJS (« à la » nodejs).

Browserify lets you require(‘modules’) in the browser by bundling up all of your dependencies.

Evidemment, on retrouve un plugin grunt pour l’intégrer à notre build.

npm install grunt-browserify --save-dev
// Au sein de notre Gruntfile
grunt.loadNpmTasks('grunt-browserify');

Nous verrons la configuration proprement dite du plugin un peu plus loin.

Structuration du code

Maintenant que nous avons intégré browserify, nous pouvons structurer notre code source de manière un tout petit peu plus évolutive que dans le HelloWorld de base. Ci-dessous un exemple avec un dossier src/ et un dossier de build/. Les jsx (composants) sont regroupés dans src/jsx/ et l’index.html est à la racine du dossier src/. Cette structure n’a d’intérêt que pour montrer l’utilisation de browserify et de l’outil de build mais mériterait sans aucun doute d’être améliorée dans le cadre d’une application complexe avec plusieurs composants, différents domaines fonctionnels, etc. On trouve d’ailleurs quelques starter-kits intéressants sur Github pour commencer une application Flux / React.

structure

En reprenant le HelloWorld présenté au début de cet article et pour suivre la structure décrite ci-dessus, nous allons découpé les sources en trois fichiers :

  • src/jsx/hello.jsx qui contiendra l’implémentation du composant React hello
  • src/jsx/app.jsx qui contiendra la partie instanciation du composant
  • src/index.html qui contiendra le layout du HelloWorld

Le point d’entrée de notre application est app.jsx, au sein duquel nous chargeons nos dépendances (frameworks, lib, composants) avec Browserify (require(‘…’)).

ReactJs étant nativement compatible CommonJS, un require(‘react’) est suffisant :

// src/jsx/hello.jsx
var React = require('react');

var Hello = React.createClass({

  render: function() {
    return <div>Hello, { this.props.name }!</div>
  }
});

module.exports = Hello;

Pour rendre notre composant Hello importable depuis un autre fichier source, on utilise module.exports en fin de fichier. Il ne nous reste plus qu’à importer notre composant depuis app.jsx :

// src/jsx/app.jsx
var React = require('react'),
    Hello = require('./hello.jsx');

React.renderComponent(
  <Hello name="World" />,
  document.getElementById('content')
);

Notre layout index.html n’a alors plus qu’à importer app.js, bientôt buildé via grunt :

<!DOCTYPE html>
<html>
    <head>
        <title>Hello ReactJS</title>
    </head>
    <body>
        <div id="content"></div>
        <script src="js/app.js"></script>
    </body>
</html>

Branchement de l’ensemble avec notre Gruntfile

Pour brancher le tout, nous ajoutons à notre Gruntfile la configuration de browserify avec le transformer reactify et nous configurons également grunt-contrib-copy pour copier notre layout (et les autres assets éventuels) :

// Gruntfile
module.exports = function(grunt) {
    grunt.initConfig({
        browserify: {
            options: {
                debug: true,
                extensions: ['.jsx'],
                transform: ['reactify']
            },
            hello: {
                src: 'src/jsx/app.jsx',
                dest: 'public/js/app.js'
            }
        },
        copy: {
            all: {
                expand: true,
                cwd: 'src/',
                src: ['index.html'],
                dest: 'public/'
            }
        }
    });

    grunt.loadNpmTasks('grunt-browserify');
    grunt.loadNpmTasks('grunt-contrib-copy');

    grunt.registerTask('default', ['browserify', 'copy']);
};

Il ne nous reste plus qu’à tester en exécutant la commande :

grunt

Normalement, on obtient alors auto-magiquement un gros fichier JS qui contient React, notre composant Hello et notre micro-application démo.

Et si on ouvre notre fichier index.html dans un navigateur, on obtient un beau HelloWorld sur fond blanc…

Et pour profiter d’un CDN ?

Comme vous avez pu le constater ci-dessus, lors du build, ReactJS se retrouve dans notre gros fichier app.js. Il peut être intéressant d’extraire les vendors pour différentes raisons :

  • Le temps nécessaire au build à chaque modification (pour peu qu’on utilise un plugin watch avec Grunt) augmente proportionnellement avec la taille des fichiers à parser. Ainsi, parser tout ReactJS n’est pas très optimisé
  • Les vendors changent moins souvent et peuvent avantageusement profiter d’une mise en cache navigateur. Pas besoin de recharger tout ReactJS à chaque modification d’une ligne de code dans notre application

Pour cela, on génère souvent au moins deux fichiers JS lors du build, un pour les sources de l’application et l’autre pour les vendors qui changent peu souvent. Dans notre exemple, la seule lib utilisée est ReactJS. Pourquoi alors ne pas profiter carrément d’un chargement de React via un CDN ? Tout en conservant nos require(‘react’) au sein de notre code…

Pour arriver à cela, une astuce constiste à spécifier à browserify que React est une librairie externe :

browserify: {
    options: {
        // [...],
        external: ['react']
    },
    // [...],
}

On utilise en parallèle le package npm browserify-shim, qui sert d’ordinaire à rendre browserifiables des libs qui ne le sont pas nativement :

npm install browserify-shim

On déplace alors les transform browserify directement au sein du package.json en rajoutant browserify-shim et en lui spécifiant l’emplacement du module React au sein de window.

Notre package.json ressemble alors à ça :

// package.json
{
    "browserify": {
        "transform": [
            "reactify", "browserify-shim"
        ]
    },
    "browserify-shim": {
        "react": "global:React"
    },
    "dependencies": {
        "reactify": "^0.14.0",
        "react": "^0.11.2"
    },
    "devDependencies": {
        "grunt": "^0.4.5",
        "grunt-browserify": "^3.1.0",
        "grunt-contrib-copy": "^0.7.0",
        "reactify": "^0.14.0",
        "browserify": "^6.2.0",
        "browserify-shim": "^3.8.0"
    }
}

Et notre fichier Gruntfile modifié complet :

// Gruntfile
module.exports = function(grunt) {
    grunt.initConfig({
        browserify: {
            options: {
                debug: true,
                extensions: ['.jsx'],
                external: ['react']
            },
            hello: {
                src: 'src/jsx/app.jsx',
                dest: 'public/js/app.js'
            }
        },
        copy: {
            all: {
                expand: true,
                cwd: 'src/',
                src: ['index.html'],
                dest: 'public/'
            }
        }
    });

    grunt.loadNpmTasks('grunt-browserify');
    grunt.loadNpmTasks('grunt-contrib-copy');

    grunt.registerTask('default', ['browserify', 'copy']);
};

Il ne nous reste plus qu’à charger React au sein de notre layout, via un CDN, à l’ancienne :

<!DOCTYPE html>
<html>
    <head>
        <title>Hello ReactJS</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.2/react.js"></script>
    </head>
    <body>
        <div id="content"></div>
        <script src="js/app.js"></script>
    </body>
</html>

Pour aller plus loin

Le but de cet article était de montrer comment partir d’un HelloWorld typique ReactJS et l’étendre pour y intégrer une gestion des dépendances avec Browserify, Grunt comme outil de build et le tout en chargeant ReactJS via un CDN.

Pour aller plus loin, voici quelques axes d’amélioration en vrac :

  • Générer des sourcemaps pour nos fichiers JS / JSX
  • Minifier / uglifier notre application

Vous pouvez retrouver le code final et les différentes étapes dans ces quelques commits poussés sur Github.

Publié par

Olivier Balais

Jeune ingénieur logiciel basé à Lyon (@overnetcity) passionné par les NTIC et le développement Web, je suis actuellement salarié chez Reputation VIP et effectue en parallèle des missions ponctuelles en temps que Freelance. Passionné depuis toujours par l'informatique et le développement, suite à une formation solide à l'INSA de Lyon, je me suis spécialisé dans la réalisation de bout en bout de projets web complexes.

Une réflexion au sujet de « React.js, Grunt, browserify et CDN »

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *