Go to Hackademy website

Une bibliothèque JavaScript modulaire grâce aux plugins

Tom Panier

Posté par dans la catégorie front

Salutations ! Aujourd’hui, je vous propose un article un poil plus « avancé » puisqu’il s’adresse davantage aux auteur·rice·s de bibliothèques ; ce pourrait être le premier d’une série plus ou moins longue, alors n’hésitez pas à nous donner votre avis en commentaire !

Nous allons étudier ensemble un pattern qui peut s’avérer fort utile dans ce contexte, à savoir l’écriture d’une bibliothèque supportant un système de plugins. Trêve de palabres, plongeons en triple salto dans le vif du sujet !

Un exemple pas piqué des vers

Par souci de simplicité, nous allons prendre un exemple… simple, à savoir une bibliothèque permettant d’appliquer une série de traitements sur une chaîne de caractères. Toujours pour la même raison, notre code consistera en un simple fichier JavaScript que nous exécuterons avec Node.js via la commande suivante :

$ node path/to/file.js

Chacun des plugins sera représenté par une simple fonction prenant comme unique argument la valeur d’origine et retournant le résultat de son traitement ; commençons par en écrire quelques-uns :

const capitalizePlugin = input => input.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
const moreLovePlugin = input => input.replace("aime", "adore");

Passons maintenant au cœur du système, à savoir la fonction chargée d’appliquer à une chaîne les différents traitements d’un ensemble donné de plugins :

function processWithPlugins(input, plugins) {
  let output = input;

  plugins.forEach(plugin => {
    output = plugin(output);
  });

  return output;
}

Attends, on itère sur les valeurs d’un tableau pour construire un résultat unique… ça me dit quelque chose !

En effet ! Si vous avez lu mon dernier article, vous devriez avoir deviné que c’est un cas d’utilisation parfait pour Array.prototype.reduce :

function processWithPlugins(input, plugins) {
  return plugins.reduce((output, plugin) => plugin(output), input);
}

Voilà qui est plus concis ! Testons donc ce que nous avons écrit jusqu’ici :

console.log(processWithPlugins("J'aime les frites !", [capitalizePlugin, moreLovePlugin]));
J'adore Les Frites !

Les deux traitements ont bien été appliqués successivement, dans l’ordre de leur position dans le tableau.

Des plugins un peu plus évolués

Ce que nous avons pour l’instant fonctionne bien, mais il serait pertinent de pouvoir faire en sorte que chaque traitement ne soit appliqué que sous certaines conditions, afin d’éviter par exemple d’appliquer inutilement capitalize sur une chaîne où tous les mots commencent déjà par autre chose qu’une lettre minuscule.

Nous allons donc réécrire nos plugins en en faisant cette fois-ci des objets comportant deux méthodes : une méthode process effectuant le traitement comme précédemment, et une méthode shouldProcess retournant un booléen indiquant, en fonction de la valeur d’entrée, si ce traitement doit être appliqué ou non :

const capitalizePlugin = {
  process(input) {
    return input.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
  },

  shouldProcess(input) {
    return !!input.match(/\b[a-z]/); // y a-t-il au moins un mot commençant par une minuscule ?
  }
};

const moreLovePlugin = {
  process(input) {
    return input.replace("aime", "adore");
  },

  shouldProcess(input) {
    return true; // on a toujours besoin de plus d'amour
  }
};

Modifions également notre fonction processWithPlugins afin de tenir compte du résultat de shouldProcess :

function processWithPlugins(input, plugins) {
  return plugins.reduce((output, plugin) => plugin.shouldProcess(output)
    ? plugin.process(output)
    : output
  , input);
}

Il est dès lors facile de valider le fonctionnement de ce nouveau pattern, en ajoutant par exemple un log dans le shouldProcess de capitalizePlugin :

const capitalizePlugin = {
  process(input) {
    return input.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
  },

  shouldProcess(input) {
    const result = !!input.match(/\b[a-z]/);
    console.log(input, result);

    return result;
  }
};
console.log(processWithPlugins("J'aime les frites !", [capitalizePlugin, moreLovePlugin]));
console.log(processWithPlugins("I'M ALL CAPS", [capitalizePlugin, moreLovePlugin]));
J'aime les frites ! true
J'adore Les Frites !
I'M ALL CAPS false
I'M ALL CAPS

Notez qu’il est quelque peu dommage de devoir déclarer une fonction shouldProcess pour moreLovePlugin alors que nous savons qu’il s’exécutera systématiquement ; peut-être pouvons-nous nous en passer ?

const moreLovePlugin = {
  process(input) {
    return input.replace("aime", "adore");
  }
};
function processWithPlugins(input, plugins) {
  return plugins.reduce((output, plugin) => !("shouldProcess" in plugin) || plugin.shouldProcess(output)
    ? plugin.process(output)
    : output
  , input);
}

Bingo ! Cette version considérera que l’absence de shouldProcess équivaut à une implémentation retournant toujours true. En s’amusant avec typeof, on pourrait aussi imaginer supporter également les plugins exclusivement constitués d’une fonction, telles les premières moutures des nôtres.

Les promesses, c’est sacré !

Pour cette dernière partie, nous allons voir comment modifier notre code afin de supporter les plugins réalisant un traitement asynchrone, ou en d’autres termes, retournant une Promise qui sera résolue à la valeur traitée.

Installons donc une nouvelle dépendance :

$ npm i node-fetch

Et ajoutons dans la foulée un tel plugin :

const weekendPlugin = {
  process(input) {
    return fetch("https://estcequecestbientotleweekend.fr/")
      .then(r => r.text())
      .then(body => body.replace(/\n/g, "").match(/\<p class="msg"\>([^<]+)\<\/p\>/)[1].trim())
      .then(msg => input + " Est-ce que c'est bientôt le week-end ? " + msg);
  }
};

Il nous faut ici prêter attention à un détail important : afin de pouvoir traiter toute notre chaîne de plugins en faisant abstraction de leur synchronicité, nous devons modifier processWithPlugins afin de lui faire gérer la résolution des Promises, ainsi que le wrapping du résultat de l’exécution de chacun d’eux dans une Promise. Nous manipulerons ainsi des Promises (vous non plus, vous n’avez jamais lu ce mot autant de fois d’affilée ?) exclusivement :

function processWithPlugins(input, plugins) {
  return plugins.reduce((output, plugin) => Promise.resolve(output).then(output => new Promise(resolve => resolve(
    !("shouldProcess" in plugin) || plugin.shouldProcess(output) ? plugin.process(output) : output
  ))), input);
}

Cette fonction étant, dans les faits, devenue asynchrone, nous devons également impacter le code appelant (et en profiter pour ajouter weekendPlugin à la liste) :

processWithPlugins("J'aime les frites !", [capitalizePlugin, moreLovePlugin, weekendPlugin]).then(console.log);
J'adore Les Frites ! Est-ce que c'est bientôt le week-end ? Non.

Notez que, fort logiquement, le résultat peut varier si vous avez la chance d’exécuter ce code alors que le week-end approche ;)

And voilà

C’en est fini de cet article ! J’espère qu’il vous a plu et que vous appréciez ce type de considérations algorithmiques ; le cas échéant, comme je le disais en début d’article, n’hésitez pas à le manifester dans les commentaires pour que je vous en propose d’autres du même acabit !


L’équipe Synbioz.
Libres d’être ensemble.

Articles connexes

Des composants universels avec React et Vue.js

24/05/2018

Dans notre article traitant de rendering avancé avec Vue.js, nous avions exploré la possibilité, pour un composant écrit pour ce framework, de tirer parti de JSX, le langage de templating initialement prévu pour être utilisé avec React. Si vous êtes…

Un code JS impeccable grâce à ESLint

06/09/2018

Après avoir vu ensemble comment écrire des tests unitaires pour votre application JavaScript, on pourrait s’imaginer que notre codebase a atteint le pinacle de la qualité. C’est sans compter sur les obsessions maniaques de votre serviteur (c’est moi…

Vue.js London 2018, en résumé.

04/10/2018

La gare de Lille est déjà pleine de voyageurs impatients quand le train à destination de Londres m’est annoncé. C’est donc avec un peu d’avance que je m’installe en espérant que Tom et Gaëtan, mes acolytes pour cette expédition, me rejoindront vite.…

Chérie, j'ai reduce les tableaux

11/10/2018

Depuis un peu plus d’un an que j’écris sur ce blog, je vous ai surtout parlé de Vue.js, ou encore d’outils comme ESLint ; mais tous les bons artisans vous le diront, avoir le meilleur marteau du monde n’interdit pas de s’écraser les doigts ! Aujour…

Afficher plus Afficher moins

Ajouter un commentaire