On entend souvent parler d'applications JavaScript isomorphiques, et même si le nom ne fait pas l'unanimité, ce qu'il y a derrière, le concept de server side rendered JavaScript est en passe de devenir un sujet plutôt tendance pour 2015 grâce à la monté en puissance de Reactjs. Faire du rendu d'application JavaScript coté serveur permet de réconcilier enfin le développeur de Single Page App (SPA) avec l’amélioration progressive, l'accessibilité et le SEO ; quoique Google comme les lecteurs d'écran ont plutôt bien évolué sur ce point. L'autre avantage non négligeable à mon sens, est qu'on améliore les performances perçues par rapport à une SPA classique puisque :
Pour plus d'info sur les avantages, il y a cet article sur le blog de M6Tech.
Afin de me familiariser avec React et son écosystème, rien de mieux que mettre les mains dedans ! C'est un peu pour toutes ces raisons que j'ai décidé de l'utiliser pour mon site web (un site statique).
Inutile et donc totalement justifié pour le développeur que je suis. Une des premières briques que j'ai mises en place a été le routeur.
##react-router Pour une fois dans la communauté JavaScript, il y a une bibliothèque de référence et c'est celle là : react-router. Le routeur est fortement inspiré par celui d'Ember au sens où les URL et leurs routes sont au cœur du dispositif. Le routeur se présente sous forme de composant React et ça donne ça.
//routes.js
var React = require("react");
var Router = require("react-router");
var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute;
var NotFoundRoute = Router.NotFoundRoute;
var App = require("./app");
var Home = require("./home/homePage");
var Project = require("./project/projectPage");
var NotFound = require("./notFound");
var routes = (
<Route path="/" handler={App}>
<DefaultRoute name="index" handler={Home} />
<Route name="project" path="/projects/:url" handler={Project} />
<NotFoundRoute name="404" handler={NotFound} />
</Route>
);
module.exports = routes;
À mon sens, l'aspect déclaratif apporte de la clarté et améliore la
compréhension du système. On voit rapidement quel composant est utilisé en
fonction de l'URL. L'autre avantage du déclaratif, c'est qu'il permet de manière
assez simple, d'imbriquer les routes en imbriquant les nœuds <Route>
.
En plus des composants, on a aussi à disposition des mixins pour naviguer programmatiquement ou accéder aux infos du routeur (chemin, paramètres, ...)
Et le gros plus de cette bibliothèque : elle peut aussi s'utiliser coté serveur, pour la génération des vues et éviter ainsi de dupliquer du code puisqu'on va pouvoir carrément utiliser tel quel le fichier précédent. Plutôt cool ! Voici un exemple de middleware pour faire marcher ça dans Express :
var React = require("react");
var Router = require("react-router");
// notre fichier routes.js
var routes = require("./src/routes");
// notre template de page html
var Html = require("./src/html");
function reactView(req, res, next) {
Router.run(routes, req.url, function(Handler, state) {
// on récupère les données pour cette vue en fonction de la requête.
var data = getViewData(req);
// on génère la soupe au tag avec nos données dedans
var markup = React.renderToString(React.createElement(Handler), data);
// on utilise React comme moteur de template
var HtmlElement = React.createElement(Html, { markup: markup });
res.send("<!DOCTYPE html>" + React.renderToStaticMarkup(HtmlElement));
});
}
Et voici le composant Html.jsx
que l'on utilise comme template pour notre
page principale.
var React = require("react");
var Html = React.createClass({
render: function() {
return (
<html>
<head>
<meta charSet="utf-8" />
<title>Mon site perso</title>
<script src="/js/app.js" />
);
},
});
module.exports = Html;
Une fois que le code HTML a été généré
viaReact.renderToString(React.createElement(Handler), data);
, on pourrait
utiliser n'importe quel moteur de template (lodash.template, handlebars, ejs,
jade, PHP...) pour générer le HTML de la page à renvoyer. Dans un souci de
rationalisation des outils, j'ai préféré utiliser React.
Vous avez dû vous apercevoir qu'on utilise 2 méthodes différentes pour générer du HTML avec React :
React.renderToString(React.createElement(Handler), data);
React.renderToStaticMarkup(React.createElement(Handler), data);
La différence entre les deux méthodes est simple. Dans la première, React annote
les nœuds HTML avec des data-reactid
dans le but de pouvoir ensuite reprendre
la main lorsque votre l'application s'exécutera dans le navigateur. De cette
manière, React sait que vous l'initialisez avec un contenu généré depuis le
serveur. Et si il détecte une différence entre le code existant et celui qu'il
génère, vous aurez droit à un
petit warning.
La deuxième méthode permet de générer du code HTML sans annotations, comme
n'importe quel moteur de template.
Les plus attentifs auront remarqué qu'on passe les données initiales lors de la création de l'application via des props React. Quid de l'utilisation de flux dans tout ça ?
Avec Flux, ce sont les stores qui
maintiennent l'état de notre application. Si l'on ne veut pas afficher notre
application sans aucune données, nous allons devoir préalablement peupler nos
stores avant d'appeler React.renderToString()
.
Là où ça se corse un peu, c'est qu'il va falloir remplir nos stores avant de
démarrer notre application sous peine de voir le message d'alerte dont je
parlais plus haut. Le plus simple alors est de passer ces données au moteur de
template, en plus du markup (par exemple sous la forme d'un nœud
<script type="application/json">JSON DATA</script>
. Il ne reste plus qu'a
récupérer ces données avant d’appeler
React.render( Application, document.getElementById("react-app"))
Par exemple :
document.addEventListener("DOMContentLoaded", function(event) {
// getData() va récuperer et parser le contenu du tag script
// qui contient nos données
var storeData = getData();
// on déclenche une actions
actions.init(storeData);
// on lance le rendu
React.render(<App />, document.getElementById("react-app"));
});
Penser son application React pour qu'elle puisse être rendue coté serveur introduit de nouvelles problématiques, notamment avec l'ajout du pattern Flux. En fonction des pages que l'on souhaite afficher, on devra initialiser différents stores. À nous de déterminer, en fonction de l'URL et du composant à afficher, lequel initialiser, et cela, que l'on soit sur le client ou le serveur. De la même manière, il faudra être capable de charger nos données, indépendamment de l'environnement d’exécution (coucou XHR).
Un début de réponse se trouve dans les exemples fournis avec react-router.
L'idée est de passer par une propriété statics
lors de la création des
composants React qui seront associés à une <Route/>
.
var ProjectPage = React.createClass({
statics: {
fetchData: function(params){
return api.getPageData(params.url)
.then( actions.initProject)
.catch( actions.loadProjectError);
}
}
...
});
Dans ce bloc statics
, on définit une fonction qui servira à récupérer les
données pour ce composant mais on pourrait très bien imaginer retourner la liste
d'actions à lancer ou encore les stores à initialiser voire même un
descripteur des données nécessaires à la vue
(cf Relay / GraphQL)
Ensuite, lorsque le callback fourni à Router.run()
est appelé, il suffit de
parcourir les Handler pour récupérer les informations contenues dans les blocs
statics
, les traiter et enfin faire React.render()
.
// On renvoie une promesse qui sera résolue lorsque que toutes les données démandées via fetchData seront reçues.
Router.run(routes, Router.HistoryLocation, function (Handler, state) {
var p = Promise.all(state.routes
.filter(route => route.handler.fetchData) // définit fetchData
.map(route => {
return route.handler.fetchData(state.params);
})
);
p.then( x => {
React.render(<Handler {...state} />, document.getElementById("react-app"))
});
});
Le principe est simple et peut être facilement encapsulé dans un module pour être partagé entre le client et le serveur. J'espère que cet article vous a permis d'appréhender un peu mieux le rendu coté serveur d'une application React. N'hésitez pas à laisser des commentaires si vous avez des questions où si vous souhaitez partager vos expériences dans ce domaine.
Et quelques liens vidéo des sessions de la #reactjsconf :