Intermédiaire

Packager son application React avec Webpack

reactAndWebpackDepuis quelques années, le développement d’interfaces web a énormément évolué. Entièrement basé sur le triptique HTML, CSS, javascript, on a tout d’abord commencé par dynamiser les pages statiques avec quelques effets. Par la suite, pour simplifier le code natif fastidieux à écrire, des librairies telles que la très célèbre JQuery sont apparues. La logique restant la même, le code est devenu plus lisible et l’API s’est enrichie.

Puis, plus tard, la révolution angularJS est arrivée. La force de ce framework est d’avoir changé la logique de manipulation directe du DOM via des sélecteurs, ainsi que d’avoir amené une séparation et une organisation du code côté front avec le paradigme MVW (pour le fameux Model, View, Whatever). Cette nouvelle couche d’abstraction a permis à de nombreux développeurs backend d’appréhender le développement front-end puisque la question de manipulation explicite du DOM a été mise de côté par le framework.

Jusque-là, les technologies mises en œuvre s’exécutent directement dans le navigateur. Le code que l’on écrit, à quelques opérations près (comme la minification), est celui que le navigateur va exécuter.

Depuis quelques mois (plus d’un an pour les précurseurs), cette logique tend à changer. Maintenant, le code se compile en Javascript interprétable par le navigateur. On peut le voir, en effet, à travers plusieurs technologies telles que typescript, jsx ou encore ES2015 (anciennement ES6). Cela provient de deux constats, le premier est que nous voulons, en tant que développeurs, pouvoir tirer profit des tous nouveaux standards, comme ES2015, sans attendre que l’implémentation soit faite dans chaque environnement cible (chrome, firefox, edge ou IE pour les plus courageux) ou bien encore d’utiliser des outils qui ajoutent une surcouche avec une action de compilation, ce qui est le cas de typescript.

Nous allons aujourd’hui explorer une solution qui permet d’écrire du code moderne qui nous permet de progresser sur la voie du code organisé, maintenable et testable. Nous allons créer une application React simple, mise en œuvre au travers de Webpack. Et, comme chez SOAT on aime les toutes dernières technos, on va écrire cette application en full ES2015 (imports inclus) et JSX.

Les outils

Commençons par passer en revue les outils utilisés. En fait, dire que l’on va utiliser uniquement Webpack n’est pas entièrement vrai.

Webpack

Webpack est un outil qui permet de prendre en compte la modularité du code javacript. Il peut le faire selon les différents standards (commonJs, AMD, etc.). En revanche, à lui tout seul, il est incapable de transformer le code ES2015 ou JSX.

Pour ce faire, il fait appel à des “loaders” qui vont se charger de gérer chaque aspect de l’application à compiler en javascript directement interprétable par le navigateur.

Chaque loader va intervenir dans un certain ordre pour appliquer sa transformation au code. En fait, on peut dire que Webpack est une machine à harmoniser le code hétérogène en code natif, statique et compréhensible directement par le navigateur. L’illustration de la documentation officielle parle d’elle-même :

what-is-webpack

Babel

Anciennement 6to5, Babel est un outil de “compilation” du code ES2015 et JSX en javascript natif.
Il peut étendre son champ d’action à quelques fonctionnalités expérimentales telles que l’ECMAScript2016 (ES7), par exemple. Si on va sur la page d’accueil de Babel, on voit que les deux principales features sont justement l’ES2015 et le JSX de React.
babelFrontPage

Babel fonctionne avec des presets, qui sont un ensemble de plugins permettant de compiler toutes les tâches liées à la technologie qu’on voudra compiler. En ce qui nous concerne, on aura besoin des presets suivants : babel-preset-es2015 et babel-preset-react.

L’application

Notre matière à packager sera donc une application HelloWorld qui affiche une string dans un composant React. Cette string va être récupérée, dans un second temps, depuis un webservice très simple via un GET HTTP. Au vu de la simplicité du webservice, j’ai opté pour node.js comme tehcnologie coté serveur. Coté stylage, on ajoutera un feuille de style Sass. Mais tout d’abord le composant avec la string en “dur”.

Le squelette HTML :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack-React</title>
</head>
<body>
    <div id="main"></div>
    <script src="build/bundle.js" type="text/javascript" ></script>
</body>
</html>

Le composant react :

import React from 'react';

class HelloWorld extends React.Component {
    render() {
        return (<div>
            Hello {this.props.name} !
        </div>);
    }
}


HelloWorld.propTypes = {
    name: React.PropTypes.string
};
HelloWorld.defaultProps = {
    name: 'world'
};

export default HelloWorld;

Le point d’entrée du code compilé par Webpack :

import ReactDOM from 'react-dom';
import React from 'react';
import HelloWorld from './helloWorld';

ReactDOM.render(
    <HelloWorld />,
    document.getElementById('main')
);

Tous les outils utilisés ici sont gérés par les dépendances npm et le package.json associé :

{
    "name": "webpack-react",
    "version": "1.0.0",
    "dependencies": {
        "react": "^0.14.0",
        "react-dom": "^0.14.0"
    },
    "devDependencies": {
        "babel-loader": "^6.2.4",
        "babel-polyfill": "^6.7.2",
        "babel-preset-es2015": "^6.6.0",
        "babel-preset-react": "^6.5.0",
        "express": "^4.13.4",
        "webpack": "^1.12.14"
    },
    "scripts": {
        "build": "webpack --progress --colors",
        "watch": "webpack --progress --colors --watch",
        "start": "node server.js",
        "package": "webpack --progress --colors -p"
    },
}

On peut y voir notamment les différents loaders utilisés tout au long de cet article ainsi que les alias pour les commandes webpack. Entrons dans le vif du sujet en examinant la configuration Webpack.

La configuration Webpack

Un fichier compilé, tout compris

La configuration suivante permet de compiler et d’assembler tout le code dans un seul et unique fichier. Elle se présente sous la forme de code javascript dans lequel on va exporter un objet contenant la configuration de Webpack. Cela apporte l’avantage de pouvoir utiliser n’importe quel module npm dont on aura besoin pour le build. C’est ici le cas avec le module path qui nous permet de récupérer le chemin vers le point d’entrée.

var path = require('path');
var config = {
    entry: [path.resolve(__dirname, 'src/main.jsx')],
    output: {
        path: path.resolve(__dirname, 'public/build'),
        filename: 'bundle.js'
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react']
            }
        }]
    }
};

module.exports = config;

On précise le ou les points d’entrée via l’attribut “entry” qui se présente sous forme de liste. L’attribut “output” permet, quant à lui, d’indiquer quel est le dossier-cible et quel sera le nom de notre fichier compilé.

L’attribut “resolve” permet de dire à Webpack quelles sont les extensions à résoudre dans les instructions d’imports sans qu’on ait à préciser celles-ci. Par exemple, ajouter ‘.jsx’ à la liste nous permet d’écrire

import HelloWorld from './helloWorld';

au lieu de

import HelloWorld from './helloWorld.jsx';

Cela peut sembler être de l’ordre du détail mais contribue à une meilleur lisibilité du code.

Le cœur de la configuration de la compilation se situe dans l’objet “module”, et c’est dans son attribut “loader” que l’on va préciser la liste des loaders. Cette liste est constituée d’objets où l’on va préciser la configuration de celui-ci, notamment la regexp qui permet d’identifier les fichiers concernés (attribut “test”), le nom du loader (attribut “loader”), les exclusions éventuelles (attribut “exclude”) et le paramétrage propre au module (attribut “query”).

Dans notre cas, nous utilisons Babel sur tous les fichiers possédant l’extension .jsx. On exclut les dépendances npm, qui sont scannées sinon, et on précise à Babel qu’on utilise les presets “es2015” et “react”.
Maintenant que tous les éléments sont là, nous pouvons tester:

$ npm run build
> webpack --progress --colors

Hash: d48ca5512149ed2314b4
Version: webpack 1.12.14
Time: 5834ms
    Asset    Size  Chunks             Chunk Names
bundle.js  679 kB       0  [emitted]  main
   [0] multi main 28 bytes {0} [built]
    + 160 hidden modules

Et voilà ! Avec cette première configuration, on peut créer une application react qui va se créer à partir de l’unique div de la page html. Le fichier bundle.js produit contient toutes nos librairies ainsi que notre code. Cela couvre tous les cas de figure où l’on maitrise tout le contexte de l’application react.

Nous allons voir quelques variations de la configuration pour tenter de répondre à des cas de figures moins favorables.

Une librairie de composants sur mesure

Commençons ce paragraphe en rappelant que React est une librairie qui permet de construire des interfaces graphiques et qu’elle est orientée composant. Ce qui est très pratique avec cet aspect c’est qu’on peut découper tout une application pour l’assembler comme bon nous semble.
Webpack permet de packager le code en un seul fichier et de l’utiliser comme une librairie de composant l’exposant sous forme de module à distribuer. C’est très pratique car le module exposé est isolé du contexte de l’application.

Pour ce faire modifions la configuration :

var path = require('path');
var config = {
    entry: [path.resolve(__dirname, 'src/main.jsx')],
    output: {
        path: path.resolve(__dirname, 'public/build'),
        filename: 'bundle.js',
        library: 'customLib',
        libraryTarget: 'var'
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react']
            }
        }]
    }
};
module.exports = config;

En recompilant l’application, si on ouvre les devtools dans un navigateur, on peut voir qu’en cherchant la variable customLib, on obtient :

>customLib
  Object {}

si on fait la même chose sans :

>customLib
VM835:2 Uncaught ReferenceError: customLib is not defined(…)

En changeant le main.jsx :

import ReactDOM from 'react-dom';
import React from 'react';
import HelloWorld from './helloWorld';

const lib = {
    helloWorld: <HelloWorld />
};

ReactDOM.render(<HelloWorld />, document.getElementById('main'));

module.exports = lib;

De cette manière, lib est exposé sous le nom défini comme output.targetlib. On peut bien évidemment remplacer l’export commonjs

module.exports = lib;

par

export default lib;

dans un contexte ECMAScript2015.

Ici, on a simplement exporté la variable pour qu’elle soit accessible globalement dans le navigateur. Webpack propose d’autres moyens d’exporter sous forme de librairie selon le contexte :

"var" - Export by setting a variable: var Library = xxx (default)
"this" - Export by setting a property of this: this["Library"] = xxx
"commonjs" - Export by setting a property of exports: exports["Library"] = xxx
"commonjs2" - Export by setting module.exports: module.exports = xxx
"amd" - Export to AMD (optionally named - set the name via the library option)
"umd" - Export to AMD, CommonJS2 or as property in root

On utilisera donc le mode approprié selon le contexte de son application.

Notre code uniquement

Jusqu’à maintenant, on a toujours inclus React et ReactDOM directement dans le bundle. Mais imaginons que la librairie de composants s’ajoute à d’autres déjà existantes, il serait mal avisé que chacune des librairies contienne sa propre version de react. Cela peut faire grossir le bundle inutilement et garder une liste de toutes les versions différentes serait fastidieux. Ainsi, on peut imaginer que React serait inclus par un autre moyen au sein de l’application et ainsi l’exclure de notre bundle.

var path = require('path');
var config = {
    entry: [path.resolve(__dirname, 'src/main.jsx')],
    output: {
        path: path.resolve(__dirname, 'public/build'),
        filename: 'bundle.js',
        library: 'customLib',
        libraryTarget: 'var'
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react']
            }
        }]
    },
    externals: {
        // exclude react
        "react": "React",
        "react-dom": "ReactDOM"
    },
};

module.exports = config;

Attention : pour exclure React complètement, il faut préciser react ET reactDOM, autrement le bundle ne se retrouvera pas ou peu allégé. En effet, on peut exclure ReactDOM et garder React mais l’inverse n’est pas vrai étant donné la dépendance de l’une envers l’autre.
Avant exclusion :

> webpack --progress --colors
Hash: 7d25d4f02c611aedf34e
Version: webpack 1.12.14
Time: 1256ms
    Asset    Size  Chunks             Chunk Names
bundle.js  679 kB       0  [emitted]  main
   [0] multi main 28 bytes {0} [built]
    + 160 hidden modules

Après exclusion :

> webpack --progress --colors
Hash: 74a85b47a3edf43b1b48
Version: webpack 1.12.14
Time: 670ms
    Asset     Size  Chunks             Chunk Names
bundle.js  4.96 kB       0  [emitted]  main
   [0] multi main 28 bytes {0} [built]
    + 4 hidden modules

On voit donc qu’on est passé de 160 modules à 4.

Ajoutons du style

Comme promis plus haut dans cet article, nous allons ajouter du style. Et quel style ! Ajoutons une bordure et de la couleur. Et tant qu’à faire, puisqu’on compile du code depuis le début, le CSS c’est bien mais le SASS (ou LESS) c’est mieux.
style.scss:

div {
  border: solid;
  .text {
    color: red;
  }
}

On installe les 3 loaders qui permettent de gérer les styles CSS et SASS:

npm i --save-dev style-loader css-loader sass-loader node-sass

Et on ajoute nos nouveaux loaders :

var path = require('path');
var config = {
    entry: [path.resolve(__dirname, 'src/main.jsx')],
    output: {
        path: path.resolve(__dirname, 'public/build'),
        filename: 'bundle.js',
        library: 'customLib',
        libraryTarget: 'var'
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react']
            }
        },{
            test: /\.css$/,
            loader: 'style!css'
        },{
            test: /\.scss$/,
            loader: 'style!css!sass'
        }]
    },
};
module.exports = config;

Nous voilà prêts à inclure notre feuille de style ! Au passage, on utilise une autre syntaxe autorisée par Webpack : tout d’abord, on peut omettre le suffixe “-loader” pour chacun d’eux et on peut remplacer une liste [‘loader1’, ‘loader2’] par ‘loader1!loader2’.

L’inclusion de la feuille de style dans helloWorld.jsx :

import React from 'react';
import './style.scss';

class HelloWorld extends React.Component {
  render() {
    return (<div id="helloWorld">
      <div class="text">
        Hello {this.props.name} !
      </div>
    </div>);
  }
}

HelloWorld.propTypes = {
  name: React.PropTypes.string
};
HelloWorld.defaultProps = {
  name: 'world'
};

export default HelloWorld;

Nous pouvons admirer maintenant le nouveau style (d’un goût discutable) du helloWorld noir et rouge.

Exemple de polyfill: fetch

J’avais parlé de webservice plus haut dans cet article, nous allons nous en occuper maintenant. Pour ce faire, nous allons utiliser une API en cours de spécification mais d’ores et déjà assez populaire chez les développeurs React : fetch. Cette api permet de remplacer l’antique et mal nommé XMLHttpRequest.

Ajoutons un nouveau composant, container.jsx :

import React from 'react';
import HelloWorld from './helloWorld';

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      string: 'You'
    }
  }

  componentDidMount() {
    fetch('/hello', {
      method: 'GET',
      headers: new Headers({
        'Content-Type': 'application/json',
      }),
    })
      .then((response) => {
        if (!response.ok) {
          throw Error(response);
        }
        return response.json();
      })
      .then(json => this.setState(json));
  }

  render() {
    return (<HelloWorld name={this.state.string} />);
  }
}
export default Container;

Le webservice ajouté au server.js :

var path = require('path');
var express = require('express');
var app = express();

app.set('port', (process.env.PORT || 3000));
app.set('host', (process.env.HOST || '127.0.0.1'));

app.use('/', express.static(path.join(__dirname, 'public')));

app.get('/hello', function (req, res) {
  res.json({ string: 'World from WS' });
});

app.listen(app.get('port'), app.get('host'), function () {
  console.log('Server started: ' + app.get('host') + ':' + app.get('port') + '/');
});

Pour être compatible avec les navigateurs qui n’implémentent pas fetch (IE), on ajoute le plugin webpack du polyfill fetch ainsi que le babel-polyfill qui fournit un environnement full ES2015 incluant les promises :

npm i babel-polyfill --save-dev
var path = require('path');
var webpack = require('webpack');
var config = {
    entry: ['babel-polyfill',path.resolve(__dirname, 'src/main.jsx')],
    output: {
        path: path.resolve(__dirname, 'public/build'),
        filename: 'bundle.js',
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react']
            }
        },{
            test: /\.css$/,
            loader: 'style!css'
        },{
            test: /\.scss$/,
            loader: 'style!css!sass'
        }]
    },
    plugins: [
        new webpack.ProvidePlugin({
            fetch: 'imports?this=>global!exports?global.fetch!whatwg-fetch'
        })
    ]
};
module.exports = config;

Il existe d’autres plugins, comme Define, qui permettent de gérer les variables d’environnement directement dans le code pour faire de l’inclusion conditionnelle de fichiers, par exemple. Cela peut être utile si l’on veut avoir des fichiers en moins pour un environnement de production.
A ce propos, en ajoutant l’option -p à la commande, Webpack lance un build pour un environement de production. Il effectue des opérations comme inclure la version minifiée de react à la compilation, minifier le code etc.

Nous avons au final un composant HelloWorld qui affiche une string depuis un webservice développé entièrement à l’aide des dernières technologies disponibles.

Le code complet de cet article est disponible sur le github de Soat.

Pour terminer cet article, je signale que Webpack fournit un serveur de live reload qui permet la compilation et l’actualisation de la page courante à la volée, très pratique lors des développements au quotidien : la doc ici.

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT

Nombre de vue : 1233

AJOUTER UN COMMENTAIRE