Les supports de cours sur la programmation réactive pour le Web sont disponibles ici.
L’objectif du TP est de construire une application temps réel de gestion de TODO.
Le cas d’utilisation est le suivant : dans un bureau de développement travaillant en Scrum, un grand écran est accroché au mur et affiche la liste des choses à faire par l’équipe (todos). Depuis son navigateur, n’importe qui peut modifier la liste des todos, la liste est alors instantanément mise à jour sur le grand écran partagé et sur tous les navigateurs ouverts sur la page.
Le TP se divise en 3 séances, la première est dédiée à la prise en main de React. La deuxième portera sur la partie distribuée / temps réel avec la mise à jour des todos via websocket. La dernière séance portera sur les applications Web hors-lignes.
Pour démarrer le TP rapidement il est aussi possible d'utiliser un serveur express très simple et un CDN pour charger les dépendances côté client, comme dans l'exemple HelloWorld.
Gardez à l'esprit qu'il s'agit d'une méthode de démarrage rapide, et qu'au moment du rendu votre application doit être structurée proprement pour servir tous les fichiers.
Nous allons utiliser npm (pas besoin de bower) pour gérer les dépendances côté serveur. Vous pouvez utiliser ce fichier package.json pour démarrer.
Il suffit de créer votre projet puis de faire un npm install --dev
.
Il est aussi possible d'utiliser React-starter-kit (qui s'installe via webstorm ou npm) contient du "boiler plate" pour démarrer un projet react proprement, mais attention c'est assez riche et on peut s'y perdre.
À vous de créer votre propre fichier Grunt.
Pour la fin du set-up, voir plus bas.
Le plus simple pour revoir les bases de la programmation réactive est de lire le manifeste. L'article d'Erik Meijer Your mouse is a database, est aussi important.
Pour bien démarrer avec React, la documentation est bien faite, n'hésitez pas à vous y référer. Prenez le temps de lire les explications, elles permettent de comprendre la logique derrière React! Attention React est une techno qui a beaucoup changé en 3 ans. Faites attention si vous suivez des tutoriaux ou copiez coller du code à leur date. Préférez les conseils datant d’après l’été 2015.
Penser à installer l’addon React-dev-tool dans votre navigateur
Servir le fichier index.html suivant via express:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>
Remarquez que le script React est de type text/babel
. Le script contient du JSX, un mélange de Javascript et de XML qui sera traduit en JavaScript pur par Babel côté client.
Les anciennes versions de React utilisaient un traducteur dédié à jsx, et beaucoup d'exemples en ligne utilisent encore cette syntaxe. Faites attention à bien utiliser Babel.
On utilisera express côté serveur.
Le code suivant permet de servir la page HTML ci-dessus :
var express = require('express');
var app = module.exports = express();
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.listen(3000, function () {
console.log('App listening on port 3000!');
});
Rajouter Bootstrap, Foundation ou une autre bibliothèque comme dépendance.
Avec React on pense d’abord à partir de la vue. La vue va être décomposée en composants. Pour démarrer (libre à vous d’adapter) voici une structure de base:
- TODO APP
- TODO TITRE
- TODO LIST
- TODO LIST ITEM #1
- TODO LIST ITEM #2
...
- TODO LIST ITEM #n
- TODO INPUT
Pour commencer, créer une premier composant React : Le titre de l'application.
<div id="todo"></div>
<script type="text/babel">
var TodoBanner = React.createClass({
render: function(){
return (
<h3>TODO List</h3>
);
}
});
ReactDOM.render(<TodoBanner/>, document.getElementById('todo'));
</script>
Pour créer la structure de l'application, nous allons créer un composant React racine appelé TodoApp
qui rendra de la structure suivante:
<div>
<TodoBanner/>
<TodoList/>
<TodoInput/>
</div>
Nous allons utiliser la fonction render de React pour faire un rendu du DOM virtuel (de React) dans un élément du DOM "réel" de la page:
ReactDOM.render(<TodoApp/>, document.getElementById('todo'));
Les composants TodoList
, TodoListItem
, et TodoInput
n'étant pas encore définis, React ne va pas afficher la structurp
Créer un composant TodoList
contenant une liste ul
, un composant TodoListItem
contenant un élement de liste li
,et un composant TodoInput
contenant un champ texte.
Vérifier que la structure est bien créée dans la partie React de vos outils de développement (un onglet s'est normalement rajouté quand vous avez installé l'extension react-dev tools).
Par statique nous entendons une application qui va seulement afficher des données pré-définies. On va commencer avec une liste de todos simple:
var todo_items = ['1 créer une structure d'application, '2 gérer l'affichage de données statiques', '3 gérer des données dynamiques'];
Il faut passer ces données à React. Il existe deux stratégies possibles. Soit de le passer directement aux composants en bas de la hiérarchie (ex : TodoListItem), soit de passer par des composants plus haut dans la hiérarchie. Dans le cas d'applications simples comme la notre on va préférer passer par "le haut", dans des applications plus complexes on passera les données plutôt par le bas.
On peut penser les composants React comme des fonctions qui prennent en entrée props
et state
et rendent du HTML.
Nous verrons state
plus tard, avec les aspects dynamiques. props
décrit les proprités d'un composant React, et permet de passer ces propriétés à ses enfants.
On passe une propriété de la façon suivante en JSX:
<TodoList listetodos={this.props.todos}/>
<ReactDOM.render(<TodoApp todos={todo_items}/>, document.getElementById('todo'));
Il est maintenant possible d'accéder à this.props.listetodos
depuis le composant TodoList
.
Il ne reste plus qu'à parcourir cette liste de todos et créer un TodoListItem
pour chaque élément de la liste dans la fonction render.
Le render du composant TodoListItem
se charge de créer un item li
, TodoList
les aggrègera dans un ul
La documentation sur les aspects dynamiques
On va maintenant rajouter des données quand un nouveau todo est entré. L'avantage de React est qu'il s'occupe de la "plomberie", c'est à dire de la mise à jour des composants dès qu'il y a un changement des données.
Pour cela React va s'appuyer sur l'état, state
, des composants. L'état va être amené à changer (il est mutable), alors que les props
elles sont plutôt immutables.
Pour que React reste efficace, il faut définir un minimum de composants avec des états. Faites la liste de vos composants et des données associées en vous posant les questions suivantes.
Après avoir inspecté vos composants il ne devrait en ressortir que deux qui ont un état : TodoApp qui gérera les todos, et TodoForm qui permettra d'en créer de nouveau.
La première chose à faire est de transformer la liste de todos initiale de propriété en un état initial de TodoApp, puisque la liste des todos va être amenée à changer:
var TodoApp = React.createClass({
getInitialState: function() {
return {items: ["1 créer une structure d'application", "2 gérer l'affichage de données statiques", "3 gérer des données dynamiques"]};
},
render: function() {
return (<div>
<TodoBanner/>
<TodoList listetodos={this.state.items}/>
<TodoInput/>
</div>
);
}
Maintenant tout le travail va consister à mettre à jour l'état items
, lorsque l'on rajoute un todo dans TodoInput
devrait être un form
. Voir la documentation de React sur les formulaires. Il existe trois évènements React liés aux formulaires:
onChange onInput onSubmit
.
Pour cela on va créer une fonction updateItems qui sera appelée lors de la soumission du formulaire:
Dans TodoApp on met à jour l'état lors de soumission du formulaire:
updateItems: function(newItem) {
var allItems = this.state.items.concat([newItem]);
this.setState({
items: allItems
});
},
On passe une référence à la fonction en définissant TodoInput:
<TodoInput newItems={this.updateItems}/>
On met à jour l'état lors de la soumission du formulaire:
handleSubmit: function(e){
e.preventDefault();
this.props.newItems(this.state.item);
return;
},
Pour cela il faut connecter handleSubmit à la soumission:
<form onSubmit={this.handleSubmit}>
<input type='text'/>
<input type='submit' value='Add'/>
</form>
La dernière chose à faire est de gérer un état interne à TodoInput qui suit les changement du champ texte.
On va modifier le champ texte pour rajouter une référence, lié la valeur du champ à l'état, et connecter à une fonction de gestion des changements.
<input type='text' ref='item' onChange={this.onChange} value={this.state.item}/>
onChange: function(e){
this.setState({
item: e.target.value
});
},
Enfin il faut penser à définir un état initial:
getInitialState: function() {
return {item: ''};
},
Et remettre l'état du champs texte à "" une fois le todo soumis (dans handleSubmit):
this.setState({item: ''});
Nous allons maintenant nous appuyer sur Backbone pour gérer les données. Il existe plusieurs stratégie pour ce faire: soit faire le lien à la main, soit utiliser une bibliothèque type backbone-react-component.
Pour démarrer cette partie simplement nous allons créer une collection Backbone, à terme il faudrait un modèle par Todo et une collection pour les gérer.
Rajouter underscore, backbone et backbone-react-component aux dépendances du projet, puis créer une collection:
var todo_items = new Backbone.Collection([{id: 0, txt:"créer une structure d'application"}, {id: 1, txt:"gérer l'affichage de données statiques"}, {id: 3, txt:"gérer des données dynamiques"}]);
Nous allons passer cette collection à la création de TodoApp:
ReactDOM.render(<TodoApp collection={todo_items}/>, document.getElementById('todo'));
Puis modifier TodoApp pour utiliser la collection, pour cela nous allons utiliser le mixin de backbone-react-component:
var TodoApp = React.createClass({
mixins: [Backbone.React.Component.mixin],
Il faut aussi modifier updateItems
pour mettre à jour la collection
updateItems: function(newItem) {
//calcul d'identifiant provisoire qui ne fonctionnera plus si l'on peut supprimer des todos.
var newItemID = todo_items.length + 1;
todo_items.add({id:newItemID, txt:newItem});
},
Et passer la collection à TodoList:
<TodoList items={this.state.collection}/>
La structure de données a changé avec l'introduction de la Collection Backbone, il faudra donc aussi modifier la façon dont TodoList et TodoListItem gèrent les données:
Nous allons maintenant pouvoir bénéficier de l'avantage principale de React, créer des vues différentes à partir des mêmes données
Rajouter un composant à l'intérieur de TodoBanner qui affiche la quantité de TODOs et se met à jour automatiquement en cas de changement de la collection Backbone en vous appuyant sur le dataflow React.
Bravo vous devriez maintenant avoir une application qui tourne!
Il est temps de terminer le set-up du projet.
Vous avez découvert les fichiers jsx utilisés par React, il est possible d'utiliser ces fichier côté client comme nous venons de le faire. Mais cet usage demande de s'appuyer sur des CDN, et ne permet pas de créer des applications modulaires et chargeant intelligement les dépendances.
On va maintenant se débarrasser des CDN et enlever les balises script
de notre index.html
pour préférer utiliser require dans nos fichiers js.
var React = require('react');
var ReactDOM = require('react-dom');
var Backbone = require('backbone');
var backboneMixin = require('backbone-react-component');
Nous devons maintenant compiler les modules js et les fichiers jsx en un seul fichier "bundle.js" qui contiendra le projet.
Attention il faudra modifier l'appel au mixin React-Backone comme suit mixins: [Backbone.React.Component.mixin]
-> mixins: [backboneMixin],
Pour cela il est possible d'utiliser Browswerify ou Webpack qui est plus généralement associé à React.
La ligne de commande suivante permet de compiler le projet (à lancer avant le grunt build):
webpack app/app.js dist/bundle.js --module-bind 'js=babel-loader'
Il peut être nécessaire de dire à babel que votre code est du React. Pour cela rajoutez un fichier .babelrc
à la racine de projet:
{
"presets": [
"react"
]
}
Si votre code devient plus modulaire (plusieurs fichiers js et jsx) il est conseillé d'utiliser un fichier webpack.config.js
TodoListItem
pour ajouter une checkbox devant chaque todo.TodoListItem
pour voir à qui est assigné chaque todo.Nous allons maintenant réaliser l'échange de todos entre les différents clients connectés à l'application via des websockets.
Nous allons utiliser le package ws
pour gérer les websockets côté serveur. La première chose à faire est de mettre en place un serveur avec ws, en plus du serveur HTTP existant, ainsi que l’initialisation de la connexion par le client. Pour cela, inspirez-vous de la doc de ws (https://github.com/websockets/ws).
wscat
est particulièrement pratique pour tester votre serveur websocket en mode cli, il permet de ne pas passer par le navigateur. Vous pouvez l'installer avec npm globalement.
Pour aller plus loin, wsta est intéressant.
En suivant l'example de la page de présentation de ws, intégrer à votre serveur la possibilité de recevoir et d'envoyer des messages sur un port donné, et testez en envoyant des messsages via wscat
.
Maintenant rajouter l'envoi d'un élement todo au serveur lors de la validation du formulaire d'input. Côté client on privilégiera l'utilisation de l'API WebSocket du navigateur (pas besoin d'utiliser une bibliothèque). Ce tutoriel en français sur les websockets fournit un exemple simple d'envoi et de réception de message côté client.
Quand le serveur reçoit un todo, il va le partager avec tous les clients connectés, pour que la liste soit synchronisée. Comme les Websockets s'appuyent sur TCP, le broadcast n'est normalement pas possible. La solution consiste à gérer une liste de tous les clients connectés puis d'envoyer un message à chacun.
Attention, il faudra donc faire attention à ce qu'un todo déjà présent dans la collection du client émetteur ne soit pas dupliqué lors de la réception du broadcast serveur. Il faudra aussi pouvoir gérer le cas ou un utilisateur modifie l'état d'un todo (par exemple le marque comme fait).
Voir la page TP Cordova. Les modalités de rendu et d'évaluation sont aussi sur cette page.