Marmelab Blog

Le jeu Awalé en isomorphique avec Next.js

Dernière semaine d'intégration et me voilà parti sur le développement du jeu Awale en React et en isomorphique.

Next.js

React et isomorphique ?

React est une petite librairie Javascript créée par Facebook destinée au développement de composants réutilisables. On peut en voir un exemple sur le projet Awale en React Native.

React se démarque des autres SPA par son utilisation d'un DOM Virtuel. Un composant contient une méthode render() qui renvoit du JSX en fonction des propriétés du composant. Cela signifie que React se comporte comme la partie View du pattern MVC. L’une des particularités les plus intéressantes de React est la possibilité de pouvoir être rendue par le serveur : un serveur Node peut donc lire les composants de React et les renvoyer en format HTML au navigateur.

Les inconvénients d’une Single Page Application

Il y a principalement deux sujets qui sont primordiaux dans beaucoup de cas :

  • La performance
  • Le référencement

Comme on peut le voir ci-dessous, toute l'application est contenu dans le fichier bundle.js. Puisque que votre application est rendue par le client, Google ne peut pas proprement indexer vos pages.

Next.js

Aujourd’hui, lorsque vous arrivez sur votre site web, voici globalement ce que cela engendre :

  • Chargement du fichier HTML
  • Chargement des différents assets (css, image, scripts javascript externes)
  • Ainsi que de l’intégralité du code javascript de votre application
  • Exécution de tout ce petit monde, qui devra reconnaître où vous vous trouvez dans l’application afin de générer le HTML correspondant à l’état demandé.

Tout ce cheminement prend du temps, votre site web va afficher une page blanche pendant l'exécution des différentes étapes. Les utilisateurs accordent une grande importance aux performances aujourd'hui, la majorité du trafic Web est effectuée depuis un terminal mobile.

La solution, une application isomorphique

L’application isomorphique permet d’obtenir un rendu html à la fois côté client et côté serveur. Comme expliqué ci-dessus, React le fait très bien.

On va réécrire le petit scénario du chargement d'une page avec l'ajout du Server-Side Rendering.

  • Vous accédez à monsite.com/mapage.html
  • Votre serveur Node construit votre page et sert le rendu HTML généré par votre application au client
  • Il sert aussi votre application javascript dans un Bundle (généré par WebPack par exemple)
  • Le client reçoit un fichier statique et l'affiche (sans attendre le moindre javascript)
  • Il reçoit le Bundle
  • Une fois affiché, React sait reprendre la main sur votre application afin de continuer en mode SPA

Si vous voulez plus d'informations sur l'isomorphisme (son histoire et un exemple pratique détaillé), je vous conseille l'article sur React Isomorphique en pratique écrit par Julien.

Le choix de Next.js

Vous avez peut être remarqué en lisant l'article de Julien qu’il y a différents outils à mettre en place, ce qui peut complexifier votre architecture. Il existe une solution qui permet de faire ce travail de Server-Side Rendering à votre place, il s'agit de Next.js.

Ce mini-Framework vient de passer en version 2, il apporte des nouveautés intéressantes, j'y reviendrai plus bas. Vous pouvez simplement regarder un exemple de code avec Next.js, un simple Hello World pour constater la simplicité de mise en place d'une application isomorphique. Pas besoin de mettre en place Babel, Webpack et autres outils, tout est intégré dans Next.

On peut aussi retrouver un grand nombre d'exemples sur la mise en place de Next.js avec d'autre librairies javascript notamment (redux/jest/express/etc..)

Si on lance le projet avec make install && make run, on voit dans le code source de la page que le code HTML du menu est bien présent et pas simplement une balise avec l'inclusion d'un Bundle.js.

<body>
   <div id="__next">
      <div data-reactroot="" data-reactid="1" data-react-checksum="1864681735">
         <div data-jsx="3449686394" data-reactid="2">
            <!-- react-empty: 3 -->
            <nav class="menu" data-jsx="3449686394" data-reactid="4">
               <h1 class="menu__h1" data-jsx="3449686394" data-reactid="5">Awale</h1>
               <ul class="menu__ul" data-jsx="3449686394" data-reactid="6">
                  <li class="menu__li" data-jsx="3449686394" data-reactid="7">
                    <a class="menu__a" id="newGame" data-jsx="3449686394" href="/game" data-reactid="8">Solo</a>
                  </li>
                  <li class="menu__li" data-jsx="3449686394" data-reactid="9">
                    <a class="menu__a" data-jsx="3449686394" href="/game" data-reactid="10">With a friend</a>
                  </li>
               </ul>
            </nav>
            <!-- react-empty: 11 -->
         </div>
      </div>
   </div>
<body>

Architecture du projet

Next.js nous contraint à avoir un dossier pages. Celui-ci représente les différentes routes de notre application. Par exemple, pages/game.js aura pour effet de créer l'url monsite.com/game. On peut aussi personnaliser les routes (next-routes, parameterized-routing, custom-server-express, etc..)

Comme on peut le voir ci-dessous, j'ai créé deux pages (index et game).

Archi Next.js

On retrouve la logique du jeu Awale dans src/awale, ce dossier est exactement le même que pour mon projet React Native, marmelab/awale-android.

Redux

Pour le projet, on a décidé de mettre en place Redux, c'est pour cela que l'on peut voir les dossiers actions, middleware et reducers.

Redux

Redux fourni un état immutable ne pouvant être modifié que par l'utilisation d’actions. Ce mode de fonctionnement permet de suivre l’ensemble des changements, de pouvoir les annuler mais également de rendre le code beaucoup plus testable.

Un exemple de code, qui montre comment modifier le state en utilisants une action :

// Dans le composant PiButton, pickPebble représente l'action que l'on souhaite lancer
handlePickPebble = () => {
    this.props.pickPebble(this.props.pitIndex);
}

// La définition de l'action pickPebble
export const pickPebble = position => ({
    type: PICK_PEBBLE,
    payload: position,
});

// Dans le reducer, le traitement de notre action
export const reducer = (state = initState, action) => {
    switch (action.type) {
    case PICK_PEBBLE:
        return { ...state, game: pickPebbleGame(state.game, action.payload) };
    default:
        return state;
    }
};

Je vous conseille d'utiliser l'excellente extension chrome redux-devtools qui permet de voir le state. On voit l'historique des différentes actions ainsi que les modifications apportées par l'action.

Redux

Au fur et à mesure que l’application se complexifie, l’unique reducer peut être découpé en plusieurs petits reducers indépendants en utilisant combineReducers.

Les tests avec Sélénium

Comme dans tous les projets chez Marmelab, on teste nos applications unitairement mais pas seulement, on fait également des tests End-to-end (E2E) avec Sélénium.

Les tests E2E font partie des tests fonctionnels, on va tester l'ensemble des différentes briques de notre application ensemble. Le but des tests E2E est de lancer l’application dans un navigateur web pour obtenir les mêmes comportements qu’un utilisateur lambda. Ces tests sont utiles pour vérifier les interactions côté client, ainsi que le bon retour lors d’une action.

François a écrit un article sur le blog de Marmelab qui détaille de manière plus précise le fonctionnement des tests E2E avec Sélénium, E2E testing React Apps with Sélénium WebDriver and Node.js is easier than you think.

Il y a notamment une notion importante à retenir, celle de Page objects : on crée simplement un objet javascript qui représente nos éléments de la page et différentes actions possibles.

// Notre page
module.exports = url => driver => ({
    elements: {
        game: By.css('.game'),
        pit: indexPit => By.id(`pit_${indexPit}`),
    },

    selectPit(indexPit) {
        return driver.findElement(this.elements.pit(indexPit));
    },

    pickPebble(indexPit) {
        return this.selectPit(indexPit).click();
    },

    countPebble(indexPit) {
        return this.selectPit(indexPit);
    },
});

On retrouve notre test qui fait appel à l'objet javascript.

const GamePageFactory = require('../pages/gamePage');

describe('Game page', () => {
    const GamePage = GamePageFactory('http://localhost:8083/game')(driver);

    before(async () => await GamePage.navigate());

    it('should pick pebble on first pit', async () => {
        await GamePage.pickPebble(0);
        await driver.sleep(300);
        assert.equal(await GamePage.countPebble(0).getAttribute('value'), 0);
    });
});

Le design

J'ai choisi de conserver les mêmes couleurs et le même fonctionnement que pour l'application Android.

Board Awale

L'intégration du design a été rapide. J’ai utilisé une grande partie du code déjà développé, en ajoutant quelques spécificités à HTML que l'on n'a pas dans React native et inversement. La position des deux indications de score a été modifiée et j’ai également apporté une ombre sous le plateau de jeu.

Pour voir plus d'images du jeu, notamment le menu vous pouvez vous rendre sur le repository GitHub.

Conclusion

J'ai découvert beaucoup d’éléments intéressants lors de la mise en place de React isomorphique.

Le démarrage du projet se fait simplement et rapidement avec Next.js. Cependant Next.js cache de nombreuses notions au développeur. Derrière l'idée de Server-Side Rendering il y a plusieurs de concepts et fonctionnements qu'il faut comprendre. J'ai pu le constater en lisant l'article de Julien ou en essayant de répondre aux questions de mon référent technique. Pour vraiment apprendre l'isomorphisme, je pense qu'il serait préférable de ne pas partir sur une solution clés en main comme Next.js.

L'utilisation de Redux me semblait compliquée au premier abord, mais avec l'aide de mes collègues j'ai bien compris le fonctionnement et tous les avantages qu'il apporte. Il suffit d'aller voir sur un projet de plus grande envergure comme Admin on rest pour se rendre compte de la simplicité des reducers.

Je suis content d'avoir découvert les tests E2E. Lors de mes précédentes expériences professionnelles, j’ai pu observer une équipe de testeurs/recette qui effectuaient les tests de l'interface à la main, on peut totalement automatiser leur travail en utilisant Sélénium.

Le code source du jeu est disponible sur GitHub marmelab/awale-isomorphic.