Kapitel 1. Asynchrone APIs

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Einführung

A Viele der in diesem Buch behandelten APIs sind asynchron. Wenn du eine dieser Funktionen oder Methoden aufrufst, bekommst du das Ergebnis möglicherweise nicht sofort zurück. Verschiedene APIs haben unterschiedliche Mechanismen, um dir das Ergebnis zurückzubekommen, wenn es fertig ist.

Rückruf-Funktionen

Das grundlegendste asynchrone Muster ist eine Callback-Funktion. Dies ist eine Funktion, die du an eine asynchrone API übergibst. Wenn die Arbeit abgeschlossen ist, ruft sie deinen Callback mit dem Ergebnis auf. Callbacks können eigenständig oder als Teil anderer asynchroner Muster verwendet werden.

Veranstaltungen

Viele Browser-APIs sind ereignisbasiert. Ein Ereignis ist etwas, das asynchron passiert. Einige Beispiele für Ereignisse sind:

  • Eine Schaltfläche wurde angeklickt.

  • Die Maus wurde bewegt.

  • Eine Netzwerkanfrage wurde abgeschlossen.

  • Es ist ein Fehler aufgetreten.

Ein Ereignis hat einen Namen, z. B. click oder mouseover, und ein Objekt mit Daten über das eingetretene Ereignis. Dazu gehören z. B. Informationen darüber, welches Element angeklickt wurde oder ein HTTP-Statuscode. Wenn du auf ein Ereignis wartest, stellst du eine Callback-Funktion bereit, die das Ereignisobjekt als Argument erhält.

Objekte, die Ereignisse ausgeben, implementieren die Schnittstelle EventTarget, die die Methoden addEventListener und removeEventListener bereitstellt. Um auf ein Ereignis für ein Element oder ein anderes Objekt zu warten, kannst du addEventListener aufrufen und dabei den Namendes Ereignisses und eine Handler-Funktion übergeben. Der Callback wird jedes Mal aufgerufen, wenn das Ereignisausgelöst wird, bis er entfernt wird. Ein Listener kann manuell entfernt werden, indem duremoveEventListeneraufgerufen wird, oder in vielen Fällen werden Listener automatisch vom Browser entfernt, wenn Objekte zerstört oder aus dem DOM entfernt werden.

Versprechen

Viele neuere APIs verwenden Promises. Ein Promise ist ein Objekt, das von einer Funktion zurückgegeben wird und einen Platzhalter für das spätere Ergebnis der asynchronen Aktion darstellt. Anstatt auf ein Ereignis zu warten, rufst du then für ein Promise Objekt auf. Du übergibst eine Callback-Funktion an then, die schließlich mit dem Ergebnis als Argument aufgerufen wird. Um Fehler zu behandeln, übergibst du eine weitere Callback-Funktion an die Methode catch von Promise.

Ein Promise wird erfüllt, wenn der Vorgang erfolgreich abgeschlossen wurde, und er wird abgelehnt, wenn ein Fehler auftritt. Der erfüllte Wert wird als Argument an den then Callback übergeben, oder der abgelehnte Wert wird als Argument an den catch Callback übergeben.

Es gibt ein paar wichtige Unterschiede zwischen Veranstaltungen und Promises:

  • Event-Handler werden mehrmals ausgelöst, während ein then Callback nur einmal ausgeführt wird. Du kannst dir Promise als einen einmaligen Vorgang vorstellen.

  • Wenn du then auf Promise aufrufst, erhältst du immer das Ergebnis (falls es eines gibt). Anders als bei Ereignissen: Wenn ein Ereignis eintritt, bevor du einen Listener hinzufügst, geht das Ereignis verloren.

  • Promises haben einen eingebauten Mechanismus zur Fehlerbehandlung. Bei Ereignissen musst du normalerweise auf ein Fehlerereignis warten, um Fehlerbedingungen zu behandeln.

Mit Versprechen arbeiten

Problem

Du möchtest eine API aufrufen, die Promises verwendet, und das Ergebnis abrufen.

Lösung

Rufe then für das Promise Objekt auf, um das Ergebnis in einer Callback-Funktion zu verarbeiten. Um mögliche Fehler zu behandeln, füge einen Aufruf an catch hinzu.

Stell dir vor, du hast eine Funktion getUsers, die eine Netzwerkanfrage stellt, um eine Liste von Benutzern zu laden. Diese Funktion gibt eine Promise zurück, die schließlich die Benutzerliste auflöst (siehe Beispiel 1-1).

Beispiel 1-1. Verwendung einer Promise-basierten API
getUsers()
  .then(
    // This function is called when the user list has been loaded.
    userList => {
      console.log('User List:');
      userList.forEach(user => {
        console.log(user.name);
      });
    }
  ).catch(error => {
    console.error('Failed to load the user list:', error);
  });

Diskussion

Das von getUsers zurückgegebene Promise ist ein Objekt mit einer then Methode. Wenn die Benutzerliste geladen ist, wird der an then übergebene Callback mit der Benutzerliste alsArgument ausgeführt.

Diese Promise hat auch eine catch Methode zur Fehlerbehandlung. Wenn beim Laden der Benutzerliste ein Fehler auftritt, wird der an catch übergebene Callback mit dem Fehlerobjekt aufgerufen. Je nach Ergebnis wird nur einer dieser Rückrufe aufgerufen.

Ein Bild mit einem Fallback laden

Problem

Du willst ein Bild laden, das auf der Seite angezeigt werden soll. Wenn beim Laden des Bildes ein Fehler auftritt, möchtest du als Ausweichlösung eine bekannte, gute Bild-URL verwenden.

Lösung

Erstelle programmatisch ein Image Element und warte auf die Ereignisse load und error. Wenn das Ereignis error ausgelöst wird, ersetze es durch das Platzhalterbild. Sobald das angeforderte Bild oder das Platzhalterbild geladen ist, fügst du es bei Bedarf zum DOM hinzu.

Für eine sauberere API kannst du dies in ein Promise verpacken. Promise löst entweder mit einem hinzuzufügenden Image auf oder weist mit einem Fehler zurück, wenn weder das Bild noch der Fallback geladen werden können (siehe Beispiel 1-2).

Beispiel 1-2. Laden eines Bildes mit einem Fallback
/**
 * Loads an image. If there's an error loading the image, uses a fallback
 * image URL instead.
 *
 * @param url The image URL to load
 * @param fallbackUrl The fallback image to load if there's an error
 * @returns a Promise that resolves to an Image element to insert into the DOM
 */
function loadImage(url, fallbackUrl) {
  return new Promise((resolve, reject) => {
    const image = new Image();

    // Attempt to load the image from the given URL
    image.src = url;

    // The image triggers the 'load' event when it is successfully loaded.
    image.addEventListener('load', () => {
      // The now-loaded image is used to resolve the Promise
      resolve(image);
    });

    // If an image failed to load, it triggers the 'error' event.
    image.addEventListener('error', error => {
      // Reject the Promise in one of two scenarios:
      // (1) There is no fallback URL.
      // (2) The fallback URL is the one that failed.
      if (!fallbackUrl || image.src === fallbackUrl) {
        reject(error);
      } else {
        // If this is executed, it means the original image failed to load.
        // Try to load the fallback.
        image.src = fallbackUrl;
      }
    });
  });
}

Diskussion

Die Funktion loadImage nimmt eine URL und eine Fallback-URL und gibt eine Promise zurück. Dann erstellt sie eine neue Image und setzt ihr src Attribut auf die angegebene URL. Der Browser versucht, das Bild zu laden.

Es gibt drei mögliche Ergebnisse:

Erfolgsfall

Wenn das Bild erfolgreich geladen wurde, wird das Ereignis load ausgelöst. Der Ereignishandler löst die Promise mit der Image auf, die dann in das DOM eingefügt werden kann.

Fallback-Fall

Wenn das Laden des Bildes fehlschlägt, wird das Ereignis error ausgelöst. Der Error-Handler setzt das Attribut src auf die Fallback-URL, und der Browser versucht, das Fallback-Bild zu laden. Wenn dies erfolgreich ist, wird das Ereignis load ausgelöst und das Promise mit dem Fallback Image aufgelöst.

Fall des Scheiterns

Wenn weder das Bild noch das Fallback-Bild geladen werden konnten, weist der Error-Handler die Promise mit dem Ereignis error zurück.

Das Ereignis error wird jedes Mal ausgelöst, wenn es einen Ladefehler gibt. Der Handler prüft zunächst, ob die Fallback-URL fehlgeschlagen ist. Wenn ja, bedeutet das, dass sowohl die ursprüngliche URL als auch die Fallback-URL fehlgeschlagen sind. In diesem Fall wird die Promise abgelehnt.

Wenn es nicht die Fallback-URL ist, bedeutet das, dass das Laden der angeforderten URL fehlgeschlagen ist. Jetzt wird die Fallback-URL festgelegt und versucht, diese zu laden.

Die Reihenfolge der Prüfungen ist hier wichtig. Wenn die erste Prüfung fehlschlägt, würde der Fehlerhandler eine Endlosschleife auslösen, in der die (ungültige) Fallback-URL gesetzt, angefordert und das Ereignis error erneut ausgelöst wird.

Beispiel 1-3 zeigt, wie diese Funktion loadImage verwendet wird.

Beispiel 1-3. Verwendung der Funktion loadImage
loadImage('https://example.com/profile.jpg', 'https://example.com/fallback.jpg')
  .then(image => {
    // container is an element in the DOM where the image will go
    container.appendChild(image);
  }).catch(error => {
    console.error('Image load failed');
  });

Verkettung von Versprechen

Problem

Du möchtest mehrere Promise-basierte APIs nacheinander aufrufen. Jeder Vorgang hängt vom Ergebnis des vorherigen ab.

Lösung

Verwende eine Kette von Promises, um die asynchronen Aufgaben nacheinander auszuführen. Stell dir eine Blog-Anwendung mit zwei APIs vor, die beide Promises zurückgeben:

getUser(id)

Lädt einen Benutzer mit der angegebenen Benutzer-ID

getPosts(user)

Lädt alle Blogbeiträge für einen bestimmten Benutzer

Wenn du die Beiträge für einen Benutzer laden willst, musst du zuerst das user Objekt laden - du kannst getPosts erst aufrufen, wenn die Benutzerdetails geladen sind. Du kannst dies tun, indem du die beiden Promises miteinander verknüpfst, wie in Beispiel 1-4 gezeigt.

Beispiel 1-4. Eine Promise Kette verwenden
/**
 * Loads the post titles for a given user ID.
 * @param userId is the ID of the user whose posts you want to load
 * @returns a Promise that resolves to an array of post titles
 */
function getPostTitles(userId) {
  return getUser(userId)
    // Callback is called with the loaded user object
    .then(user => {
      console.log(`Getting posts for ${user.name}`);
      // This Promise is also returned from .then
      return getPosts(user);
    })
    // Calling then on the getPosts' Promise
    .then(posts => {
      // Returns another Promise that will resolve to an array of post titles
      return posts.map(post => post.title);
    })
    // Called if either getUser or getPosts are rejected
    .catch(error => {
      console.error('Error loading data:', error);
    });
}

Diskussion

Der Wert, der von einem Promise's then Handler zurückgegeben wird, wird in ein neues Promise verpackt. Dieses Promise wird von der Methode then selbst zurückgegeben. Das bedeutet, dass der Rückgabewert von then auch ein Promise ist, so dass du einen weiteren then an ihn ketten kannst. So erstellst du eine Kette von Promises.

getUser gibt ein Promise zurück, das in das user Objekt aufgelöst wird. Der then Handler ruft getPosts auf und gibt das Ergebnis Promise zurück, das wiederum von then zurückgegeben wird, so dass du then noch einmal aufrufen kannst, um das Endergebnis, das Array der Posts, zu erhalten.

Am Ende der Kette steht ein Aufruf an catch, um eventuelle Fehler zu behandeln. Dies funktioniert wie ein try/catch Block. Wenn an irgendeiner Stelle der Kette ein Fehler auftritt, wird der catch Handler mit diesem Fehler aufgerufen und der Rest der Kette wird nicht ausgeführt .

Verwendung der Schlüsselwörter async und await

Problem

Du arbeitest mit einer API, die eine Promise zurückgibt, aber du möchtest, dass der Code linearer oder synchroner gelesen wird.

Lösung

Verwende das Schlüsselwort await mit dem Promise, anstatt then aufzurufen (siehe Beispiel 1-5). Betrachte noch einmal die Funktion getUsers aus "Arbeiten mit Promises". Diese Funktion gibt eine Promise zurück, die in eine Liste von Benutzern aufgelöst wird.

Beispiel 1-5. Verwendung des Schlüsselworts await
// A function must be declared with the async keyword
// in order to use await in its body.
async function listUsers() {
  try {
    // Equivalent to getUsers().then(...)
    const userList = await getUsers();
    console.log('User List:');
    userList.forEach(user => {
      console.log(user.name);
    });
  } catch (error) { // Equivalent to .catch(...)
    console.error('Failed to load the user list:', error);
  }
}

Diskussion

await ist eine alternative Syntax für die Arbeit mit Promises. Anstatt then mit einem Callback aufzurufen, der das Ergebnis als Argument nimmt, "pausiert" der Ausdruck effektiv die Ausführung des Rests der Funktion und gibt das Ergebnis zurück, wenn die Promise erfüllt ist.

Wenn die Promise abgelehnt wird, wirft der await Ausdruck den abgelehnten Wert. Dies wird mit einem Standard try/catch Block behandelt.

Parallele Verwendung von Promises

Problem

Du willst eine Reihe von asynchronen Aufgaben mit Promiseparallel ausführen.

Lösung

Sammle alle Promises und übergebe sie an Promise.all. Diese Funktion nimmt ein Array von Promises und wartet, bis sie alle erfüllt sind. Sie gibt ein neues Promise zurück, das erfüllt ist, wenn alle angegebenen Promises erfüllt sind, oder sie lehnt ab, wenn eine der angegebenen Promises abgelehnt wurde (siehe Beispiel 1-6).

Beispiel 1-6. Laden mehrerer Benutzer mit Promise.all
// Loading three users at once
Promise.all([
  getUser(1),
  getUser(2),
  getUser(3)
]).then(users => {
  // users is an array of user objects—the values returned from
  // the parallel getUser calls
}).catch(error => {
  // If any of the above Promises are rejected
  console.error('One of the users failed to load:', error);
});

Diskussion

Wenn du mehrere Aufgaben hast, die nicht voneinander abhängig sind, ist Promise.all eine gute Wahl. Beispiel 1-6 ruft getUser dreimal auf und übergibt jedes Mal eine andere Benutzerkennung. Es sammelt diese Promises in einem Array, das an Promise.all übergeben wird. Alle drei Anfragen laufen parallel.

Promise.all gibt ein weiteres Promise zurück. Wenn alle drei Benutzer erfolgreich geladen wurden, wird dieses neue Promise mit einem Array erfüllt, das die geladenen Benutzer enthält. Der Index jedes Ergebnisses entspricht dem Index des Promise im Eingabe-Array. In diesem Fall wird ein Array mit den Benutzern 1, 2 und 3 zurückgegeben, und zwar in dieser Reihenfolge.

Was ist, wenn einer oder mehrere dieser Benutzer nicht geladen werden konnten? Vielleicht existiert eine der Benutzer-IDs nicht oder es gab einen vorübergehenden Netzwerkfehler. Wenn eine der Promises, die an Promise.all übergeben wurden, abgelehnt wird, wird auch die neue Promise sofort abgelehnt. Der Ablehnungswert ist der gleiche wie der des abgelehnten Promise.

Wenn einer der Benutzer beim Laden fehlschlägt, wird die von Promise.all zurückgegebene Promise mit dem aufgetretenen Fehler zurückgewiesen. Die Ergebnisse der anderen Promises sind verloren.

Wenn du trotzdem die Ergebnisse aller erledigten Promises (oder Fehler von anderen abgelehnten) erhalten möchtest, kannst du stattdessen Promise.allSettled verwenden. Mit Promise.allSettled wird ein neues Promise zurückgegeben, genau wie mit Promise.all. Allerdings wird dieses Promise immer erfüllt, sobald alle Promises erledigt sind (entweder erfüllt oder abgelehnt).

Wie in Beispiel 1-7 gezeigt, ist der aufgelöste Wert ein Array, dessen Elemente jeweils eine status Eigenschaft haben. Diese ist entweder fulfilled oder rejected, je nach dem Ergebnis von Promise. Wenn der Status fulfilled ist, hat das Objekt auch eine value Eigenschaft, die der aufgelöste Wert ist. Lautet der Status hingegen rejected, hat es stattdessen einereason Eigenschaft, die der abgelehnte Wert ist.

Beispiel 1-7. verwenden Promise.allSettled
Promise.allSettled([
  getUser(1),
  getUser(2),
  getUser(3)
]).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('- User:', result.value.name);
    } else {
      console.log('- Error:', result.reason);
    }
  });
});
// No catch necessary here because allSettled is always fulfilled.

Ein Element mit requestAnimationFrame animieren

Problem

Du willst ein Element auf performante Weise mit JavaScript animieren.

Lösung

Verwende die Funktion requestAnimationFrame, um deine Animationsaktualisierungen in regelmäßigen Abständen durchzuführen.

Stell dir vor, du hast ein div Element, das du mit einer Fade-Animation ausblenden möchtest. Dazu wird die Deckkraft in regelmäßigen Abständen angepasst, indem ein Callback an request​A⁠nimationFrame übergeben wird (siehe Beispiel 1-8). Die Dauer der einzelnen Intervalle hängt von den gewünschten Bildern pro Sekunde (FPS) der Animation ab.

Beispiel 1-8. Ausblendungsanimation mit requestAnimationFrame
const animationSeconds = 2; // Animate over 2 seconds
const fps = 60; // A nice, smooth animation

// The time interval between each frame
const frameInterval = 1000 / fps;

// The total number of frames for the animation
const frameCount = animationSeconds * fps;

// The amount to adjust the opacity by in each frame
const opacityIncrement = 1 / frameCount;

// The timestamp of the last frame
let lastTimestamp;

// The starting opacity value
let opacity = 1;

function fade(timestamp) {
  // Set the last timestamp to now if there isn't an existing one.
  if (!lastTimestamp) {
    lastTimestamp = timestamp;
  }

  // Calculate how much time has elapsed since the last frame.
  // If not enough time has passed yet, schedule another call of this
  // function and return.
  const elapsed = timestamp - lastTimestamp;
  if (elapsed < frameInterval) {
    requestAnimationFrame(animate);
    return;
  }

  // Time for a new animation frame. Remember this timestamp.
  lastTimestamp = timestamp;

  // Adjust the opacity value and make sure it doesn't go below 0.
  opacity = Math.max(0, opacity - opacityIncrement)
  box.style.opacity = opacity;

  // If the opacity hasn't reached the target value of 0, schedule another
  // call to this function.
  if (opacity > 0) {
    requestAnimationFrame(animate);
  }
}

// Schedule the first call to the animation function.
requestAnimationFrame(fade);

Diskussion

Dies ist eine gute, performante Methode, um Elemente mit JavaScript zu animieren, die vom Browser gut unterstützt wird. Da die Animation asynchron ausgeführt wird, blockiert sie nicht den Hauptthread des Browsers. Wenn der Nutzer zu einem anderen Tab wechselt, wird die Animation angehalten und requestAnimationFrame wird nicht unnötig aufgerufen.

Wenn du die Ausführung einer Funktion mit requestAnimationFrame planst, wird die Funktion vor dem nächsten Malvorgang aufgerufen. Wie oft dies geschieht, hängt vom Browser und der Bildwiederholrate ab.

Vor der Animation führt Beispiel 1-8 einige Berechnungen auf der Grundlage einer vorgegebenen Animationsdauer (2 Sekunden) und Bildrate (60 Bilder pro Sekunde) durch. Es berechnet die Gesamtzahl der Bilder und verwendet die Dauer, um zu berechnen, wie lange jedes Bild läuft. Wenn du eine andere Bildrate haben möchtest, die nicht mit der Aktualisierungsrate des Systems übereinstimmt, wird festgehalten, wann die letzte Aktualisierung der Animation durchgeführt wurde, um deine Zielbildrate beizubehalten.

Dann berechnet es anhand der Anzahl der Bilder die Deckkraftanpassung, die in jedem Bild vorgenommen wird.

Die Funktion fade wird geplant, indem sie an einen Aufruf von requestAnimationFrame übergeben wird. Jedes Mal, wenn der Browser diese Funktion aufruft, übergibt er einen Zeitstempel. Die Funktion fade berechnet, wie viel Zeit seit dem letzten Frame verstrichen ist. Wenn noch nicht genug Zeit vergangen ist, unternimmt sie nichts und bittet den Browser, beim nächsten Mal erneut aufzurufen.

Sobald genügend Zeit vergangen ist, wird ein Animationsschritt ausgeführt. Er nimmt die berechnete Deckkraftanpassung und wendet sie auf den Stil des Elements an. Je nach genauem Timing kann dies zu einer Deckkraft von weniger als 0 führen, was ungültig ist. Dies wird mit Math.max behoben, indem ein Mindestwert von 0 festgelegt wird.

Wenn die Deckkraft noch nicht 0 erreicht hat, müssen weitere Animationsbilder ausgeführt werden. Es ruft erneut requestAnimationFrame auf, um die nächste Ausführung zu planen.

Als Alternative zu dieser Methode unterstützen neuere Browser die Web Animations API, die du in Kapitel 8 kennenlernen wirst. Mit dieser API kannst du Keyframes mit CSS-Eigenschaften festlegen, und der Browser übernimmt die Aktualisierung der Zwischenwerte für .

Eine Ereignis-API in ein Versprechen verpacken

Problem

Du möchtest eine ereignisbasierte API umhüllen, um eine Promise zurückzugeben.

Lösung

Erstelle ein neues Promise Objekt und registriere in seinem Konstruktor Ereignis-Listener. Wenn du das Ereignis erhältst, auf das du wartest, löse die Promise mit dem Wert auf. Verwirf das Promise, wenn ein Fehlerereignis eintritt.

Manchmal wird dies auch als "Versprechen" einer Funktion bezeichnet. Beispiel 1-9 zeigt, wie die XMLHttpRequest API versprochen wird.

Beispiel 1-9. Das Versprechen der XMLHttpRequest API
/**
 * Sends a GET request to the specified URL. Returns a Promise that will resolve to
 * the JSON body parsed as an object, or will reject if there is an error or the
 * response is not valid JSON.
 *
 * @param url The URL to request
 * @returns a Promise that resolves to the response body
 */
function loadJSON(url) {
  // Create a new Promise object, performing the async work inside the
  // constructor function.
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();

    // If the request is successful, parse the JSON response and
    // resolve the Promise with the resulting object.
    request.addEventListener('load', event => {
      // Wrap the JSON.parse call in a try/catch block just in case
      // the response body is not valid JSON.
      try {
        resolve(JSON.parse(event.target.responseText));
      } catch (error) {
        // There was an error parsing the response body.
        // Reject the Promise with this error.
        reject(error);
      }
    });

    // If the request fails, reject the Promise with the
    // error that was emitted.
    request.addEventListener('error', error => {
      reject(error);
    });

    // Set the target URL and send the request.
    request.open('GET', url);
    request.send();
  });
}

Beispiel 1-10 zeigt, wie du die versprochene Funktion loadJSON verwenden kannst.

Beispiel 1-10. Verwendung der Hilfe loadJSON
// Using .then
loadJSON('/api/users/1').then(user => {
  console.log('Got user:', user);
})

// Using await
const user = await loadJSON('/api/users/1');
console.log('Got user:', user);

Diskussion

Du erstellst eine Promise, indem du die Promise Konstruktorfunktion mit dem new Operator aufrufst. Diese Funktion erhält zwei Argumente, eine resolve und reject Funktion.

Die Funktionen resolve und reject werden von der JavaScript-Engine bereitgestellt. Im Promise Konstruktor erledigst du deine asynchrone Arbeit und wartest auf Ereignisse. Wenn die Funktion resolve aufgerufen wird, wird Promise sofort auf diesen Wert aufgelöst. Der Aufruf von reject funktioniert auf die gleiche Weise - er weist die Funktion Promise mit dem Fehler zurück.

Die Erstellung eines eigenen Promise kann in solchen Situationen hilfreich sein, aber im Allgemeinen musst du sie nicht manuell erstellen. Wenn eine API bereits eine Promise zurückgibt, musst du diese nicht in deine eigene Promiseverpacken - verwende sie einfach direkt .

Get Web API Kochbuch now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.