TP programmation réactive, Web sockets, et applications offlines

Cours #

Les supports de cours sur la programmation réactive pour le Web sont disponibles ici.

Présentation du TP #

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.

Set-up du projet #

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.

React #

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

Hello World #

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!');
});

Présentation avancée #

Rajouter Bootstrap, Foundation ou une autre bibliothèque comme dépendance.

Structurer l’application #

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

Un premier composant React #

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>

Créer les autres composants #

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).

Créer une application statique #

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

Rendre l'application dynamique #

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: ''});

Lier React à Backbone #

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:

Bénéficier de React: Création d’un compteur de todos #

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.

Terminer le set-up #

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

Enrichir l'application #

Bonus: intégrer le routage Backbone #

Web sockets #

Nous allons maintenant réaliser l'échange de todos entre les différents clients connectés à l'application via des websockets.

Serveur websocket #

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.

Échange entre client et serveur #

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.

Échanges entre tous les clients (broadcast) #

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).

Application Mobile #

Voir la page TP Cordova. Les modalités de rendu et d'évaluation sont aussi sur cette page.

Liens utiles