Mise à jour

Le service d’appel vers une API a été changé pour l’utilisation d’une API sur un poste local.

Avant de commencer ce tutoriel, retrouvez toutes les informations pour exécuter l’api sur le dépôt GitHub de la série d’article : https://github.com/ineat/micro-frontend-series/tree/main/swapi

Présentation

Les micro-frontend sont des mini-applications autonomes, intégrables dans n’importe quel site web et ce, quelle que soit la technologie employée par ce dernier.

Ils sont souvent composés de composants web (WebComponent) et de services qui leur permettent de communiquer avec des API de manière complètement indépendante du site qui les intègre.

Il existe plusieurs frameworks/librairies qui permettent de créer des WebComponent (HTML/JS, Angular / React / VueJS, litElement, …). Nous verrons ici comment créer un micro-frontend avec la librairie VueJS, de la mise en place de l’environnement de développement jusqu’à l’intégration du micro-frontend dans une page HTML.

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

Installation du CLI VueJS

Le CLI de VueJS nous permet de créer des webComponent de manière rapide et simple. 

npm install -g @vue/cli
# OR
yarn global add @vue/cli

Création d’un nouveau projet

La création du projet est relativement facile avec le CLI.

vue create sw-character-component

On utilise les paramètres par défaut (VueJS 2, Babel, ESlint, …) auquel nous allons ajouter le sass-loader et node-sass afin d’utiliser du CSS dans nos fichiers .vue

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

cd sw-character-component
// puis
npm run serve
// ou
yarn serve

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éer un dossier nommé services dans le dossier src puis dans ce nouveau dossier créer un fichier nommé api.service.js.

// api.service.js
export default class ApiService {
  constructor() {}
 
  getCharacters(page) {
    return fetch(`http://localhost:3010/people?_page=${page}&_limit=10`);
  }
 
  searchCharacters(search, page) {
    return fetch(`http://localhost:3010/people?q=${search}&_page=${page}&_limit=10`);
  }
}

Ce service va permettre de communiquer avec l’api Star Wars et va nous renvoyer la liste des personnages de la célèbre série de films.

Ce service contient deux méthodes :

  • La première nous donnes accès à la liste des personnages.
  • La seconde nous permet d’effectuer une recherche dans la liste des personnages.

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

Notre application micro-frontend ne pourrait comporter qu’un seul composant mais ce n’est pas la philosophie de VueJS, ni de la séparation des couches comme on le voit de plus en plus souvent en développement front-end.
Notre application va donc contenir un composant dit container et 3 composants de présentation : 

  • Une recherche.
  • Une liste des personnages.
  • Une pagination.

Le composant de recherche

Dans le dossier components, créer un fichier nommé CharacterSearch.vue.

// ./components/CharacterSearch.vue
<template>
  <div class="sw-search">
    <input
      type="text"
      name="searchField"
      v-model="search"
    />
    <button class="btn-clear" v-if="search !== ''" @click="handleClear">
      clear
    </button>
    <button @click="handleSearch">
      Search
    </button>
  </div>
</template>
 
<script>
export default {
  name: 'CharacterSearch',
  data: () => ({
    search: ''
  }),
  methods: {
    handleSearch() {
      this.$emit('searchChange', this.search)
    },
    handleClear() {
      this.search = ''
      this.$emit('searchChange', this.search)
    }
  }
}
</script>
 
<style lang="scss" scoped>
.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;
  }
}
</style>

Nous n’allons pas ici détailler le composant mais celui-ci contient une variable search liée à l’input du template afin de récupérer la valeur entrée pas l’utilisateur.
Il contient également deux méthodes :

  • La première qui va émettre un évènement contenant la valeur du champ de recherche qui sera utilisée dans le composant conteneur pour effectuer une recherche grâce à un appel API.
  • La seconde qui va permettre de vider le champ de recherche et de remettre à jour la liste des personnages via l’émission d’un évènement.

Le composant affichant la liste des personnages

Dans le dossier components, créer un fichier nommé CharacterTile.vue

// ./components/CharacterTile.vue
<template>
  <li>
    <span>{{ character.name }}</span>
    <a class="btn btn-primary">Voir</a>
  </li>
</template>
 
<script>
export default {
  name: 'CharacterTile',
  props: {
    character: {
      type: Object
    },
  },
}
</script>
 
<style lang="scss" scoped>
  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
    }
  }
</style>

Ce composant ne sert qu’à afficher le nom de chacun des personnages et un bouton pour éventuellement accéder à la page du personnage. Il ne contient qu’une prop correspondant à l’objet JSON du personnage.

Le composant de pagination

Dans le dossier components, créer un fichier nommé Pagination.vue.

// ./components/Pagination.vue
<template>
  <div class="pagination-wrapper">
    <ul>
      <li
        v-for="page in pages"
        :key="page.id"
        :class="{current: currentPage === page.id}"
        @click="handleClick(page.id)"
      >
        <span>{{ page.text }}</span>
      </li>
    </ul>
  </div>
</template>
 
<script>
export default {
  name: 'Pagination',
  props: {
    currentPage: {
      type: Number,
      required: true
    },
    pageNumber: {
      type: Number,
      required: true
    }   
  },
  data: () => ({
    pages: [],
    current: 1
  }),
  mounted() {
    this.createPagination()
  },
  methods: {
    createPagination() {
      this.pages = [];
      for(let i = 1; i <= this.pageNumber; i++) {
        this.pages = [...this.pages, {id: i, text: i }]
      }
    },
    handleClick(pageIndex) {
      this.$emit('changePage', pageIndex)
    }
  },
  watch: {
    currentPage() {
      this.current = this.currentPage
    },
    pageNumber() {
      this.createPagination()
    }
  }
}
</script>
 
<style lang="scss" scoped>
.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 .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;
      }
    }
  }
}
</style>

Ce composant contient plusieurs éléments :

  • deux props, l’une pour définir la page courante de la pagination et la seconde pour définir le nombre total de pages que contient la pagination.
  • deux méthodes, la première createPagination pour créer la pagination à partir du nombre total de page et la seconde handleClick qui va émettre un évènement vers le conteneur pour savoir sur quel élément de la pagination l’utilisateur a cliqué.

Le composant conteneur

Dans le dossier components, créer un fichier nommé CharacterComponent.vue.

// ./components/CharacterComponent.vue
<template>
  <div>
    <h2>{{title}}</h2>
    <div class="sw-bloc">
      <CharacterSearch @searchChange="onSearch" />
      <div class="sw-list">
        <ul class="list">
          <CharacterTile
            v-for="item in characters"
            :key="item.uid"
            :character="item"
          />
        </ul>
      </div>
      <Pagination
        v-if="pageNumber > 1"
        :currentPage="currentPage"
        :pageNumber="pageNumber"
        @changePage="onChangePage"
      />
    </div>
  </div>
</template>
 
<script>
import CharacterTile from './CharacterTile.vue'
import CharacterSearch from './CharacterSearch.vue'
import Pagination from './Pagination.vue'
import ApiService from '../services/api.service.js'
 
const apiService = new ApiService()
 
export default {
  name: 'CharacterComponent',
  components: {
    CharacterTile,
    CharacterSearch,
    Pagination
  },
  props: {
    title: {
      type: String,
      default: 'Liste des personnages'
    },
  },
  data: () => ({
    characters: [],
    currentPage: 1,
    pageNumber: 1,
    isSearch: false,
    search: ''
  }),
  mounted() {
    this.fetchCharacters()
  },
  methods: {
    async fetchCharacters(page = 1) {
      const response = await apiService.getCharacters(page)
      const characterCount = response.headers.get('X-Total-Count')
      const data = await response.json()
      this.characters = [...data]
      this.pageNumber = Math.round(parseInt(characterCount) / 10)
    },
    async onChangePage(event) {
      this.currentPage = event
      if(!this.isSearch) {
        this.fetchCharacters(event)
      } else {
        this.onSearch(this.search, event)
      }
    },
    async onSearch(event) {
      if(event !== '') {
        this.isSearch = true
        this.search = event
        const response = await apiService.searchCharacters(this.search, page)
        const characterCount = response.headers.get('X-Total-Count')
        const data = await response.json()
        this.characters = [...data]
        this.pageNumber = Math.round(parseInt(characterCount) / 10)
      } else {
        this.isSearch = false
        this.search = ''
        this.fetchCharacters()
      }
    }
  }
}
</script>
 
<style lang="scss" scoped>
.sw-bloc {
  padding: 0rem 1rem;
}
 
.sw-list {
  flex: 1;
   
  ul {
    margin: 0;
    padding: 0;
  }
}
</style>

Le conteneur qui sera aussi le corps de notre application micro-frontend va lui contenir plusieurs éléments :

  • Une prop pour définir le titre affiché dans l’application, qui par défaut sera “Liste des personnages”.
  • 3 méthodes, une première qui va faire un appel API pour aller chercher la liste par défaut des personnages, la seconde qui va gérer le changement de page lors d’un clic sur la pagination et la troisième qui va gérer la recherche.
  • Les data avec la définition par défaut de la page courante, du nombre de pages, et de la liste des personnages.
  • La méthode mounted du cycle de vie, qui, au chargement de l’application va appeler la méthode pour charger la liste des personnages.

Le composant App

Pour visualiser notre composant, il faut l’inclure dans le composant root de l’application VueJS qui est représenté par le composant App.vue.

// App.vue
<template>
  <div id="app">
    <CharacterComponent />
  </div>
</template>

<script>
import CharacterComponent from './components/CharacterComponent.vue'

export default {
  name: 'App',
  components: {
    CharacterComponent
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Transformer le tout en WebComponent

Avant de compiler notre application, nous allons vérifier que celle-ci fonctionne. Dans votre navigateur vous devez obtenir ceci :

Application VueJS avant la transformation en Application Micro-Frontend

L’application fonctionne correctement. Nous allons maintenant packager cette application en WebComponent. Le CLI de VueJS nous permet de packager tout composant en WebComponent ou Librairie VueJS très simplement.
Dans le fichier package.json, ajouter la ligne suivante au niveau des scripts :

...
"scripts": {
    ...
    "build:characters": "vue-cli-service build --target wc --name sw-characters ./src/components/CharacterComponent.vue",
    ...
}
...

Puis dans un terminal exécuter cette nouvelle commande :

npm run build:characters
ou
yarn build:characters
Résultat de la compilation du composant VueJS en WebComponent
Résultat de la compilation du composant VueJS en WebComponent

Utiliser l’application micro-frontend

Le WebComponent est maintenant packagé en deux versions : une version JavaScript classique et une version minifiée. Il nous reste maintenant à l’utiliser dans une page HTML classique.

Dans votre projet, créer un dossier demo, puis un fichier index.html dans ce dossier et copier dans ce dossier la version minifiée du WebComponent.

<!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>My StarWars Micro-Frontent VueJS</title>
    <script src="https://unpkg.com/vue"></script>
    <script type="text/javascript" src="./sw-characters.min.js"></script>
    <base href="/">
  </head>
  <body>
 
    <sw-characters></sw-characters>
 
  </body>
</html>

Dans ce fichier on appelle le script de notre WebComponent à la ligne 9 et on inclut notre WebComponent dans le corps de notre page HTML à la ligne 13.
On inclut également le runtime de VueJS à la ligne 8, ce dernier est servi via un CDN.

Pour voir le résultat, il suffit de servir avec un serveur HTTP le dossier demo. (ex : http-server)

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é performance, notre application est relativement simple et comporte peu de code. Notre WebComponent ne pèse que 29.1kB mais du fait qu’on réalise notre composant avec VueJS il faut prendre en compte le téléchargement du runtime VueJS qui pèse 85.7kB

Analyse des performances de notre application micro-frontend
Analyse des performances de notre application micro-frontend

Une fois minifié et en utilisant la compression GZip, notre WebComponent fait une petite cure d’amaigrissement pour atteindre un poids de 10.7kB + 85.9kB de runtime VueJS.

Conclusion

La réalisation d’application micro-frontend avec VueJS est une chose relativement simple à mettre en place. Le CLI de Vue est très bien fait et nous permet de développer rapidement notre composant grâce au serveur de développement, mais nous permet de builder et packager rapidement notre application pour une utilisation simplifiée dans n’importe quel environnement.

Malgré un poids de composant relativement faible, une fois le composant minifié et compressé en GZip, le fait de devoir charger le runtime de VueJS est tout de même pénalisant pour les performances, mais est loin d’être rédhibitoire. À voir ce que donne les autres frameworks pour la création d’application micro-frontend…

La suite dans un autre article pour créer une application micro-frontend similaire avec Angular.

Liste des articles liés :