I. Introduction▲
Depuis quelques années, le développement d'interfaces web a énormément évolué. Entièrement basé sur le triptyque 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 back-end 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 tout 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.
I-A. 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.
I-B. Webpack▲
Webpack est un outil qui permet de prendre en compte la modularité du code JavaScript. 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 :
I-C. 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.
Babel fonctionne avec des presets, qui sont un ensemble de plug-ins 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.
II. 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 technologie côté serveur. Côté stylage, on ajoutera une feuille de style Sass. Mais débutons par le composant avec la string en « dur ».
Le squelette HTML :
Le composant React :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
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 :
2.
3.
4.
5.
6.
7.
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é :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
{
"name"
:
"webpack-react"
,
"version"
:
"1.0.0"
,
"dependencies"
:
{
"react"
:
"^0.14.0"
,
"react-dom"
:
"^0.14.0"
},
"devDependencies"
:
{
"babel-core"
:
"^6.2.4"
,
"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 ce tutoriel ainsi que les alias pour les commandes Webpack. Entrons dans le vif du sujet en examinant la configuration Webpack.
III. La configuration Webpack▲
III-A. Un fichier compilé, tout compris▲
La configuration suivante permet de compiler et d'assembler tout le code dans un seul 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
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 meilleure 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 du « module », 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:
2.
3.
4.
5.
6.
7.
8.
9.
$ 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 maîtrise tout le contexte de l'application React.
Nous allons voir quelques variations de la configuration pour tenter de répondre à des cas de figure moins favorables.
III-B. 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 toute 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
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 :
2.
3.
4.
5.
6.
"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.
III-C. 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
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 :
2.
3.
4.
5.
6.
7.
8.
> 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 :
2.
3.
4.
5.
6.
7.
8.
> 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.
III-D. 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.
2.
3.
4.
5.
6.
div {
border
:
solid;
.
text
{
color
:
red;
}
}
On installe les trois 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
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.
III-E. Exemple de polyfill: fetch▲
J'avais parlé de webservice plus haut dans ce tutoriel, 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
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 plug-in webpack du polyfill fetch ainsi que le babel-polyfill qui fournit un environnement full ES2015 incluant les promises :
npm i babel-polyfill --save-dev
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
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 plug-ins, 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.
À ce propos, en ajoutant l'option -p à la commande, Webpack lance un build pour un environnement de production. Il effectue des opérations comme inclure la version minifiée de React à la compilation, minifier le code, etc.
Nous avons finalement 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.