I. Exécution côté client avec les Modules ECMAScript▲
Pour commencer, utilisons un navigateur récent (Chrome, Firefox, Edge, Safari…) et faisons des choses très simples. En ECMAScript version 6 (« ES6 ») — qui succède la très populaire ECMAScript version 5 (« ES5 ») progressivement dans tous les navigateurs — il existe une manière de packager et servir du code JavaScript sous forme d'unités de code. Ces unités de code se suffisent à elles-mêmes et peuvent être réutilisées par d'autres unités de code. C'est ce qu'on appelle les Modules ECMAScript. Voici les étapes de mise en place :
- je crée un module JavaScript grâce au nouveau mot clé réservé du langage export ;
- et j'utilise un module JavaScript grâce au nouveau mot clé réservé du langage import.
I-A. Architecture▲
Testons donc cela dans un navigateur à travers l'architecture de fichier suivante :
2.
3.
4.
5.
isomorphism/
├─ javascripts/
│ ├─ operation.js
│ └─ isomorphic.js
└─ es6.htm
Nous allons donc remplir le fichier es6.htm avec le contenu suivant :
es6.htm (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
<!
DOCTYPE
html
>
<
html
lang
=
"
en
"
>
<
head>
<
meta
charset
=
"
utf-8
"
/
>
<
title>
ES6 example<
/
title>
<
/
head>
<
body>
<
section
class
=
"
main-content
"
>
<
h1>
Instructions:<
/
h1>
<
p>
Open console with F12.<
/
p>
<
/
section>
<!--
Appel
des
différents
fichiers
à
faire
exécuter
par
le
moteur
JavaScript
du
navigateur.
-->
<script src
=
"
javascripts/operation.js
"
>
</
script>
<script src
=
"
javascripts/isomorphic.js
"
>
</
script>
<
/
body>
<
/
html>
Nous allons ensuite nous créer un module JavaScript dans le fichier operation.js :
javascripts/operation.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
/*
Export
direct.
*/
export default function
(
number) {
return {
round
:
Math.round
(
number),
floor
:
Math.floor
(
number),
ceil
:
Math.ceil
(
number)
}
;
}
/*
Export
nommé
`addition`.
*/
export function addition
(
number1,
number2) {
return number1 +
number2;
}
/*
Export
nommé
`substraction`.
*/
export function substraction
(
number1,
number2) {
return number1 -
number2;
}
/*
Export
nommé
`multiplication`.
*/
export function multiplication
(
number1,
number2) {
return number1 *
number2;
}
/*
Export
nommé
`division`.
*/
export function division
(
number1,
number2) {
return number1 /
number2;
}
Et nous allons créer le cœur du programme dans un module isomorphic.js qui fera office de contrôleur :
javascripts/isomorphic.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/*
Récupération
du
module
direct
depuis
`export
default
function
()`
*/
import tools from "
./operation.js
"
;
/*
Récupération
des
exports
nommés
du
module
avec
`export
function
<name>()`
*/
import {
addition,
substraction,
multiplication,
division }
from "
./operation.js
"
;
/*
Variables
à
tester.
*/
var number1 =
13
,
number2 =
7
.
7
;
/*
Utilisation
des
fonctions
de
nos
modules.
*/
console.log
(
'
addition
'
,
addition
(
number1,
number2));
console.log
(
'
substraction
'
,
substraction
(
number1,
number2));
console.log
(
'
multiplication
'
,
multiplication
(
number1,
number2));
console.log
(
'
division
'
,
division
(
number1,
number2));
console.log
(
'
round
'
,
tools
(
number2).
round);
console.log
(
'
floor
'
,
tools
(
number2).
floor);
console.log
(
'
ceil
'
,
tools
(
number2).
ceil);
I-B. Quelques erreurs▲
Nous allons donc ouvrir le fichier es6.htm dans le navigateur et ouvrir notre console avec F12.
Les erreurs suivantes sont affichées (dans Chrome) :
Access to Script at 'file:///<path/to/your/workspace>/isomorphism/javascripts/operation.js' from origin 'null' has been blocked by CORS policy: Invalid response. Origin 'null' is therefore not allowed access.
Access to Script at 'file:///<path/to/your/workspace>/isomorphism/javascripts/isomporphic.js' from origin 'null' has been blocked by CORS policy: Invalid response. Origin 'null' is therefore not allowed access.
Cela est dû au fait qu'il vous faut l'autorisation d'utiliser un module depuis un autre nom de domaine que le vôtre à cause du mécanisme de « Cross-origin resource sharing » des navigateurs. Vous allez me dire que vos fichiers .js sont pourtant sur le même serveur web que votre page .htm ? En fait, pour que ce soit le cas, il faudrait que votre page soit sur un serveur web ! Aussi dans l'URL de votre page dans le navigateur, il faudrait que file:///<path/to/your/workspace>/es6.htm soit remplacée, par exemple par http://<your-local-domain-name>/es6.htm. Lançons donc un serveur web.
De mon côté, je vais utiliser Node.js qui une fois installé me donne accès à la commande npm install -g node-atlas, ce qui me permet d'utiliser la commande node-atlas. Celle-ci lance un serveur web basique là où elle est lancée. Vous pouvez tout autant utiliser http-server ou votre propre serveur Apache, etc. pour tester ça.
Une fois le serveur web lancé, en vous rendant à http://<your-local-domain-name>/es6.htm, vous aurez cette fois l'erreur suivante :
Uncaught SyntaxError: Unexpected token export
Uncaught SyntaxError: Unexpected token import
Cela vient du fait que pour utiliser des modules JavaScript, il faut le préciser dans le type de la balise <script>. Notre code HTML précédent devient donc :
es6.htm (code source)
I-C. Résultat▲
Cette fois la magie opère ! Vous constaterez dans votre console les sorties suivantes :
2.
3.
4.
5.
6.
7.
addition 20.7
substraction 5.3
multiplication 100.10000000000001
division 1.6883116883116882
round 8
floor 7
ceil 8
Vous pouvez aussi voir ce résultat en live en vous rendant à cette adresse qui se sert d'un serveur web GitHub Pages pour faire fonctionner l'exemple ES6.
II. Exécution côté serveur des Modules ECMAScript avec Node.js▲
L'idée ici va être de faire exécuter le fichier isomorphic.js du côté serveur. Il va faire appel à operation.js afin d'obtenir le même résultat que côté client dans votre console. Pour cela nous allons utiliser la commande :
>
node ./javascripts/isomorphic.js
depuis le dossier où se situe actuellement es6.htm.
Mais faire cela nous renvoie l'erreur :
SyntaxError: Unexpected token import
...
II-A. Fonctionnalité expérimentale▲
Pour pouvoir exécuter notre fichier isomorphic.js côté serveur, il va falloir utiliser une fonctionnalité expérimentale de Node.js car, à l'heure actuelle, les Modules ECMAScript (« ESM ») ne sont pas supportés par Node.js en standard. En réalité, Node.js a déjà son propre système de chargement de module basé sur une spécification appelée CommonJS. Parce que Node.js a déjà son système d'import, appelé require, le meilleur moyen pour lui de savoir si un fichier doit être interprété en tant que Module ECMAScript ou en tant que module Node.js standard est de vérifier l'extension du fichier. C'est pourquoi un fichier JavaScript écrit sous forme de module ne doit plus avoir l'extension .js mais l'extension .mjs. Dans ce cas, Node.js sait que c'est un Module ECMAScript et utilise le système de chargement de module ESM et non CommonJS.
Nous allons donc dans un premier temps renommer nos fichiers operation.js et isomorphic.js en operation.mjs et isomorphic.mjs :
2.
3.
4.
5.
isomorphism/
├─ javascripts/
│ ├─ operation.mjs
│ └─ isomorphic.mjs
└─ es6.htm
Et puisque le nom a changé, notre fichier isomorphic.mjs va maintenant faire appel à operation.mjs.
javascripts/isomorphic.mjs (code source)
II-B. Résultat▲
Il est à présent possible d'obtenir le même résultat que côté client avec la commande :
>
node --experimental-modules ./javascripts/isomorphic.mjs
Vous obtiendrez alors la sortie :
2.
3.
4.
5.
6.
7.
addition 20.7
substraction 5.3
multiplication 100.10000000000001
division 1.6883116883116882
round 8
floor 7
ceil 8
II-C. Et le client ?▲
Pour finir, afin de toujours rendre opérationnel notre appel depuis http://<your-local-domain-name>/es6.htm, nous allons également changer les chemins vers les nouveaux fichiers operation.mjs et isomorphic.mjs :
Nous avons ici un exemple de fichiers ES6 parfaitement isomorphiques !
Vous pouvez revoir ce résultat en live en vous rendant sur le serveur web GitHub Pages.
III. Méthodes ES5 pour l'import / export▲
Comme vous avez pu le constater, la fonctionnalité de Modules ECMAScript est expérimentale côté serveur et pas encore totalement supportée par tous les navigateurs côté client, car elle est introduite avec ES6. La question que l'on peut se poser est la suivante : est-il nécessaire d'utiliser une syntaxe ES6 et un Module ECMAScript pour faire de l'isomorphisme ? La réponse est non. Il est tout à fait possible d'arriver au même résultat en utilisant les modules CommonJS utilisés par Node.js et en mimant ce mécanisme côté client.
Nous allons ajouter les fichiers operation.js et isomorphic.js qui n'utilisent pas la syntaxe de module ES6, et créer un nouveau fichier es5.htm qui utilisera ces fichiers.
2.
3.
4.
5.
6.
7.
8.
isomorphism/
├─ javascripts/
│ ├─ operation.js
│ ├─ operation.mjs
│ ├─ isomorphic.js
│ └─ isomorphic.mjs
├─ es5.htm
└─ es6.htm
III-A. Côté serveur▲
Nous allons par ailleurs dans chacun de ces fichiers utiliser l'export CommonJS de Node.js. Celui-ci fonctionne avec les propriétés module.exports et require.
Nous permettons donc l'export de nos fonctionnalités :
javascripts/operation.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
/*
Export
CommonJS
de
Node.js.
*/
module.
exports =
function
(
number) {
return {
/*
Export
direct.
*/
round
:
Math.round
(
number),
floor
:
Math.floor
(
number),
ceil
:
Math.ceil
(
number),
/*
Export
fonction
`addition`.
*/
addition
:
function
(
number1,
number2) {
return number1 +
number2;
}
,
/*
Export
fonction
`substraction`.
*/
substraction
:
function
(
number1,
number2) {
return number1 -
number2;
}
,
/*
Export
fonction
`multiplication`.
*/
multiplication
:
function
(
number1,
number2) {
return number1 *
number2;
}
,
/*
Export
fonction
`division`.
*/
division
:
function
(
number1,
number2) {
return number1 /
number2;
}
}
;
}
;
Puis nous les exécutons depuis le script d'appel :
javascripts/isomorphic.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
Variables
à
tester.
*/
var number1 =
13
,
number2 =
7
.
7
,
/*
Récupération
du
module
direct
depuis
`module.exports`
*/
tools =
require
(
'
.
/
operation
.
js
'
),
operation =
tools
(
);
/*
Utilisation
des
fonctions
de
notre
import.
*/
console.log
(
'
addition
'
,
operation.addition
(
number1,
number2));
console.log
(
'
substraction
'
,
operation.substraction
(
number1,
number2));
console.log
(
'
multiplication
'
,
operation.multiplication
(
number1,
number2));
console.log
(
'
division
'
,
operation.division
(
number1,
number2));
console.log
(
'
round
'
,
tools
(
number2).
round);
console.log
(
'
floor
'
,
tools
(
number2).
floor);
console.log
(
'
ceil
'
,
tools
(
number2).
ceil);
Nous obtenons alors avec la commande :
>
node ./javascripts/isomorphic.js
le résultat suivant :
2.
3.
4.
5.
6.
7.
addition 20.7
substraction 5.3
multiplication 100.10000000000001
division 1.6883116883116882
round 8
floor 7
ceil 8
III-B. Côté client▲
Alimentons alors côté client notre fichier es5.htm :
es5.htm (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<!
DOCTYPE
html
>
<
html
lang
=
"
en
"
>
<
head>
<
meta
charset
=
"
utf-8
"
/
>
<
title>
ES5 example<
/
title>
<
/
head>
<
body>
<
section
class
=
"
main-content
"
>
<
h1>
Instructions:<
/
h1>
<
p>
Open console with F12.<
/
p>
<
/
section>
<script>
var module =
{
}
;
</
script>
<script src
=
"
javascripts/operation.js
"
>
</
script>
<script>
var require =
function
(
) {
return module.
exports;
}
</
script>
<script src
=
"
javascripts/isomorphic.js
"
>
</
script>
<
/
body>
<
/
html>
Ce qui nous permet d'obtenir à l'adresse http://<your-local-domain-name>/es5.htm, en allant dans la console derrière F12 le résultat :
2.
3.
4.
5.
6.
7.
addition 20.7
substraction 5.3
multiplication 100.10000000000001
division 1.6883116883116882
round 8
floor 7
ceil 8
Nous avons alors « presque » à faire à de l'isomorphisme, car nous avons dû ajouter les morceaux de code :
et :
pour simuler le comportement de CommonJS côté client.
Vous pouvez aussi voir ce résultat en live en vous rendant à cette adresse qui se sert d'un serveur web GitHub Pages pour faire fonctionner l'exemple ES5.
IV. Isomorphisme exploitable pour un site web avec Vanilla JS et Node.js▲
Vous aurez probablement remarqué que les trois premières parties de cet article vous font une belle jambe pour faire un site web. Certes, le résultat est exécuté de la même manière avec une commande node (côté serveur) qu'avec un appel depuis une balise <script> mais vous ne pouvez rien en faire. Effectivement, la grande différence entre client et serveur c'est que ce que vous ferez côté client consistera à manipuler le DOM alors que ce que vous ferez côté serveur consistera à générer une réponse HTTP à envoyer au client. On est donc loin des messages à afficher dans la console !
IV-A. Partie cliente, partie serveur et partie isomorphique▲
Nous pouvons donc voir assez rapidement que la totalité du code ne pourra pas être isomorphique. Il y aura forcément :
- coté serveur, du code dédié à faire le pont entre les fichiers et données stockées sur le serveur et leurs envois par réponse HTTP. Ce sera le code uniquement serveur ;
- côté client, du code dédié à faire le pont entre ce que l'on récupère en source HTML ou par requête XMLHttpRequest et le DOM. Ce sera le code uniquement client.
Cependant, hormis ces mécanismes, la totalité du code restant pourra être utilisée aussi bien pour générer côté serveur la réponse HTTP dont va se servir le client pour générer son DOM lors du premier affichage, que pour hydrater le code côté client ou générer toutes les nouvelles pages visitées sans solliciter le serveur.
C'est à cette condition que nous pourrons réellement estimer que l'on développe une application isomorphique.
Voyons cela par l'exemple côté navigateur sans bibliothèque avec Vanilla JS et côté serveur avec Node.js !
IV-B. Le serveur HTTP▲
Nous allons donc créer un serveur Node.js dans le fichier server.js en utilisant l'API HTTP native de Node.js ainsi que le module communautaire JSDOM permettant de manipuler virtuellement le DOM côté serveur afin d'exploiter du code isomorphique. Nous aurions pu utiliser Express ou NodeAtlas pour faire cela avec facilité, mais ce sera un bon exercice de compréhension complète de A à Z sans zones d'ombres.
Partons de la structure actuelle à laquelle nous allons rajouter notre fichier server.js pour développer le code serveur non isomorphique servant les fichiers demandés au client ainsi que le fichier package.json pour permettre l'installation du DOM virtuel JSDOM. Nous allons également créer un fichier layout.htm qui va servir de base HTML pour tous les fichiers renvoyés par le serveur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
isomorphism/
├─ javascripts/
│ ├─ isomorphic.js
│ ├─ isomorphic.mjs
│ ├─ operation.js
│ ├─ operation.mjs
├─ es5.htm
├─ es6.htm
├─ layout.htm
├─ server.js
└─ package.json
Remplissons le fichier package.json avec :
package.json (code source)
{
}
Puis exécutons la commande :
>
npm install --save jsdom
Ce qui va remplir le fichier package.json ainsi :
package.json (code source)
2.
3.
4.
5.
{
"
dependencies
"
:
{
"
jsdom
"
:
"
^11.5.1
"
}
}
et créer un fichier package-lock.json.
Grâce à cela, le module communautaire npm JSDOM et ses dépendances seront installés dans le dossier node_modules.
Basiquement, notre fichier layout.htm ressemblera à cela :
layout.htm (code source)
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.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
<!
DOCTYPE
html
>
<
html
lang
=
"
en
"
>
<
head>
<
base
href
=
"
MyBase
"
/
>
<
meta
charset
=
"
utf-8
"
/
>
<
title>
Isomporphic example<
/
title>
<!--
Un
peu
de
CSS
pour
faire
changer
les
pages
en
changeant
l'opacité.
-->
<style>
/*
Chaque
`div`
`layout`
va
s'afficher
les
unes
sur
les
autres
permettant
de
faire
disparaitre
progressivement
celle
du
dessus…
*/
.
layout
{
position:
absolute
;
width:
100
%;
height:
100
%;
top
:
0
;
left
:
0
;
opacity:
1
;
background-color:
#fff
;
-webkit-transition:
opacity 1s
ease
;
-moz-transition:
opacity 1s
ease
;
-ms-transition:
opacity 1s
ease
;
-o-transition:
opacity 1s
ease
;
transition:
opacity 1s
ease
;
}
/*
…
pour
rendre
visible
celle
du
dessous.
Nous
verrons
cela
plus
loin.
*/
.
change
{
opacity:
0
;
z-index:
2
;
}
</
style>
<
/
head>
<
body>
<!--
Ici
sera
montée
la
page
demandée
par
le
routeur
côté
serveur
ou
sera
hydratée
la
page
demandée
côté
client.
-->
<
div
class
=
"
layout
"
>
<
/
div>
<!--
Ici
sera
exécuté
la
partie
cliente
-->
<script src
=
"
javascripts/client.js
"
>
</
script>
<
/
body>
<
/
html>
Remplissons maintenant le fichier server.js avec le code serveur dédié :
server.js (code source)
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.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
/*
Récupération
de
l'API
native
HTTP
*
pour
faire
des
échanges
client-server
*
(l'équivalent
de
APACHE).
*/
var http =
require
(
'
http
'
),
/*
Récupération
de
l'API
native
File
System
*
pour
lire
et
écrire
dans
des
fichiers
*
sur
le
serveur.
*/
fs =
require
(
'
fs
'
),
/*
Récupération
de
la
bibliothèque
JSDOM
*
pour
lire
et
écrire
dans
des
fichiers
*
sur
le
serveur.
*/
JSDOM =
require
(
'
jsdom
'
).
JSDOM,
/*
Port
d'écoute
de
notre
site
web.
*/
httpPort =
8080
,
/*
Nom
de
domaine
de
notre
site
web.
*/
httpDomain =
'
localhost
'
;
/*
Création
du
serveur
web
avec
*
récupération
de
toutes
les
requêtes
faites
*
par
le
navigateur
dans
`request`
et
un
objet
*
`response`
pour
renvoyer
le
contenu
HTML
demandé
*
au
navigateur.
*/
http.createServer
(
function
(
request,
response) {
var router,
file,
statusCode,
contentType;
/*
Cas
des
demandes
d'adresse
finissant
par
`/`.
*/
if
(
/
\/
$/
g.test
(
request.
url)) {
/*
Le
site
répondra
donc
à
:
*
-
`http://localhost:8080/`
*
-
`http://localhost:8080/about-us/`
*
-
`http://localhost:8080/contact-us/`
*
...
*/
router =
{
'
/
'
:
'
index
'
,
'
/
about
-
us
/
'
:
'
overview
'
,
'
/
contact
-
us
/
'
:
'
contact
'
}
;
/*
...ou
à
n'importe
quoi
finissant
par
`/`
*
`http://localhost:8080/.+/`.
*/
file =
router[
request.
url]
|
|
'
error
'
;
/*
Si
l'adresse
est
trouvée
dans
`router`,
*
la
`response`
sera
valide
et
en
`200`
sinon
*
ce
sera
une
page
inexistante
d'erreur
`404`.
*/
statusCode =
(
router[
request.
url]
) ?
200
:
404
;
/*
Récupération
de
la
structure
globale
de
*
chaque
page
dans
le
fichier
`layout.htm`.
*/
fs.readFile
(
'
layout
.
htm
'
,
function
(
err,
layout) {
if
(
err) {
/*
Information
en
cas
d'erreur.
*/
console.log
(
'
We
cannot
open
layout
file
.
'
,
err);
/*
Renvoi
d'une
page
serveur
500
en
cas
d'erreur.
*/
response.writeHead
(
500
,
{
}
);
/*
Fin
de
la
transaction.
*/
response.end
(
'
'
);
}
/*
Ouverture
du
code
isomorphique
correspondant
aux
pages
:
*
-
`views/index.htm`
si
`http://localhost:8080/`
est
demandée
*
-
`views/overview.htm`
si
`http://localhost:8080/about-us/`
est
demandée
*
-
`views/contact.htm`
si
`http://localhost:8080/contact-us/`
est
demandée
*
-
`views/error.htm`
si
`http://localhost:8080/.+/`
est
demandée.
*/
fs.readFile
(
'
views
/
'
+
file +
'
.
htm
'
,
'
utf
-
8
'
,
function
(
err,
content) {
var dom =
new JSDOM
(
layout);
if
(
err) {
/*
Information
en
cas
d'erreur.
*/
console.log
(
'
We
cannot
open
'
+
file +
'
view
file
.
'
,
err);
}
/*
Récupération
de
la
balise
`<base
href="MyBase"
/>`
*/
dom.
window
.
document
.getElementsByTagName
(
'
base
'
)[
0
]
/*
et
changement
en
`<base
href="http://localhost:8080/"
/>`.
*/
.setAttribute
(
'
href
'
,
'
http
:
/
/
'
+
httpDomain +
'
:
'
+
httpPort +
'
/
'
);
/*
Récupération
de
la
balise
`<div
class="layout"></div>`
*/
dom.
window
.
document
.getElementsByClassName
(
'
layout
'
)[
0
]
/*
et
changement
de
leur
contenu
par
le
contenu
*
généré
à
partir
d'appels
isomorphiques
des
fichiers
:
*
-
`require('./views/index.js')(<contenu
de
`views/index.htm`>,
<objet
window
virtuel>)`
ou
*
-
`require('./views/overview.js')(<contenu
de
`views/overview.htm`>,
<objet
window
virtuel>)`
ou
*
-
`require('./views/contact.js')(<contenu
de
`views/contact.htm`>,
<objet
window
virtuel>)`
ou
*
-
`require('./views/error.js')(<contenu
de
`views/error.htm`>,
<objet
window
virtuel>)`
*/
.
innerHTML =
require
(
'
.
/
views
/
'
+
file +
'
.
js
'
)(
content,
dom.
window
)
/*
et
contenu
dans
la
propriété
`template`
*
(par
ex.
:
`'<div
class="layout"><h1>Welco[...]</ul></div>'`).
*/
.
template;
/*
Création
des
entêtes
de
réponse
HTTP
*
pour
un
fichier
HTML
*
soit
en
code
`200`
soit
`404`.
*/
response.writeHead
(
statusCode,
{
'
Content
-
Type
'
:
'
text
/
html
;
charset
=
utf
-
8
'
}
);
/*
Fin
de
la
transaction
avec
envoi
*
du
fichier
complet
(par
ex.
`'<!DOCTYPE
html><html
lang="en"><head>[...]
*
<div
class="layout"><h1>Welco[...]</ul></div>[...]
*
</body></html>'`).
*/
response.end
(
dom.serialize
(
));
}
);
}
);
/*
Cas
de
toutes
les
autres
demandes
du
navigateur
*
fait
pour
récupérer
directement
les
fichiers
*
de
ressources
statiques.
*/
}
else {
/*
Retrait
du
`/`
de
départ
pour
tentative
*
d'ouverture
du
fichier.
(par
ex.
la
requête
*
`/javascripts/client.js`
tentera
d'ouvrir
le
*
fichier
`javascripts/client.js`).
*/
file =
request.
url.slice
(
1
);
/*
Ouverture
du
fichier
statique
demandé
*/
fs.readFile
(
file,
'
utf
-
8
'
,
function
(
err,
content) {
/*
Par
défaut
on
estime
que
le
fichier
est
trouvé...
*/
statusCode =
200
;
/*
et
n'a
pas
de
`'Content-type'`
particulier
*/
contentType =
{
}
;
/*
Association
d'un
fichier
de
`'Content-type'`
*
par
`application/javascript`
si
l'extension
*
du
fichier
est
`'.js'`.
*/
if
(
/
\.
js$/
g.test
(
file)) {
contentType =
{
'
Content
-
Type
'
:
'
application
/
javascript
;
charset
=
utf
-
8
'
}
;
}
/*
Association
d'un
fichier
de
`'Content-type'`
*
par
`text/html`
si
l'extension
*
du
fichier
est
`'.htm'`.
*/
if
(
/
\.
htm$/
g.test
(
file)) {
contentType =
{
'
Content
-
Type
'
:
'
text
/
html
;
charset
=
utf
-
8
'
}
;
}
if
(
err) {
/*
Si
le
ficher
demandé
n'existe
pas
*
on
retourne
un
fichier
en
erreur
*
400
à
contenu
vide.
*/
statusCode =
404
;
contentType =
{
}
;
content =
'
'
;
/*
Information
en
cas
d'erreur
*/
console.log
(
'
We
cannot
open
'
+
file +
'
asset
file
.
'
,
err);
}
/*
Création
des
entêtes
de
réponse
HTTP
*
pour
un
fichier
statique
*
soit
en
code
`200`
soit
`404`.
*/
response.writeHead
(
statusCode,
contentType);
/*
Fin
de
la
transaction
avec
envoi
*
du
contenu
du
fichier
s'il
existe
*
ou
d'un
contenu
vide
s'il
n'existe
pas.
*/
response.end
(
content);
}
);
}
/*
Démarrage
du
serveur
web
*/
}
).listen
(
httpPort,
function
(
) {
/*
Envoi
d'un
message
à
la
console
côté
serveur
*
quand
le
serveur
est
démarré
et
prêt
à
répondre
*
aux
demandes
du
client.
*/
console.log
(
'
Server
listening
on
:
http
:
/
/
'
+
httpDomain +
'
:
'
+
httpPort +
'
/
'
);
}
);
IV-C. Les fichiers isomporphiques▲
À ce stade, le fichier server.js va retourner une réponse HTTP différente à votre navigateur en fonction de l'adresse demandée.
- Pour http://localhost:8080/, ce sont les fichiers de vue views/index.html et de modèle views/index.js qui vont être impliqués.
- Pour http://localhost:8080/about-us/, ce sont les fichiers de vue views/overview.html et de modèle views/overview.js qui vont être impliqués.
- Pour http://localhost:8080/contact-us/, ce sont les fichiers de vue views/contact.html et de modèle views/contact.js qui vont être impliqués.
- Et pour http://localhost:8080/.+/ (n'importe quelle adresse finissant par /), ce sont les fichiers de vue views/error.html et de modèle views/error.js qui vont être impliqués.
Pour cela, nous allons créer ces fichiers dans notre structure existante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
isomorphism/
├─ javascripts/
│ ├─ isomorphic.js
│ ├─ isomorphic.mjs
│ ├─ operation.js
│ └─ operation.mjs
├─ node_molules/
│ ├─ ...
├─ views/
│ ├─ contact.htm
│ ├─ contact.js
│ ├─ error.htm
│ ├─ error.js
│ ├─ index.htm
│ ├─ index.js
│ ├─ overview.htm
│ └─ overview.js
├─ es5.htm
├─ es6.htm
├─ server.js
└─ package.json
└─ package-lock.json
et les remplir comme suit :
views/index.htm (code source)
views/index.js (code source)
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.
/*
Utilisation
de
l'export
CommonJS
de
Node.js.
*/
module.
exports =
function
(
template,
window
) {
/*
Création
d'un
espace
pour
manipuler
un
fragment
HTML...
*/
var body =
window
.
document
.
implementation.createHTMLDocument
(
).
body,
/*
Préparation
des
liens
pour
injection.
*/
links =
[
{
href
:
'
.
/
about
-
us
/
'
,
content
:
'
Go
to
about
page
'
}
,
{
href
:
'
.
/
contact
-
us
/
'
,
content
:
'
Go
to
contact
page
'
}
,
{
href
:
'
.
/
error
/
'
,
content
:
'
Try
an
error
page
'
}
]
;
/*
...lu
depuis
le
fichier
`views/index.htm`.
*/
body.
innerHTML =
template;
/*
Injection
du
titre.
*/
body.getElementsByTagName
(
'
h1
'
)[
0
]
.
textContent =
'
Welcome
'
;
/*
Injection
du
contenu.
*/
body.getElementsByTagName
(
'
p
'
)[
0
]
.
textContent =
'
This
is
the
welcome
page
!
'
;
/*
Injection
des
liens.
*/
Array.
prototype.
forEach.call
(
body.getElementsByTagName
(
'
a
'
),
function
(
a,
i) {
a.
textContent =
links[
i]
.
content;
a.setAttribute
(
'
href
'
,
links[
i]
.
href);
}
);
return {
template
:
body.
innerHTML
}
;
}
;
views/overview.htm (code source)
views/overview.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
Idem
que
pour
`views/index.js`.
*/
module.
exports =
function
(
template,
window
) {
var body =
window
.
document
.
implementation.createHTMLDocument
(
).
body,
a;
body.
innerHTML =
template;
body.getElementsByTagName
(
'
h1
'
)[
0
]
.
textContent =
'
About
this
website
'
;
body.getElementsByTagName
(
'
p
'
)[
0
]
.
textContent =
'
The
goal
of
this
website
is
to
provide
a
way
to
run
isomporphique
from
scratch
!
'
;
a =
body.getElementsByTagName
(
'
a
'
)[
0
]
;
a.
textContent =
'
Back
to
the
home
'
;
a.setAttribute
(
'
href
'
,
'
.
/
'
);
return {
template
:
body.
innerHTML
}
;
}
;
views/contact.htm (code source)
views/contact.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
Idem
que
pour
`views/index.js`.
*/
module.
exports =
function
(
template,
window
) {
var body =
window
.
document
.
implementation.createHTMLDocument
(
).
body,
a;
body.
innerHTML =
template;
a =
body.getElementsByTagName
(
'
a
'
)[
0
]
;
a.
textContent =
'
Back
to
the
home
'
;
a.setAttribute
(
'
href
'
,
'
.
/
'
);
body.getElementsByTagName
(
'
h1
'
)[
0
]
.
textContent =
'
Contact
US
'
;
body.getElementsByTagName
(
'
p
'
)[
0
]
.
innerHTML =
'
You
can
contact
us
by
using
the
following
email
:
<
a
href
=
"
mailto
:
bruno
.
lesieur
@
gmail
.
com
"
>
bruno
.
lesieur
@
gmail
.
com
<
/
a
>
'
;
return {
template
:
body.
innerHTML
}
;
}
;
views/error.htm (code source)
views/error.js (code source)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
Idem
que
pour
`views/index.js`.
*/
module.
exports =
function
(
template,
window
) {
var body =
window
.
document
.
implementation.createHTMLDocument
(
).
body,
a;
body.
innerHTML =
template;
body.getElementsByTagName
(
'
h1
'
)[
0
]
.
textContent =
'
Error
page
'
;
body.getElementsByTagName
(
'
p
'
)[
0
]
.
textContent =
'
This
is
the
error
page
.
.
.
'
;
a =
body.getElementsByTagName
(
'
a
'
)[
0
]
;
a.
textContent =
'
Back
to
the
home
'
;
a.setAttribute
(
'
href
'
,
'
.
/
'
);
return {
template
:
body.
innerHTML
}
;
}
;
Comprenez bien qu'à ce stade, toutes les nouvelles pages que vous ajouterez se rempliront avec une partie vue représentée par le HTML pour l'affichage de la page et une partie modèle pour les actions que vous ferez sur cette vue (ici, ajouter des textes). Ce code fonctionne aussi bien en étant appelé depuis le serveur qu'en étant appelé depuis le client. Il est donc parfaitement isomorphique.
IV-D. Le navigateur web▲
À partir d'ici, vous pouvez naviguer sur le site et le parcourir en utilisant les liens à l'adresse http://localhost:8080/. Si vous regardez dans la console de votre navigateur (F12 > Console), vous verrez juste que le fichier http://localhost:8080/javascripts/client.js n'est pas chargé.
GET http://localhost:8080/javascripts/client.js 404 (Not Found)
Vous constaterez également que changer de page se fait en rechargeant le navigateur pour chaque page.
C'est ici que va entrer en jeu la partie cliente dont le but va être d'exécuter les fichiers isomorphiques contenus dans le dossier views mais côté client. C'est grâce à cela que l'on sera capable de changer de page dynamiquement sans rechargement de page grâce aux appels XMLHttpRequest. Le fait de reprendre la main côté client sur la page courante s'appelle l'hydratation. Et en réalité, changer de page revient seulement à faire exécuter le couple .htm / .js directement dans le navigateur et simuler un changement de page avec pushState et l'évènement popstate.
Nous allons donc remplir le fichier client.js, qui lui, n'est compatible que du côté client :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
isomorphism/
├─ javascripts/
│ ├─ client.js
│ ├─ isomorphic.js
│ ├─ isomorphic.mjs
│ ├─ operation.js
│ └─ operation.mjs
├─ node_molules/
│ ├─ ...
├─ views/
│ ├─ contact.htm
│ ├─ contact.js
│ ├─ error.htm
│ ├─ error.js
│ ├─ index.htm
│ ├─ index.js
│ ├─ overview.htm
│ └─ overview.js
├─ es5.htm
├─ es6.htm
├─ server.js
└─ package.json
└─ package-lock.json
avec le contenu suivant :
javascripts/client.js (code source)
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.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
/*
Sortir
de
la
portée
globale
*
pour
notre
code
personnel
afin
*
d'éviter
des
conflits.
*/
;
(
function
(
) {
/*
Les
pages
navigables
sont
:
*
-
`http://localhost:8080/`
*
-
`http://localhost:8080/about-us/`
*
-
`http://localhost:8080/contact-us/`
*
...
*/
var router =
{
'
/
'
:
'
index
'
,
'
/
about
-
us
/
'
:
'
overview
'
,
'
/
contact
-
us
/
'
:
'
contact
'
}
,
/*
...et
celle
envoyant
un
contenu
en
erreur
sont
:
`/`
*
`http://localhost:8080/.+/`.
*/
file =
router[
location
.
pathname]
|
|
'
error
'
;
/*
Compatiblilité
CommonJS
simple.
*/
window
.
module =
{
}
;
/*
Gestion
de
la
navigation
dans
l'historique
*
notamment
en
cliquant
sur
le
bouton
« Retour »
*
du
navigateur.
*/
window
.addEventListener
(
'
popstate
'
,
function
(
) {
/*
Récupération
de
l'URL
après
retour
en
*
arrière
ou
en
avant
dans
l'historique.
*/
file =
router[
location
.
pathname]
|
|
'
error
'
;
/*
Puis
récupération
du
bon
couple
`.htm`
/
`.js`
*
en
provenance
de
`views`.
*/
changeRoute
(
file,
true);
}
);
/*
Gestion
du
changement
de
page
sans
rechargement
*
à
partir
du
fichier
de
destination.
`animate`
permet
*
de
savoir
si
l'hydratation
va
être
faite
avec
un
effet
*
d'animation
ou
non.
*/
function changeRoute
(
file,
animate) {
/*
Récupération
de
la
vue
et
du
modèle
isomorphique.
*
`file`
vaut
soit
`index`,
`overview`
ou
`contact`
*
en
fonction
de
ce
que
détecte
`router`
comme
*
page
courante.
*/
Promise.all
(
[
/*
On
fait
une
demande
XMLHttpRequest
*
avec
`fetch`
qui
retourne
une
promesse
*
puis
on
transforme
le
résultat
au
format
texte.
*/
fetch
(
'
views
/
'
+
file +
'
.
htm
'
).then
(
x =
>
x.text
(
)),
fetch
(
'
views
/
'
+
file +
'
.
js
'
).then
(
x =
>
x.text
(
))
]
).then
(
function
(
result) {
/*
Récupération
d'une
référence
sur
le
contenu
de
*
la
page
à
hydrater.
*/
var layout =
document
.getElementsByClassName
(
'
layout
'
),
/*
Récupération
des
fichiers
`.htm`
et
`.js`
*
en
retour
de
promesse.
*/
view =
result[
0
]
,
model =
result[
1
]
,
/*
Exécution
côté
client
des
fichiers
*
en
provenance
du
serveur.
*/
content =
eval
(
model)(
view,
window
);
/*
Ajout
du
nouveau
contenu
dans
une
`div`
*
différente
pour
faire
une
animation
si
*
demandé.
*/
if
(
animate) {
/*
Ajout
de
la
nouvelle
page
après
la
*
page
courante.
*/
layout =
layout[
layout.
length -
1
]
;
layout.insertAdjacentHTML
(
"
afterend
"
,
'
<
div
class
=
"
layout
"
>
'
+
content.
template +
"
</div>
"
);
/*
Ajout
de
la
classe
indiquant
*
l'animation
CSS3
a
été
exécutée.
*/
layout.
classList.add
(
'
change
'
);
/*
Retrait
de
la
page
d'où
l'on
vient
*
à
la
fin
de
l'animation.
*/
setTimeout
(
function
(
) {
layout.
parentNode.removeChild
(
layout);
}
,
1000
);
/*
Hydratation
simple
du
contenu
existant
*
si
pas
d'animation.
*/
}
else {
layout.
innerHTML =
content.
template;
}
/*
Faire
changer
la
page
sans
rechargement
*
à
tous
les
liens
présents
dans
la
page.
*/
Array.
prototype.
forEach.call
(
document
.getElementsByTagName
(
'
a
'
),
function
(
a) {
/*
Pour
chaque
lien,
lors
du
clic.
*/
a.addEventListener
(
'
click
'
,
function
(
e) {
/*
Pas
de
changement
de
page...
*/
e.preventDefault
(
);
/*
...mais
changement
d'URL
de
la
page.
*/
history
.pushState
(
null,
'
Isomporphic
example
'
,
document
.getElementsByTagName
(
'
base
'
)[
0
]
.getAttribute
(
'
href
'
) +
a.getAttribute
(
'
href
'
));
/*
La
page
à
charger
étant
choisie
par
le
routeur
*/
/*
en
se
basant
sur
la
nouvelle
URL.
*/
file =
router[
location
.
pathname]
|
|
'
error
'
;
/*
Changement
de
page.
*/
changeRoute
(
file,
true);
}
);
}
);
}
);
}
/*
Hydratation
de
la
page
appelant
`javascripts/client.js`
*
avec
le
bon
couple
`.htm`
/
`.js`
*/
changeRoute
(
file,
false);
}
(
));
À partir de maintenant, depuis n'importe quelle page affichée en tapant l'URL dans la barre d'adresse, c'est le serveur qui répondra par retour HTTP grâce au fichier server.js. En fouillant la source HTML de votre page, vous constaterez que la page est correctement générée et peut donc être indexée par les moteurs de recherche. Une fois sur une page, le fichier javascripts/client.js va s'exécuter, hydratant le DOM actuel et permettant aux liens de changer de page sans rechargement, mais en appelant seulement les fragments isomorphiques pour les exécuter sur place (par le navigateur). Le résultat est un changement de page dynamique que vous pouvez apprécier grâce à l'animation de transition CSS mise en place dans layout.htm.
Vous pouvez tester l'hydratation cliente grâce à la mockup que vous trouverez en live grâce au système GitHub Pages : exemple d'hydratation côté client.
V. Conclusion▲
Vous en savez plus maintenant sur les mécanismes d'utilisation des Modules ECMAScript ou CommonJS / Node.js pour créer des applications web isomorphiques !
Bien entendu, le code actuel est loin d'être pratique pour la maintenance et n'est ni optimisé pour l'hydratation cliente (actuellement on jette le DOM et on le recrée au lieu de réellement l'hydrater), ni optimisé pour la charge serveur (on pourrait utiliser du cache côté serveur pour ne faire générer les rendus d'une page qu'une fois toutes les X secondes, minutes ou même heures en fonction des zones statiques ou dynamiques des pages).
On se demandera comment gérer plus simplement l'injection de texte dans les templates plutôt que de manipuler le DOM, comment faire fonctionner du code avec des évènements JavaScript côté serveur ? Si c'est possible ? Comment mélanger différents types de modules ?
Toutes ces solutions sont adressées plus ou moins simplement avec l'utilisation de l'écosystème Vue (par ex. webpack pour la partie cliente, Nuxt pour la partie serveur et Vue.js pour la partie isomorphique).
Pour ma part, pour passer à l'étape supérieure, tout en comprenant ce que vous faites (pour de l'isomorphisme aux petits oignons), je vous propose de vous tourner vers le couple Vue / NodeAtlas (un framework basé sur Express et Socket.io). Vue et NodeAtlas vous permettront de réaliser des sites réactifs et isomorphiques facilement et progressivement. Essayez avec l'article Vue + NodeAtlas : de l'art du SSR ou Rendu Côté Serveur avec JavaScript qui vous expliquera les bases.
Et cerise sur le gâteau, les documentations de Vue, Nuxt et NodeAtlas sont toutes en français, traduites par votre serviteur !
Vous pouvez obtenir l'intégralité des sources de ce tutoriel sur ce dépôt GitHub : Haeresis/import-export-require-isomorphism.
VI. Remerciements▲
Nous remercions Bruno Lesieur qui nous a autorisés à publier ce tutoriel.
Nous remercions également Winjerome pour la mise au gabarit et Fabien pour la correction orthographique.