ActiveResource.js: Erstellen Sie schnell ein leistungsstarkes JavaScript-SDK für Ihre JSON-API

Veröffentlicht: 2022-03-11

Ihr Unternehmen hat gerade seine API eingeführt und möchte nun eine Community von Benutzern darum herum aufbauen. Sie wissen, dass die meisten Ihrer Kunden mit JavaScript arbeiten werden, da die von Ihrer API bereitgestellten Dienste es den Kunden erleichtern, Webanwendungen zu erstellen, anstatt alles selbst zu schreiben – Twilio ist ein gutes Beispiel dafür.

Sie wissen auch, dass Benutzer, so einfach Ihre RESTful-API auch sein mag, ein JavaScript-Paket einfügen möchten, das ihnen die ganze schwere Arbeit abnimmt. Sie werden Ihre API nicht lernen und jede benötigte Anfrage selbst erstellen wollen.

Sie bauen also eine Bibliothek um Ihre API herum auf. Oder vielleicht schreiben Sie gerade ein Zustandsverwaltungssystem für eine Webanwendung, die mit Ihrer eigenen internen API interagiert.

In jedem Fall möchten Sie sich nicht jedes Mal wiederholen, wenn Sie eine Ihrer API-Ressourcen CRUD, oder noch schlimmer, eine Ressource, die sich auf diese Ressourcen bezieht, CRUD. Dies ist weder für die Verwaltung eines wachsenden SDKs auf lange Sicht gut, noch ist es eine gute Nutzung Ihrer Zeit.

Stattdessen können Sie ActiveResource.js verwenden, ein JavaScript-ORM-System für die Interaktion mit APIs. Ich habe es erstellt, um einen Bedarf zu decken, den wir bei einem Projekt hatten: ein JavaScript-SDK in so wenigen Zeilen wie möglich zu erstellen. Dies ermöglichte maximale Effizienz für uns und für unsere Entwickler-Community.

Es basiert auf den Prinzipien hinter dem einfachen ActiveRecord ORM von Ruby on Rails.

JavaScript-SDK-Prinzipien

Es gibt zwei Ruby on Rails-Ideen, die das Design von ActiveResource.js geleitet haben:

  1. „Konvention über Konfiguration“: Machen Sie einige Annahmen über die Art der Endpunkte der API. Wenn Sie beispielsweise über eine Product verfügen, entspricht diese dem /products -Endpunkt. Auf diese Weise wird keine Zeit damit verbracht, jede Ihrer SDK-Anforderungen an Ihre API wiederholt zu konfigurieren. Entwickler können Ihrem wachsenden SDK in Minuten statt Stunden neue API-Ressourcen mit komplizierten CRUD-Abfragen hinzufügen.
  2. „Hervorheben von schönem Code“: Rails-Schöpfer DHH hat es am besten ausgedrückt – es gibt einfach etwas Großartiges an schönem Code um seiner selbst willen. ActiveResource.js verpackt manchmal hässliche Anfragen in ein schönes Äußeres. Sie müssen keinen benutzerdefinierten Code mehr schreiben, um Filter und Paginierung hinzuzufügen und Beziehungen einzuschließen, die in Beziehungen zu GET-Anforderungen verschachtelt sind. Sie müssen auch keine POST- und PATCH-Anforderungen erstellen, die Änderungen an den Eigenschaften eines Objekts übernehmen und sie zur Aktualisierung an den Server senden. Rufen Sie stattdessen einfach eine Methode für eine ActiveResource auf: Sie müssen nicht mehr mit JSON herumspielen, um die gewünschte Anfrage zu erhalten, nur um es für die nächste erneut tun zu müssen.

Bevor wir anfangen

Es ist wichtig zu beachten, dass zum Zeitpunkt der Erstellung dieses Artikels ActiveResource.js nur mit APIs funktioniert, die gemäß dem JSON:API-Standard geschrieben wurden.

Wenn Sie mit JSON:API nicht vertraut sind und mitmachen möchten, gibt es viele gute Bibliotheken zum Erstellen eines JSON:API-Servers.

Allerdings ist ActiveResource.js eher eine DSL als ein Wrapper für einen bestimmten API-Standard. Die Schnittstelle, die es für die Interaktion mit Ihrer API verwendet, kann erweitert werden, sodass zukünftige Artikel die Verwendung von ActiveResource.js mit Ihrer benutzerdefinierten API behandeln könnten.

Dinge einrichten

Installieren Sie zunächst active-resource in Ihrem Projekt:

 yarn add active-resource

Der erste Schritt besteht darin, eine ResourceLibrary für Ihre API zu erstellen. Ich werde alle meine ActiveResource s im Ordner src/resources ablegen:

 // /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary('http://example.com/api/v1'); export default library;

Der einzige erforderliche Parameter für createResourceLibrary ist die Stamm-URL Ihrer API.

Was wir erstellen werden

Wir werden eine JavaScript-SDK-Bibliothek für eine Content-Management-System-API erstellen. Das bedeutet, dass es Benutzer, Beiträge, Kommentare und Benachrichtigungen geben wird.

Benutzer können Beiträge lesen, erstellen und bearbeiten; Kommentare lesen, hinzufügen und löschen (zu Posts oder anderen Kommentaren) und Benachrichtigungen über neue Posts und Kommentare erhalten.

Ich werde keine bestimmte Bibliothek zum Verwalten der Ansicht (React, Angular usw.) oder des Status (Redux usw.) verwenden, sondern das Tutorial abstrahieren, um nur mit Ihrer API über ActiveResource s zu interagieren.

Die erste Ressource: Benutzer

Wir beginnen mit der Erstellung einer User , um die Benutzer des CMS zu verwalten.

Zuerst erstellen wir eine User mit einigen attributes :

 // /src/resources/User.js import library from './library'; class User extends library.Base { static define() { this.attributes('email', 'userName', 'admin'); } } export default library.createResource(User);

Nehmen wir zunächst an, dass Sie einen Authentifizierungsendpunkt haben, der, sobald ein Benutzer seine E-Mail-Adresse und sein Kennwort übermittelt, ein Zugriffstoken und die ID des Benutzers zurückgibt. Dieser Endpunkt wird von einer Funktion requestToken . Sobald Sie die authentifizierte Benutzer-ID erhalten haben, möchten Sie alle Daten des Benutzers laden:

 import library from '/src/resources/library'; import User from '/src/resources/User'; async function authenticate(email, password) { let [accessToken, userId] = requestToken(email, password); library.headers = { Authorization: 'Bearer ' + accessToken }; return await User.find(userId); }

Ich habe „ library.headers “ so eingestellt, dass es einen „ Authorization “-Header mit accessToken , sodass alle zukünftigen Anfragen meiner „ ResourceLibrary “ autorisiert sind.

In einem späteren Abschnitt wird beschrieben, wie ein Benutzer authentifiziert und das Zugriffstoken nur mithilfe der User festgelegt wird.

Der letzte Schritt der authenticate ist eine Anfrage an User.find(id) . Dadurch wird eine Anfrage an /api/v1/users/:id gestellt, und die Antwort könnte etwa so aussehen:

 { "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false } } }

Die Antwort von authenticate ist eine Instanz der User -Klasse. Von hier aus können Sie auf die verschiedenen Attribute des authentifizierten Benutzers zugreifen, wenn Sie diese irgendwo in der Anwendung anzeigen möchten.

 let user = authenticate(email, password); console.log(user.id) // '1' console.log(user.userName) // user1 console.log(user.email) // [email protected] console.log(user.attributes()) /* { email: '[email protected]', userName: 'user1', admin: false } */

Jeder der Attributnamen wird camelCased, um den typischen JavaScript-Standards zu entsprechen. Sie können jedes von ihnen direkt als Eigenschaften des user abrufen oder alle Attribute abrufen, indem user.attributes() aufrufen.

Hinzufügen eines Ressourcenindex

Bevor wir weitere Ressourcen hinzufügen, die sich auf die User beziehen, wie z. B. Benachrichtigungen, sollten wir eine Datei hinzufügen, src/resources/index.js , die alle unsere Ressourcen indiziert. Dies hat zwei Vorteile:

  1. Es wird unsere Importe bereinigen, indem es uns ermöglicht, src/resources für mehrere Ressourcen in einer Import-Anweisung zu destrukturieren, anstatt mehrere Import-Anweisungen zu verwenden.
  2. Es initialisiert alle Ressourcen in der ResourceLibrary , die wir erstellen, indem wir für jede einzelne library.createResource aufrufen, was für ActiveResource.js erforderlich ist, um Beziehungen aufzubauen.
 // /src/resources/index.js import User from './User'; export { User };

Hinzufügen einer verwandten Ressource

Lassen Sie uns nun eine verwandte Ressource für den User erstellen, eine Notification . Erstellen Sie zuerst eine Notification -Klasse, belongsTo zur User -Klasse gehört:

 // /src/resources/Notification.js import library from './library'; class Notification extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Notification);

Dann fügen wir es dem Ressourcenindex hinzu:

 // /src/resources/index.js import Notification from './Notification'; import User from './User'; export { Notification, User };

Verknüpfen Sie dann die Benachrichtigungen mit der User :

 // /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); } }

Sobald wir den Benutzer von authenticate zurückerhalten haben, können wir alle seine Benachrichtigungen laden und anzeigen:

 let notifications = await user.notifications().load(); console.log(notifications.map(notification => notification.message));

Wir können auch Benachrichtigungen in unsere ursprüngliche Anfrage für den authentifizierten Benutzer aufnehmen:

 async function authenticate(email, password) { /* ... */ return await User.includes('notifications').find(userId); }

Dies ist eine von vielen Optionen, die im DSL verfügbar sind.

Überprüfung der DSL

Lassen Sie uns behandeln, was bereits mit dem Code angefordert werden kann, den wir bisher geschrieben haben.

Sie können eine Sammlung von Benutzern oder einen einzelnen Benutzer abfragen.

 let users = await User.all(); let user = await User.first(); user = await User.last(); user = await User.find('1'); user = await User.findBy({ userName: 'user1' });

Sie können die Abfrage mit verkettbaren relationalen Methoden ändern:

 // Query and iterate over all users User.each((user) => console.log(user)); // Include related resources let users = await User.includes('notifications').all(); // Only respond with user emails as the attributes users = await User.select('email').all(); // Order users by attribute users = await User.order({ email: 'desc' }).all(); // Paginate users let usersPage = await User.page(2).perPage(5).all(); // Filter users by attribute users = await User.where({ admin: true }).all(); users = await User .includes('notifications') .select('email', { notifications: ['message', 'createdAt'] }) .order({ email: 'desc' }) .where({ admin: false }) .perPage(10) .page(3) .all(); let user = await User .includes('notification') .select('email') .first();

Beachten Sie, dass Sie die Abfrage mit beliebig vielen verketteten Modifikatoren zusammenstellen können und dass Sie die Abfrage mit .all() , .first() , .last() oder .each() .

Sie können einen Benutzer lokal oder auf dem Server erstellen:

 let user = User.build(attributes); user = await User.create(attributes);

Sobald Sie einen dauerhaften Benutzer haben, können Sie Änderungen an ihn senden, damit sie auf dem Server gespeichert werden:

 user.email = '[email protected]'; await user.save(); /* or */ await user.update({ email: '[email protected]' });

Sie können es auch vom Server löschen:

 await user.destroy();

Diese grundlegende DSL erstreckt sich auch auf verwandte Ressourcen, wie ich im weiteren Verlauf des Tutorials demonstrieren werde. Jetzt können wir ActiveResource.js schnell anwenden, um den Rest des CMS zu erstellen: Beiträge und Kommentare.

Beiträge erstellen

Erstellen Sie eine Ressourcenklasse für Post und ordnen Sie sie der User -Klasse zu:

 // /src/resources/Post.js import library from './library'; class Post extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Post);
 // /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); this.hasMany('posts'); } }

Post auch zum Ressourcenindex hinzufügen:

 // /src/resources/index.js import Notification from './Notification'; import Post from './Post'; import User from './User'; export { Notification, Post, User };

Binden Sie dann die Post -Ressource in ein Formular ein, damit Benutzer Posts erstellen und bearbeiten können. Wenn der Benutzer das Formular zum ersten Mal besucht, um einen neuen Post zu erstellen, wird eine Beitragsressource erstellt, und jedes Mal, wenn das Formular geändert wird, wenden wir die Änderung auf den Post an:

 import Post from '/src/resources/Post'; let post = Post.build({ user: authenticatedUser }); onChange = (event) => { post.content = event.target.value; };

Fügen Sie als Nächstes einen onSubmit -Callback zum Formular hinzu, um den Beitrag auf dem Server zu speichern, und behandeln Sie Fehler, wenn der Speicherversuch fehlschlägt:

 onSubmit = async () => { try { await post.save(); /* successful, redirect to edit post form */ } catch { post.errors().each((field, error) => { console.log(field, error.message) }); } }

Beiträge bearbeiten

Sobald der Beitrag gespeichert wurde, wird er als Ressource auf Ihrem Server mit Ihrer API verknüpft. Sie können feststellen, ob eine Ressource auf dem Server persistent ist, indem Sie persisted aufrufen:

 if (post.persisted()) { /* post is on server */ }

Für persistente Ressourcen unterstützt ActiveResource.js Dirty-Attribute, da Sie überprüfen können, ob der Wert eines Attributs einer Ressource auf dem Server geändert wurde.

Wenn Sie save() für eine persistente Ressource aufrufen, wird eine PATCH Anforderung erstellt, die nur die an der Ressource vorgenommenen Änderungen enthält, anstatt den gesamten Satz von Attributen und Beziehungen der Ressource unnötigerweise an den Server zu senden.

Mithilfe der attributes können Sie nachverfolgte Attribute zu einer Ressource hinzufügen. Lassen Sie uns Änderungen an post.content :

 // /src/resources/Post.js class Post extends library.Base { static define() { this.attributes('content'); /* ... */ } }

Jetzt können wir mit einem Server-persistenten Beitrag den Beitrag bearbeiten und die Änderungen auf dem Server speichern, wenn auf die Schaltfläche „Senden“ geklickt wird. Wir können den Submit-Button auch deaktivieren, wenn noch keine Änderungen vorgenommen wurden:

 onEdit = (event) => { post.content = event.target.value; } onSubmit = async () => { try { await post.save(); } catch { /* display edit errors */ } } disableSubmitButton = () => { return !post.changed(); }

Es gibt Methoden zum Verwalten einer einzelnen Beziehung wie post.user() , wenn wir den mit einem Beitrag verknüpften Benutzer ändern möchten:

 await post.updateUser(user);

Dies ist äquivalent zu:

 await post.update({ user });

Die Kommentar-Ressource

Erstellen Sie nun eine Ressourcenklasse Comment und verknüpfen Sie sie mit Post . Denken Sie an unsere Anforderung, dass Kommentare eine Antwort auf einen Beitrag oder einen anderen Kommentar sein können, sodass die relevante Ressource für einen Kommentar polymorph ist:

 // /src/resources/Comment.js import library from './library'; class Comment extends library.Base { static define() { this.attributes('content'); this.belongsTo('resource', { polymorphic: true, inverseOf: 'replies' }); this.belongsTo('user'); this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } } export default library.createResource(Comment);

Stellen Sie sicher, dass Sie auch Comment zu /src/resources/index.js hinzufügen.

Wir müssen auch der Post -Klasse eine Zeile hinzufügen:

 // /src/resources/Post.js class Post extends library.Base { static define() { /* ... */ this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } }

Die inverseOf Option, die an die hasMany Definition für replies übergeben wird, gibt an, dass diese Beziehung die Umkehrung der Polymorphie belongsTo Definition für resource . Die inverseOf Eigenschaft von Beziehungen wird häufig verwendet, wenn Operationen zwischen Beziehungen durchgeführt werden. Normalerweise wird diese Eigenschaft automatisch über den Klassennamen bestimmt, aber da polymorphe Beziehungen eine von mehreren Klassen sein können, müssen Sie die Option inverseOf selbst definieren, damit polymorphe Beziehungen dieselbe Funktionalität wie normale haben.

Kommentare zu Beiträgen verwalten

Dieselbe DSL, die für Ressourcen gilt, gilt auch für die Verwaltung verwandter Ressourcen. Nachdem wir nun die Beziehungen zwischen Posts und Kommentaren eingerichtet haben, gibt es eine Reihe von Möglichkeiten, wie wir diese Beziehung verwalten können.

Sie können einem Beitrag einen neuen Kommentar hinzufügen:

 onSubmitComment = async (event) => { let comment = await post.replies().create({ content: event.target.value, user: user }); }

Sie können eine Antwort auf einen Kommentar hinzufügen:

 onSubmitReply = async (event) => { let reply = await comment.replies().create({ content: event.target.value, user: user }); }

Sie können einen Kommentar bearbeiten:

 onEditComment = async (event) => { await comment.update({ content: event.target.value }); }

Sie können einen Kommentar aus einem Beitrag entfernen:

 onDeleteComment = async (comment) => { await post.replies().delete(comment); }

Beiträge und Kommentare anzeigen

Das SDK kann verwendet werden, um eine paginierte Liste von Posts anzuzeigen, und wenn auf einen Post geklickt wird, wird der Post mit all seinen Kommentaren auf einer neuen Seite geladen:

 import { Post } from '/src/resources'; let postsPage = await Post .order({ createdAt: 'desc' }) .select('content') .perPage(10) .all();

Die obige Abfrage ruft die 10 neuesten Posts ab, und zur Optimierung ist das einzige Attribut, das geladen wird, ihr content .

Wenn ein Benutzer auf eine Schaltfläche klickt, um zur nächsten Seite mit Beiträgen zu wechseln, ruft ein Change-Handler die nächste Seite ab. Hier deaktivieren wir auch die Schaltfläche, wenn keine nächsten Seiten vorhanden sind.

 onClickNextPage = async () => { postsPage = await postsPage.nextPage(); if (!postsPage.hasNextPage()) { /* disable next page button */ } };

Wenn auf einen Link zu einem Beitrag geklickt wird, öffnen wir eine neue Seite, indem wir den Beitrag mit all seinen Daten laden und anzeigen, einschließlich seiner Kommentare – bekannt als Antworten – sowie Antworten auf diese Antworten:

 import { Post } from '/src/resources'; onClick = async (postId) => { let post = await Post.includes({ replies: 'replies' }).find(postId); console.log(post.content, post.createdAt); post.replies().target().each(comment => { console.log( comment.content, comment.replies.target().map(reply => reply.content).toArray() ); }); }

Der Aufruf .target() für eine hasMany Beziehung wie post.replies() gibt eine ActiveResource.Collection von Kommentaren zurück, die geladen und lokal gespeichert wurden.

Diese Unterscheidung ist wichtig, da post.replies().target().first() den ersten geladenen Kommentar zurückgibt. Im Gegensatz dazu gibt post.replies().first() ein Versprechen für einen von GET /api/v1/posts/:id/replies replies angeforderten Kommentar zurück.

Sie können die Antworten für einen Beitrag auch getrennt von der Anfrage für den Beitrag selbst anfordern, wodurch Sie Ihre Anfrage ändern können. Sie können Modifikatoren wie order , select , includes , where , perPage , page verketten, wenn Sie hasMany Beziehungen abfragen, genau wie Sie es bei der Abfrage von Ressourcen selbst können.

 import { Post } from '/src/resources'; onClick = async (postId) => { let post = await Post.find(postId); let userComments = await post.replies().where({ user: user }).perPage(3).all(); console.log('Your comments:', userComments.map(comment => comment.content).toArray()); }

Ändern von Ressourcen, nachdem sie angefordert wurden

Manchmal möchten Sie die Daten vom Server nehmen und ändern, bevor Sie sie verwenden. Beispielsweise könnten Sie post.createdAt in ein moment() Objekt einschließen, sodass Sie dem Benutzer eine benutzerfreundliche datetime anzeigen können, wann der Beitrag erstellt wurde:

 // /src/resources/Post.js import moment from 'moment'; class Post extends library.Base { static define() { /* ... */ this.afterRequest(function() { this.createdAt = moment(this.createdAt); }); } }

Unveränderlichkeit

Wenn Sie mit einem Zustandsverwaltungssystem arbeiten, das unveränderliche Objekte bevorzugt, kann das gesamte Verhalten in ActiveResource.js unveränderlich gemacht werden, indem Sie die Ressourcenbibliothek konfigurieren:

 // /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary( 'http://example.com/api/v1', { immutable: true } ); export default library;

Circling Back: Verknüpfung des Authentifizierungssystems

Zum Abschluss zeige ich Ihnen, wie Sie Ihr Benutzerauthentifizierungssystem in Ihre User ActiveResource integrieren.

Verschieben Sie Ihr Token-Authentifizierungssystem zum API-Endpunkt /api/v1/tokens . Wenn die E-Mail-Adresse und das Passwort eines Benutzers an diesen Endpunkt gesendet werden, werden die Daten des authentifizierten Benutzers plus das Autorisierungs-Token als Antwort gesendet.

Erstellen Sie eine Token -Ressourcenklasse, die zu User gehört:

 // /src/resources/Token.js import library from './library'; class Token extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Token);

Token zu /src/resources/index.js .

Fügen Sie dann eine statische Methode authenticate zu Ihrer User hinzu und verknüpfen Sie User mit Token :

 // /src/resources/User.js import library from './library'; import Token from './Token'; class User { static define() { /* ... */ this.hasOne('token'); } static async authenticate(email, password) { let user = this.includes('token').build({ email, password }); let authUser = await this.interface().post(Token.links().related, user); let token = authUser.token(); library.headers = { Authorization: 'Bearer ' + token.id }; return authUser; } }

Diese Methode verwendet resourceLibrary.interface() , in diesem Fall die JSON:API-Schnittstelle, um einen Benutzer an /api/v1/tokens zu senden. Dies ist gültig: Ein Endpunkt in JSON:API erfordert nicht, dass die einzigen Typen, die an und von ihm gesendet werden, diejenigen sind, nach denen er benannt ist. Die Anfrage wird also lauten:

 { "data": { "type": "users", "attributes": { "email": "[email protected]", "password": "password" } } }

Die Antwort ist der authentifizierte Benutzer mit dem darin enthaltenen Authentifizierungstoken:

 { "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false }, "relationships": { "token": { "data": { "type": "tokens", "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX", } } } }, "included": [{ "type": "tokens", "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX", "attributes": { "expires_in": 3600 } }] }

Dann verwenden wir die token.id , um den Authorization -Header unserer Bibliothek festzulegen, und geben den Benutzer zurück, was dasselbe ist, als ob wir den Benutzer wie zuvor über User.find() anfordern würden.

Wenn Sie nun User.authenticate(email, password) aufrufen, erhalten Sie als Antwort einen authentifizierten Benutzer, und alle zukünftigen Anfragen werden mit einem Zugriffstoken autorisiert.

ActiveResource.js ermöglicht die schnelle Entwicklung von JavaScript-SDKs

In diesem Tutorial haben wir untersucht, wie ActiveResource.js Ihnen helfen kann, schnell ein JavaScript-SDK zu erstellen, um Ihre API-Ressourcen und ihre verschiedenen, manchmal komplizierten, zugehörigen Ressourcen zu verwalten. Sie können alle diese Funktionen und mehr in der README-Datei für ActiveResource.js sehen.

Ich hoffe, Sie haben die Leichtigkeit genossen, mit der diese Operationen durchgeführt werden können, und dass Sie meine Bibliothek für Ihre zukünftigen Projekte verwenden (und vielleicht sogar dazu beitragen) werden, wenn es Ihren Bedürfnissen entspricht. Im Geiste von Open Source sind PRs immer willkommen!