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 secondehandleClick
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 :
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
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
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 :