Verwendung von JavaScript-Proxies für Spaß und Gewinn

Es gibt eine sehr neue Funktion der JavaScript-Sprache, die noch nicht weit verbreitet ist: JavaScript-Proxys.

Mit JavaScript-Proxys können Sie ein vorhandenes Objekt umbrechen und den Zugriff auf seine Attribute oder Methoden abfangen. Auch wenn es sie nicht gibt!

Sie können Aufrufe von Methoden abfangen, die nicht vorhanden sind

Hallo Weltproxy

Beginnen wir mit den Grundlagen. Ein "Hallo Welt" Beispiel könnte sein:

const wrap = obj => {
  neuen Proxy zurückgeben (obj, {
    get (target, propKey) {
        console.log (`Leseeigenschaft" $ {propKey} "`)
        Rückgabeziel [propKey]
    }
  })
}
const object = {message: 'hallo welt'}
const wrapped = wrap (Objekt)
console.log (wrapped.message)

Welche Ausgänge:

Leseeigenschaft "Nachricht"
Hallo Welt

In diesem Beispiel tun wir etwas, bevor wir auf die Eigenschaft / Methode zugreifen. Aber dann geben wir die ursprüngliche Eigenschaft oder Methode zurück.

Sie können Änderungen an Eigenschaften auch abfangen, indem Sie einen Set-Handler implementieren.

Dies kann nützlich sein, um Attribute oder ähnliches zu validieren. Ich halte diese Funktion jedoch für viel erfolgversprechender. Ich hoffe, dass neue Frameworks entstehen, die Proxys für ihre Kernfunktionalität verwenden. Ich habe darüber nachgedacht und dies sind einige Ideen:

Ein SDK für eine API mit 20 Codezeilen

Wie gesagt, Sie können Methodenaufrufe für Methoden abfangen, die es noch nicht einmal gibt. Wenn jemand eine Methode für ein Proxy-Objekt aufruft, wird der Get-Handler aufgerufen, und Sie können eine dynamisch generierte Funktion zurückgeben. Sie müssen das Proxy-Objekt nicht berühren, wenn Sie dies nicht müssen.

Mit dieser Idee können Sie die aufgerufene Methode analysieren und ihre Funktionalität dynamisch in der Laufzeit implementieren! Zum Beispiel könnten wir einen Proxy haben, der, wenn er mit api.getUsers () aufgerufen wird, ein GET / users in einer API erzeugt. Mit dieser Konvention können wir weiter gehen und api.postItems ({name: ‘Item name '}) würde POST / items mit dem ersten Parameter als Anforderungshauptteil aufrufen.

Sehen wir uns eine vollständige Implementierung an:

const {METHODS} = require ('http')
const api = neuer Proxy ({},
  {
    get (target, propKey) {
      const method = METHODS.find (method =>
        propKey.startsWith (method.toLowerCase ()))
      if (! Methode) return
      const path =
        '/' +
        propKey
          .substring (method.length)
          .replace (/ ([a-z]) ([A-Z]) / g, '$ 1 / $ 2')
          .replace (/ \ $ / g, '/ $ /')
          .toLowerCase ()
      return (... args) => {
        const finalPath = path.replace (/ \ $ / g, () => args.shift ())
        const queryOrBody = args.shift () || {}
        // Du könntest hier fetch benutzen
        // return fetch (finalPath, {method, body: queryOrBody})
        console.log (Methode, finalPath, queryOrBody)
      }
    }
  }
)
// ERHALTEN /
api.get ()
// GET / users
api.getUsers ()
// GET / users / 1234 / likes
api.getUsers $ Likes ('1234')
// GET / users / 1234 / likes? Page = 2
api.getUsers $ Likes ('1234', {page: 2})
// POST / Items mit body
api.postItems ({name: 'Item name'})
// api.foobar ist keine Funktion
api.foobar ()

Hier ist das Proxy-Objekt nur {}, da alle Methoden dynamisch implementiert werden. Wir müssen eigentlich kein funktionales Objekt einwickeln.

Sie werden sehen, dass einige Methoden ein $ haben. Dies ist ein Platzhalter für eingebettete Parameter.

Wenn Ihnen das nicht gefällt, könnte es auf andere Weise implementiert werden.

Randnotiz: Diese Beispiele können optimiert werden. Sie können die dynamisch generierte Funktion in einem Hash-Objekt zwischenspeichern, anstatt jedes Mal eine neue Funktion zurückzugeben. Aber der Klarheit halber habe ich es für die Beispiele so belassen.

Abfragen von Datenstrukturen mit besser lesbaren Methoden

Was wäre, wenn Sie eine Vielzahl von Menschen hätten und Folgendes tun könnten:

arr.findWhereNameEquals ('Lily')
arr.findWhereSkillsIncludes ('Javascript')
arr.findWhereSkillsIsEmpty ()
arr.findWhereAgeIsGreaterThan (40)

Klar kannst du das mit Proxies! Wir können einen Proxy implementieren, der ein Array umschließt, Methodenaufrufe analysiert und Abfragen wie diese ausführt.

Ich habe hier einige Möglichkeiten implementiert:

const camelcase = require ('camelcase')
const prefix = 'findWhere'
const assertions = {
  Gleich: (Objekt, Wert) => Objekt === Wert,
  IsNull: (Objekt, Wert) => Objekt === null,
  IsUndefined: (Objekt, Wert) => Objekt === undefined,
  IsEmpty: (Objekt, Wert) => object.length === 0,
  Includes: (Objekt, Wert) => object.includes (Wert),
  IsLowerThan: (Objekt, Wert) => Objekt === Wert,
  IsGreaterThan: (Objekt, Wert) => Objekt === Wert
}
const assertionNames = Object.keys (Zusicherungen)
const wrap = arr => {
  neuen Proxy zurückgeben (arr, {
    get (target, propKey) {
      if (propKey in target) Ziel zurückgeben [propKey]
      const assertionName = assertionNames.find (assertion =>
        propKey.endsWith (Zusicherung))
      if (propKey.startsWith (Präfix)) {
        const field = camelcase (
          propKey.substring (Präfix.Länge,
            propKey.length - assertionName.length)
        )
        const assertion = Zusicherungen [assertionName]
        Rückgabewert => {
          return target.find (item => assertion (item [field], value))
        }
      }
    }
  })
}
const arr = wrap ([
  {Name: 'John', Alter: 23, Fähigkeiten: ['mongodb']},
  {Name: 'Lily', Alter: 21, Fähigkeiten: ['redis']},
  {Name: 'Iris', Alter: 43, Fähigkeiten: ['Python', 'Javascript']}
])
console.log (arr.findWhereNameEquals ('Lily')) // findet Lily
console.log (arr.findWhereSkillsIncludes ('Javascript')) // findet Iris

Es wäre super ähnlich, eine Assertionsbibliothek zu schreiben, wie man es von Proxies erwartet.

Eine andere Idee wäre, eine Bibliothek zu erstellen, um Datenbanken mit einer API wie dieser abzufragen:

const id = wait db.insertUserReturningId (userInfo)
// Führt einen INSERT INTO-Benutzer aus ... RETURNING-ID

Überwachung von Async-Funktionen

Da Sie Methodenaufrufe abfangen können, können Sie auch nachverfolgen, wenn ein Methodenaufruf ein Versprechen zurückgibt, wenn das Versprechen erfüllt ist. Mit dieser Idee habe ich ein kurzes Beispiel für das Überwachen der asynchronen Methoden eines Objekts und das Drucken einiger Statistiken in der Befehlszeile erstellt.

Sie haben einen Service wie den folgenden und können ihn mit einem Methodenaufruf umbrechen:

const service = {
  callService () {
    neues Versprechen zurückgeben (Beschluss =>
      setTimeout (resolve, Math.random () * 50 + 50))
  }
}
const monitoringService = monitor (Dienst)
monitoringService.callService () // Wir möchten dies überwachen

Dies ist ein vollständiges Beispiel:

const logUpdate = require ('log-update')
const asciichart = require ('asciichart')
const chalk = require ('chalk')
const Measured = require ('gemessen')
const timer = new Measured.Timer ()
const history = neues Array (120)
history.fill (0)
const monitor = obj => {
  neuen Proxy zurückgeben (obj, {
    get (target, propKey) {
      const origMethod = target [propKey]
      if (! origMethod) gibt zurück
      return (... args) => {
        const stopwatch = timer.start ()
        const result = origMethod.apply (this, args)
        return result.then (out => {
          const n = stopwatch.end ()
          history.shift ()
          history.push (n)
          geh raus
        })
      }
    }
  })
}
const service = {
  callService () {
    neues Versprechen zurückgeben (Beschluss =>
      setTimeout (resolve, Math.random () * 50 + 50))
  }
}
const monitoringService = monitor (Dienst)
setInterval (() => {
  monitoringService.callService ()
    .then (() => {
      const fields = ['min', 'max', 'sum', 'varianz',
        'Mittelwert', 'Anzahl', 'Median']
      const histogram = timer.toJSON (). histogram
      const lines = [
        '',
        ... fields.map (field =>
          chalk.cyan (Feld) + ':' +
          (Histogramm [Feld] || 0) .toFixed (2))
      ]
      logUpdate (asciichart.plot (Verlauf, {Höhe: 10})
        + lines.join ('\ n'))
    })
    .catch (err => console.error (err))
}, 100)

JavaScript-Proxys sind sehr leistungsfähig.

Sie erhöhen den Aufwand etwas, aber auf der anderen Seite macht die Fähigkeit, Methoden zur Laufzeit dynamisch zu implementieren, den Code überaus elegant und lesbar. Ich habe noch keine Benchmarks erstellt, aber wenn Sie diese in der Produktion verwenden möchten, würde ich zuerst einige Leistungstests durchführen.

Es gibt jedoch kein Problem, sie für die Entwicklung zu verwenden, wie die Überwachung von asynchronen Funktionen zum Debuggen!