🇫🇷 Un Proxy en JS, c'est bien pratique

Tout d'abord, tous mes voeux pour vous et vos proches. Que cette nouvelle année soit pleine de nouveautés, de curiosités et surtout l'occasion de se retrouver plus souvent en famille et entre amis. 😘

Pour mon 1er article de l'année (en français car je 'ai pas le courage ce matin de faire l'exercice en anglais), j'ai fait un peu de "Vanilla JS" pour quelques expérimentations et j'ai joué avec les proxies (en JavaScript). Je vais donc vous donner un cas d'usage d'un Proxy JS: J'ai été et suis encore très fan de Backbone, et ce que j'aime par dessus tout dans Backbone ce sont les models. Donc, mon expérimentation consiste en l'écriture des prémices d'un successeur des Backbone Models (c'est très présomptueux, mais c'est Dimanche, soyons fous 🤪)

👋 c'est une expérimentation, et je n'ai pas encore exploré toutes les possibilités du Proxy, donc n'hésitez pas à proposer des corrections, améliorations, etc. ...

Mais qu'est-ce qu'un proxy en JavaScript?

Sur le site de Mozilla, vous trouverez cette définition:

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

Source:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Si je m'essaye à une définition en français, je dirais que le Proxy est un objet qui réagit à différents événements, comme par exemple le changement de valeur d'une propriété.

1er Proxy

Ce qui m'intéresse essentiellement aujourd'hui, ce sont les propriétés de mon objet (autrement dit les getters et les setters). Un Proxy est un objet qui "englobe" un autre objet. On pourrait dire que le Proxy est un wrapper et nous appellerons l'"autre objet" la target. Le Proxy possède aussi un objet handler qui "possède" toutes les fonctions qui sont déclenchées lors de certaines actions (on parle aussi de trap), comme par exemple [handler.get()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get) ou [handler.set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set).

Par exemple, je vais utiliser le code ci-dessous dans la console de mon navigateur:

let johnDoe = new Proxy({ // Object
  firstName: "John",
  lastName: "Doe"
}, { // traps handler
  get: function(target, property) {
    return `👋${target[property]}🖖`
  },
  set: function(target, property, value) {
    console.log("old value:", target[property])
    console.log("new value:", value)
    target[property] = value
    return true
  }
})

Et le résultat va être le suivant:

text

Je trouve ça plutôt sympa, pas vous? Cela peut être très pratique. J'ai donc voulu aller plus loin, toujours avec mon idée de models en tête.

Donc, les prémices de mon modèle

J'ai bien bricolé, mais à chaque fois le résultat obtenu ne me convenait pas. Je me suis arrêté un moment, pris le temps de feuilleter l'excellent livre Exploring ES6 du non moins excellent Axel Rauschmayer

👋 Tous les livres d'Axel sont excellents

L'extrait qui ma rendu service est ici: https://exploringjs.com/es6/ch_proxies.html#sec_proxy-use-cases au § 28.4.2. Axel explique comment transformer un Proxy en constructor (et donc cela vous donne la possibilité d'en hériter):

function PropertyChecker() { }
PropertyChecker.prototype = new Proxy(···);

class Point extends PropertyChecker {
    constructor(x, y) {
        super();
        this.x = x;
        this.y = y;
    }
}

Petit détour avant de continuer: construisons un WebComponent

Pour afficher les modifications de mon futur modèle, je vais utiliser un web component (fait from scratch, mais c'est ensuite adaptable). Donc, créez une page index.html avec le code suivant:

<!doctype html>
<html lang="en">
<head>  
  <title>have fun with web components</title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta charset="utf-8">
  <style></style>
</head>
  <body>  

    <my-buddy 
      id="john-doe"
      avatar="😎"   
      firstName="???" 
      lastName="???">
    </my-buddy>
    
  </body>
  <script>
    
    class MyBuddy extends HTMLElement {
      
      connectedCallback() {
        this.attachShadow({mode: 'open'})
        this.render()
      }

      render() {
        this.shadowRoot.innerHTML=`
          <h1>
            ${this.getAttribute("avatar")} ${this.getAttribute("firstName")} ${this.getAttribute("lastName")}
          </h1>
        `
      }
    }

    customElements.define('my-buddy', MyBuddy)

  </script>
</html>

Puis ajoutons notre modèle

Ensuite, toujours dans index.html, ajoutons notre Proxy transformé en constructor (BuddyProxy) et notre modèle Buddy qui hérite de BuddyProxy

function BuddyProxy() {}
BuddyProxy.prototype = new Proxy({
  on: function(eventName, eventHandler) {
    this[eventName]=eventHandler
  }
}, 
{
  set: function(target, property, value) {
    // old property value: target[property]
    // new property value: value
    target[property] = value
    // if the `change` method exists, call it
    if (target.change) target.change(target, property)
    
    return true
  }
})

class Buddy extends BuddyProxy {
  constructor(options) {
    super()
    Object.assign(this, options)
  }
}

👋 Remarque: j'ai ajouté à la target du Proxy, une méthode on(eventName, eventHandler) qui va me permettre d'ajouter dynamiquement des méthodes à l'instance de mon modèle. et j'appelle cette méthode dans le set du Proxy (ex: si une méthode change existe: if (target.change) target.change(target, property)).

Voici un exemple:

bob.on("change", (model, property) => {
  console.log(` this ${property} has changed: ${model[property]}`)
})

il y a différentes façons de faire ceci, je le rappelle, c'est une expérimentation.

Sauvegardez votre page et "ouvrez" la dans un navigateur:

text

Alors le chemin est long avant de rivaliser avec Backbone, mais cela va me faire un nouveau side project sympa.

Bonne soirée et bonne reprise demain.

Last Articles

Last Updated: 05/01/2020 à 17:35:11