Présentation

Cet article est le troisième d’une série dédiée aux applications micro-frontend. Après avoir réalisé l’exercice avec VueJS et Angular, c’est au tour de ReactJS. Si vous n’avez pas encore lu les précédents articles, je vous les conseille vivement, vous découvrirez alors les premiers composants développés autour de l’API Star Wars :

Par défaut, React ne propose pas de “packager” une application sous forme de WebComponent. Nous devons donc faire appel à une librairie tierce Direflow. Cette librairie va nous mettre à disposition un CLI et un environnement de développement adapté à la création de WebComponent utilisant ReactJS.

Le code source présenté est disponible sur le repository Github d’Ineat.

Installation du CLI Direflow

Le CLI Direflow nous permet de créer une librairie de composants et de WebComponents facilement.

Installez le CLI au moyen de la commande suivante :

npm i -g direflow-cli

Création d’un nouveau projet

Le CLI Direflow permet d’initialiser un nouveau projet en une simple commande.

direflow create

À la demande du CLI, saisissez les informations suivantes :

  • Choose a name for your Direflow Setup : sw-species-component
  • Give your Direflow Setup a description (optional) : Ne rien ajouter
  • Which language do you want to use? JavaScript
  • Do you want this to be a NPM module? Yes

Une fois la structure du projet créée, accédez au dossier sw-species-component et exécutez la commande suivante pour lancer le serveur de développement via le CLI :

cd sw-species-component && npm install && npm start
Serveur de développement Direflow

Configuration de direflow

Nous allons appliquer quelques modifications à la configuration de Direflow. Modifiez le fichier direflow-config.json pour désactiver le chargement de React et React-Dom (en lazy-load) depuis un CDN.

// direflow-config.json
{
    "build": {
        "componentPath": "direflow-components",
        "filename": "sw-species.js"
    },
    "modules": {
        "react": false,
        "reactDOM": false
    }
}

Supprimez le chargement des fonts en retirant les lignes suivantes du fichier src/direflow-components/sw-species-component/index.js .

plugins: [
    {
      name: 'font-loader',
      options: {
        google: {
          families: ['Advent Pro', 'Noto Sans JP'],
        },
      },
    },
  ],

Création d’un service pour les appels API

Pour que notre application micro-frontend soit autonome et puisse communiquer avec une API, nous allons créer un service.

Créez un dossier nommé services dans le dossier src/direflow-components/sw-species-component puis dans ce nouveau dossier créez un fichier nommé api.service.js.

// api.service.js

export function getSpecies(page) {
  return fetch(`https://ilab-swapi-api.azurewebsites.net/api/species?page=${page}&limit=10`);
}

export function searchSpecies(search, page = 1) {
  return fetch(`https://ilab-swapi-api.azurewebsites.net/api/species/search?name=${search}&page=${page}&limit=10`);
}

Ce service va permettre de communiquer avec l’API Star Wars (déjà utilisée dans les précédents articles) et va nous renvoyer la liste des espèces de la célèbre série de films.

Ce service contient deux méthodes :

  • getSpecies: retourne la liste des espèces
  • searchSpecies: recherche une espèce

Création des différents composants de l’application

Comme pour les autres applications micro-frontend de la série, une application mono composant n’est pas la philosophie de React, ni de la séparation des couches.
Notre application va donc contenir un composant dit conteneur et 3 composants de présentation : 

  • Une recherche.
  • Une liste des espèces.
  • Une pagination.

Le composant de recherche

Créez un dossier nommé components dans le dossier src/direflow-components/sw-species-component puis dans ce nouveau dossier créez deux fichiers nommé SpeciesSearch.scss et SpeciesSearch.jsx.

// SpeciesSearch.scss
.sw-search {
  display: flex;
  flex-direction: row;
  width: 100%;
 
  button {
    min-width: 6rem;
    margin-left: 1rem;
    border: 1px solid rgba(darkred, 0.7);
    background: darkred;
    color: #fff;
 
    &.btn-clear {
      border: 1px solid rgba(grey, 0.7);
      background: lightgrey;
      color: grey;
    }
  }
 
  input {
    width: 100%;
    height: 35px;
    padding-left: 1rem;
    border: 1px solid #eee;
    background: #fafafa;
    font-size: 1.125rem;
  }
}
// SpeciesSearch.jsx
import React from 'react';
import { func, string } from 'prop-types';
import { Styled } from 'direflow-component';
import styles from './SpeciesSearch.scss';

const SpeciesSearch = ({ search, onSearchChange, onSubmit, onClear }) => {
  const handleSubmit = e => {
    e.preventDefault();
    onSubmit();
  }
  return (
    <Styled styles={styles} scoped>
      <form className="sw-search" onSubmit={handleSubmit} onReset={onClear}>
        <input
          type="text"
          name="searchField"
          value={search}
          onChange={onSearchChange}
        />
        {
          search !== '' && (
            <button type="reset" className="btn-clear">
              Clear
            </button>
          )
        }
        <button type="submit">
          Search
        </button>
      </form>
    </Styled>
  )
}

SpeciesSearch.propTypes = {
  search: string.isRequired,
  onSearchChange: func.isRequired,
  onSubmit: func.isRequired,
  onClear: func.isRequired
}

export default SpeciesSearch

Le composant affichant la liste des espèces

Ajoutez un nouveau composant SpeciesTile dans le dossier components en créant deux fichiers SpeciesTile.scss et SpeciesTile.jsx

// SpeciesTile.scss
li {
  list-style: none;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 2rem;
  border-bottom: 1px solid #efefef;

  span {
    font-size: 1.2rem;
    font-weight: 500;
  }

  .btn {
    background: firebrick;
    color: white;
    padding: 0.25rem 0.75rem;
    border-radius: 0.5rem;
    border: 1px solid firebrick;
  }
}
//SpeciesTile.jsx
import React from 'react';
import { shape, string } from 'prop-types';
import { Styled } from 'direflow-component';
import styles from './SpeciesTile.scss';

const SpeciesTile = ({ species }) => (
  <Styled styles={styles} scoped>
    <li>
      <span>{species.name}</span>
      <a className="btn btn-primary">Voir</a>
    </li>
  </Styled>
);

SpeciesTile.propTypes = {
  species: shape({
    name: string.isRequired
  }).isRequired
}

export default SpeciesTile

Le composant de pagination

Comme pour les composants précédents, créez deux nouveaux fichiers Pagination.scss et Pagination.jsx dans le dossier components.

// Pagination.scss
.pagination-wrapper {
  display: flex;
  flex-direction: row-reverse;
  padding: 1rem 0;

  ul {
    display: flex;
    margin: 0;
    padding: 0;
    flex-direction: row;
    align-items: center;

    li {
      list-style: none;
      padding: 0.25rem 0.5rem;
      margin: 0 0.125rem;
      border: 1px solid #fff;
      cursor: pointer;

      &:hover {
        color: #fff;
        background: rgba($color: #ff2200, $alpha: 0.5);
        border: 1px solid rgba($color: #ff2200, $alpha: 0.5);
      }

      &.current {
        border: 1px solid darkred;
        cursor: default;
        color: #fff;
        background: darkred;
      }
    }
  }
}
// Pagination.jsx
import React, { memo } from 'react';
import { number, func } from 'prop-types';
import { Styled } from 'direflow-component';
import styles from './Pagination.scss';

const Pagination = ({ currentPage, pageNumber, onChangePage }) => {
  const pages = new Array(pageNumber).fill(0).map((_, index) => ({ id: index + 1, text: index + 1 }));
  const getClassName = (id) => id === currentPage ? 'current' : null

  return (
    <Styled styles={styles} scoped>
      <div className="pagination-wrapper">
        <ul>
          {
            pages.map(({ id, text }) => (
              <li className={getClassName(id)} key={id} onClick={() => onChangePage(id)}>
                <span>{text}</span>
              </li>))
          }
        </ul>
      </div>
    </Styled>
  )
}

Pagination.propTypes = {
  currentPage: number.isRequired,
  pageNumber: number.isRequired,
  onChangePage: func.isRequired
};

export default memo(Pagination);

Le composant conteneur

Dans le dossier src/direflow-components/sw-species-component renommez le fichier App.css en App.scss et remplacez son contenu.

// App.scss
.sw-bloc {
  padding: 0rem 1rem;
}

.sw-list {
  flex: 1;

  ul {
    margin: 0;
    padding: 0;
  }
}

Puis renommez le fichier App.js en App.jsx et remplacez également son contenu.

// App.jsx
import React, { useCallback, useEffect, useState } from 'react';
import { Styled } from 'direflow-component';
import styles from './App.scss';
import SpeciesSearch from './components/SpeciesSearch';
import SpeciesTile from './components/SpeciesTile';
import Pagination from './components/Pagination';
import { getSpecies, searchSpecies } from './services/api.service';

const App = () => {
  const [species, setSpecies] = useState([]);
  const [paginationData, setPaginationData] = useState({ currentPage: 1, pageNumber: 1 });
  const [search, setSearch] = useState('');

  useEffect(() => {
    fetchCharacters()
  }, [])

  const fetchCharacters = async (page = 1) => {
    const response = await getSpecies(page);
    const data = await response.json();
    setSpecies(data.results);
    setPaginationData({ currentPage: page, pageNumber: data.total_pages })
  }

  const fetchSearch = async (search, page = 1) => {
    const response = await searchSpecies(search, page);
    const data = await response.json();
    setSpecies(data.results);
    setPaginationData({ currentPage: page, pageNumber: data.total_pages });
  }

  const onSearchChange = useCallback(event => {
    setSearch(event.currentTarget.value);
  }, [])

  const onSearchClear = useCallback(() => {
    setSearch('');
    fetchCharacters();
  }, []);

  const onChangePage = useCallback(async (event) => {
    setPaginationData(state => ({ ...state, currentPage: event }))
    if (!search) {
      fetchCharacters(event);
    } else {
      fetchSearch(search, event);
    }
  }, [search]);

  const onSearchSubmit = useCallback(async () => {
    if (search !== '') {
      fetchSearch(search);
    } else {
      fetchCharacters();
    }
  }, [search]);

  return (
    <Styled styles={styles} scoped>
      <div>
        <h2>Liste des espèces</h2>
        <div className="sw-bloc">
          <SpeciesSearch search={search} onSearchChange={onSearchChange} onSubmit={onSearchSubmit} onClear={onSearchClear} />
          <div className="sw-list">
            <ul className="list">
              {
                species.map(t => (
                  <SpeciesTile key={t.id} species={t} />
                ))
              }
            </ul>
          </div>
          {
            paginationData.pageNumber > 1 &&
            <Pagination onChangePage={onChangePage} {...paginationData} />
          }
        </div>
      </div>
    </Styled>
  )
};

export default App;

Relancez le serveur de développement avec la commande npm run start, vous devez obtenir le résultat suivant :

Pour packager notre composant, il ne nous reste plus qu’à exécuter la commande suivante dans le terminal :

npm run build
Résultat de la compilation en WebComponent

Attention le package ne contient pas de polyfills, ils seront chargés depuis un cdn.

Utiliser le composant micro-frontend

Le WebComponent est maintenant packagé en version minifiée « Ready for Production ». Il nous reste maintenant à l’utiliser dans une page HTML classique.

Dans votre projet, créez un dossier sample. Dans ce dossier, créez un fichier index.html et copiez le fichier du WebComponent (sw-species.js) présent dans le dossier build.

<!-- sample/index.html -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>SW Species Web Component</title>
  <base href="/">
</head>

<body>
  <sw-species-component></sw-species-component>
  <script type="text/javascript" src="sw-species.js"></script>
</body>

</html>

Pour voir le résultat, il suffit d’exposer avec un serveur HTTP le dossier sample. (ex: HTTP-Server => https://github.com/http-party/http-server#readme)

Performance

Notre application micro-frontend fonctionne correctement. Nous pouvons l’intégrer dans une page HTML ou dans n’importe quel framework frontend.

Côté poids, pour une application simple le WebComponent généré pèse 92,6 Ko en gzip, ce qui est assez conséquent.

Conclusion

La réalisation d’applications micro-frontend avec Direflow est relativement facile. Le CLI permet d’avoir l’environnement de développement clé en main.

Avec un WebComponent pesant 92,6 kB, Direflow permet de créer une application micro-frontend intégrable dans n’importe quelle page Web sans trop l’alourdir, avec des performances comparables aux autres librairies / frameworks (VueJS et Angular).

Dans le prochain article, nous allons passer au crible une autre librairie spécialisée dans les WebComponent: LitElement, nous pourrons alors clore cette série avec petite une synthèse !

Liste des articles liés :