Les polyfills et les wrappers

Voici un petit chapitre dans lequel nous allons aborder deux concepts de programmation relativement utilisés en JavaScript : les polyfills et les wrappers. Nous allons étudier leurs particularités, pourquoi nous en avons besoin, et surtout comment les mettre en place.

Ce chapitre est assez théorique mais il vous guidera sur la manière dont vous pouvez structurer vos codes dans certains cas. Connaître les deux structures que nous allons étudier ci-après vous permettra notamment de comprendre facilement certains codes rédigés de cette manière.

Introduction aux polyfills

La problématique

Vous n'êtes pas sans savoir que certaines technologies récentes sont plus ou moins bien supportées par certains navigateurs, voire même pas du tout. Cela nous pose à tous de nombreux problèmes dans le développement de nos projets destinés au Web. Dans ce cours, nous avons déjà étudié des méthodes et des propriétés qui ne sont pas supportées par de vieux navigateurs, commeisArray()par exemple.

Pour réaliser nos projets, il nous faut alors ruser avec des conditions permettant de tester si le navigateur actuel supporte telle ou telle technologie ; dans le cas contraire il nous faut alors déployer des solutions dont certaines sont peu pratiques. Dans certains cas, nous sommes même obligés de nous passer de ces technologies récentes et de nous rabattre sur de vieilles solutions… Bref, un vrai casse-tête !

La solution

Il existe un moyen de se faciliter plus ou moins la tâche, cela s'appelle les polyfills ! Concrètement, un polyfill est un script qui a pour but de fournir une technologie à tous les navigateurs existants. Une fois implémenté dans votre code, un polyfill a deux manières de réagir :

  • Le navigateur est récent et supporte la technologie souhaitée, le polyfill ne va alors strictement rien faire et va vous laisser utiliser cette technologie comme elle devrait l'être nativement.

  • Le navigateur est trop vieux et ne supporte pas la technologie souhaitée, le polyfill va alors « imiter » cette technologie grâce à diverses astuces et vous permettra de l'utiliser comme si elle était disponible nativement.

Rien ne vaut un bon exemple pour comprendre le principe ! Essayez donc le script suivant avec votre navigateur habituel (qui se doit d'être récent) puis sur un vieux navigateur ne supportant pas la méthodeisArray(), Internet Explorer 8 fera très bien l'affaire :

if (!Array.isArray) { // Si isArray() n'existe pas, alors on crée notre méthode alternative :
Array.isArray = function(element) {
return Object.prototype.toString.call(element) == '[object Array]';
};
}
alert(Array.isArray([])); // Affiche : « true »
alert(Array.isArray({})); // Affiche : « false »

Essayer le code

La méthodeisArray()fonctionne maintenant sur tous les navigateurs ! Pas besoin de s'embêter à vérifier à chaque fois si elle existe, il suffit juste de s'en servir comme à notre habitude et notre polyfill s'occupe de tout !

Quelques polyfills importants

Le principe des polyfills ayant été abordé, sachez maintenant que la plupart d'entre vous n'auront pratiquement jamais à réaliser vos propres polyfills, car ils sont déjà nombreux à avoir été créés par d'autres développeurs JavaScript. Le MDN est un bon concentré de polyfills et les recherches sur Google peuvent aussi vous aider. Essayez donc de taper le nom d'une méthode suivi du mot-clé « polyfill », vous trouverez rapidement ce que vous cherchez. ;)

Depuis le début de ce cours, nous vous avons parlé de nombreuses méthodes et propriétés qui ne sont pas supportées par de vieux navigateurs (Internet Explorer étant souvent en cause). À chaque fois, nous avons tâché de vous fournir une solution fonctionnelle, cependant il existe trois méthodes pour lesquelles nous ne vous avions pas fourni de solutions car les polyfills sont bien plus adaptés. Vous trouverez donc ici un lien vers un polyfill pour chacune des méthodes désignées :

Introduction aux wrappers

La problématique

Il se peut que, par moments, vous ayez besoin de créer une méthode supplémentaire pour certains objets natifs du JavaScript. Pour cela, rien de plus simple, il vous suffit de l'ajouter au prototype de l'objet souhaité. Exemple :

Array.prototype.myMethod = function() {
// Votre code…
};
[].myMethod(); // La méthode myMethod() est maintenant disponible pour toutes les instances de tableaux

Mais est-ce que vous vous souvenez de ce qui a été dit dans le premier chapitre de cette partie du cours ? Voici un petit rappel :

Citation

En théorie, chaque objet peut se voir attribuer des méthodes via prototype. Mais en pratique, si cela fonctionne avec les objets natifs génériques commeString,Date,Array,Object,Number,Booleanet de nombreux autres, cela fonctionne moins bien avec les objets natifs liés au DOM commeNode,Elementou encoreHTMLElement, en particulier dans Internet Explorer.

De plus, la modification d'un objet natif est plutôt déconseillée au final, car vous risquez de modifier une méthode déjà existante. Bref, nous avons besoin de méthodes et de propriétés supplémentaires mais nous ne pouvons pas les ajouter sans risques, alors comment faire ?

La solution

Il existe une solution nommée « wrapper ». Un wrapper est un code qui a pour but d'encadrer l'utilisation de certains éléments du JavaScript. Il peut ainsi contrôler la manière dont ils sont employés et peut réagir en conséquence pour fournir des fonctionnalités supplémentaires aux développeurs.

Vous vous souvenez lorsque nous avions abordé l'objetImage? La propriétécompleteavait été évoquée mais non étudiée en raison de son comportement hasardeux. Nous allons ici essayer de permettre son support.

Tout d'abord, par quoi commence-t-on le développement d'un wrapper ? Comme dit plus haut, il s'agit d'un code qui a pour but d'encadrer l'utilisation de certains éléments, il s'agit en fait d'une surcouche par laquelle nous allons passer pour pouvoir contrôler nos éléments. Dans l'idéal, un wrapper doit permettre au développeur de se passer de l'élément original, ainsi le travail ne s'effectuera que par le biais de la surcouche que constitue le wrapper.

Puisque notre wrapper doit servir de surcouche de A à Z, celui-ci va se présenter sous forme d'objet qui sera instancié à la place de l'objetImage:

function Img() {
var obj = this; // Nous faisons une petite référence vers notre objet Img. Cela nous facilitera la tâche.
this.originalImg = new Image(); // On instancie l'objet original, le wrapper servira alors d'intermédiaire
}

Notre but étant de permettre le support de la propriétécomplete, nous allons devoir créer par défaut un événementloadqui se chargera de modifier la propriétécompletelors de son exécution. Il nous faut aussi assurer le support de l'événementloadpour les développeurs :

function Img() {
var obj = this; // Nous faisons une petite référence vers notre objet Img. Cela nous facilitera la tâche.
this.originalImg = new Image(); // On instancie l'objet original, le wrapper servira alors d'intermédiaire
this.complete = false;
this.onload = function() {}; // Voici l'événement que les développeurs pourront modifier
this.originalImg.onload = function() {
obj.complete = true; // L'image est chargée !
obj.onload(); // On exécute l'événement éventuellement spécifié par le développeur
};
}

Actuellement, notre wrapper fait ce qu'on voulait qu'il fasse : assurer un support de la propriétécomplete. Cependant, il nous est actuellement impossible de spécifier les propriétés standards de l'objet original sans passer par notre propriétéoriginalImg, or ce n'est pas ce que l'on souhaite car le développeur pourrait compromettre le fonctionnement de notre wrapper, par exemple en modifiant la propriétéonloadde l'objet original. Il va donc nous falloir créer une méthode permettant l'accès à ces propriétés sans passer par l'objet original.

Ajoutons donc deux méthodesset()etget()assurant le support des propriétés d'origine :

Img.prototype.set = function(name, value) {
var allowed = ['width', 'height', 'src']; // On spécifie les propriétés dont on autorise la modification
if (allowed.indexOf(name) != -1) {
this.originalImg[name] = value; // Si la propriété est autorisée alors on la modifie
}
};
Img.prototype.get = function(name) {
return this.originalImg[name]; // Pas besoin de contrôle tant qu'il ne s'agit pas d'une modification
};

Nous voici maintenant avec un wrapper relativement complet qui possède cependant une certaine absurdité : l'accès aux propriétés de l'objet d'origine se fait par le biais des méthodesset()etget(), tandis que l'accès aux propriétés relatives au wrapper se fait sans ces méthodes. Le principe est plutôt stupide vu qu'un wrapper a pour but d'être une surcouche transparente. La solution pourrait donc être la suivante : faire passer les modifications/lectures des propriétés par les méthodesset()etget()dans tous les cas, y compris lorsqu'il s'agit de propriétés appartenant au wrapper.

Mettons cela en place :

Img.prototype.set = function(name, value) {
var allowed = ['width', 'height', 'src'], // On spécifie les propriétés dont on autorise la modification
wrapperProperties = ['complete', 'onload'];
if (allowed.indexOf(name) != -1) {
this.originalImg[name] = value; // Si la propriété est autorisée alors on la modifie
} else if (wrapperProperties.indexOf(name) != -1) {
this[name] = value; // Ici, la propriété appartient au wrapper et non pas à l'objet original
}
};
Img.prototype.get = function(name) {
// Si la propriété n'existe pas sur le wrapper, on essaye alors sur l'objet original :
return typeof this[name] != 'undefined' ? this[name] : this.originalImg[name];
};

Nous approchons grandement du code final. Il nous reste maintenant une dernière chose à mettre en place qui peut se révéler pratique : pouvoir spécifier l'adresse de l'image dès l'instanciation de l'objet. La modification est simple :

function Img(src) { // On ajoute un paramètre « src »
var obj = this; // Nous faisons une petite référence vers notre objet Img. Cela nous facilitera la tâche.
this.originalImg = new Image(); // On instancie l'objet original, le wrapper servira alors d'intermédiaire
this.complete = false;
this.onload = function() {}; // Voici l'événement que les développeurs pourront modifier
this.originalImg.onload = function() {
obj.complete = true; // L'image est chargée !
obj.onload(); // On exécute l'événement éventuellement spécifié par le développeur
};
if (src) {
this.originalImg.src = src; // Si elle est spécifiée, on défini alors la propriété src
}
}

Et voilà ! Notre wrapper est terminé et entièrement opérationnel ! Voici le code complet dans le cas où vous auriez eu du mal à suivre :

function Img(src) {
var obj = this; // Nous faisons une petite référence vers notre objet Img. Cela nous facilitera la tâche.
this.originalImg = new Image(); // On instancie l'objet original, le wrapper servira alors d'intermédiaire
this.complete = false;
this.onload = function() {}; // Voici l'événement que les développeurs pourront modifier
this.originalImg.onload = function() {
obj.complete = true; // L'image est chargée !
obj.onload(); // On exécute l'événement éventuellement spécifié par le développeur
};
if (src) {
this.originalImg.src = src; // Si elle est spécifiée, on défini alors la propriété src
}
}
Img.prototype.set = function(name, value) {
var allowed = ['width', 'height', 'src'], // On spécifie les propriétés dont on autorise la modification
wrapperProperties = ['complete', 'onload'];
if (allowed.indexOf(name) != -1) {
this.originalImg[name] = value; // Si la propriété est autorisée alors on la modifie
} else if (wrapperProperties.indexOf(name) != -1) {
this[name] = value; // Ici, la propriété appartient au wrapper et non pas à l'objet original
}
};
Img.prototype.get = function(name) {
// Si la propriété n'existe pas sur le wrapper, on essaye alors sur l'objet original :
return typeof this[name] != 'undefined' ? this[name] : this.originalImg[name];
};

Faisons maintenant un essai :

var myImg = new Img(); // On crée notre objet Img
alert('complete : ' + myImg.get('complete')); // Vérification de la propriété complete : elle est bien à false
myImg.set('onload', function() { // Affichage de diverses informations une fois l'image chargée
alert(
'complete : ' + this.get('complete') + '\n' +
'width : ' + this.get('width') + ' px\n' +
'height : ' + this.get('height') + ' px'
);
});
myImg.set('src', 'http://www.sdz-files.com/cours/javascript/part3/chap9/img.png'); // On spécifie l'adresse de l'image

Essayer le code

Alors, c'est plutôt convaincant, non ?

Pour information, sachez que les wrappers sont à la base de nombreuses bibliothèques JavaScript. Ils ont l'avantage de permettre une gestion simple du langage sans pour autant l'altérer.

En résumé
  • Les polyfills sont un moyen de s'assurer de la prise en charge d'une méthode si celle-ci n'est pas supportée par le navigateur, et ce sans intervenir dans le code principal. C'est donc totalement transparent.

  • Les wrappers permettent d'ajouter des propriétés ou des méthodes aux objets, en particulier les objets natifs, en créant un objet dérivé de l'objet en question.