ActiveResource.js: creazione rapida di un potente SDK JavaScript per la tua API JSON
Pubblicato: 2022-03-11La tua azienda ha appena lanciato la sua API e ora vuole creare una comunità di utenti attorno ad essa. Sai che la maggior parte dei tuoi clienti lavorerà in JavaScript, perché i servizi forniti dalla tua API rendono più facile per i clienti creare applicazioni Web invece di scrivere tutto da soli: Twilio ne è un buon esempio.
Sai anche che, per quanto semplice possa essere la tua API RESTful, gli utenti vorranno inserire un pacchetto JavaScript che farà tutto il lavoro pesante per loro. Non vorranno imparare la tua API e costruire da soli ogni richiesta di cui hanno bisogno.
Quindi stai costruendo una libreria attorno alla tua API. O forse stai solo scrivendo un sistema di gestione dello stato per un'applicazione web che interagisce con la tua API interna.
Ad ogni modo, non vuoi ripeterti più e più volte ogni volta che crei CRUD una delle tue risorse API o, peggio, CRUD una risorsa correlata a quelle risorse. Questo non è utile per gestire un SDK in crescita a lungo termine, né è un buon uso del tuo tempo.
Invece, puoi utilizzare ActiveResource.js, un sistema ORM JavaScript per interagire con le API. L'ho creato per soddisfare un'esigenza che avevamo su un progetto: creare un SDK JavaScript nel minor numero di righe possibile. Ciò ha consentito la massima efficienza per noi e per la nostra comunità di sviluppatori.
Si basa sui principi alla base del semplice ActiveRecord ORM di Ruby on Rails.
Principi dell'SDK JavaScript
Ci sono due idee di Ruby on Rails che hanno guidato la progettazione di ActiveResource.js:
- "Convenzione sulla configurazione:" Fare alcune ipotesi sulla natura degli endpoint dell'API. Ad esempio, se si dispone di una risorsa
Product
, corrisponde all'endpoint/products
. In questo modo il tempo non viene speso ripetutamente per configurare ciascuna delle richieste dell'SDK alla tua API. Gli sviluppatori possono aggiungere nuove risorse API con complicate query CRUD al tuo SDK in crescita in pochi minuti, non ore. - "Esalta il bellissimo codice:" il creatore di Rails DHH lo ha detto meglio: c'è qualcosa di eccezionale nel bellissimo codice fine a se stesso. ActiveResource.js avvolge le richieste a volte brutte in un bellissimo esterno. Non è più necessario scrivere codice personalizzato per aggiungere filtri e impaginazione e includere relazioni nidificate su relazioni per ottenere richieste. Né è necessario creare richieste POST e PATCH che prendono le modifiche alle proprietà di un oggetto e le inviano al server per l'aggiornamento. Invece, chiama semplicemente un metodo su un ActiveResource: non dovrai più giocare con JSON per ottenere la richiesta che desideri, solo per doverlo ripetere per il prossimo.
Prima di iniziare
È importante notare che al momento della stesura di questo documento, ActiveResource.js funziona solo con API scritte secondo lo standard JSON:API.
Se non hai familiarità con JSON:API e vuoi seguire, ci sono molte buone librerie per la creazione di un server JSON:API.
Detto questo, ActiveResource.js è più un DSL che un wrapper per un particolare standard API. L'interfaccia che utilizza per interagire con l'API può essere estesa, quindi gli articoli futuri potrebbero illustrare come utilizzare ActiveResource.js con l'API personalizzata.
Sistemare le cose
Per iniziare, installa active-resource
nel tuo progetto:
yarn add active-resource
Il primo passaggio consiste nel creare una ResourceLibrary
per la tua API. Metterò tutti i miei ActiveResource
nella cartella src/resources
:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary('http://example.com/api/v1'); export default library;
L'unico parametro richiesto per createResourceLibrary
è l'URL radice della tua API.
Cosa creeremo
Creeremo una libreria SDK JavaScript per un'API del sistema di gestione dei contenuti. Ciò significa che ci saranno utenti, post, commenti e notifiche.
Gli utenti potranno leggere, creare e modificare i post; leggere, aggiungere ed eliminare commenti (ai post o ad altri commenti) e ricevere notifiche di nuovi post e commenti.
Non utilizzerò alcuna libreria specifica per la gestione della vista (React, Angular, ecc.) o dello stato (Redux, ecc.), Astraendo invece il tutorial per interagire solo con la tua API tramite ActiveResource
s.
La prima risorsa: gli utenti
Inizieremo creando una risorsa User
per gestire gli utenti del CMS.
Innanzitutto, creiamo una classe di risorse User
con alcuni 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);
Supponiamo per ora di avere un endpoint di autenticazione che, una volta che un utente ha inviato la propria email e password, restituisce un token di accesso e l'ID dell'utente. Questo endpoint è gestito da una funzione requestToken
. Una volta ottenuto l'ID utente autenticato, si desidera caricare tutti i dati dell'utente:
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); }
Ho impostato library.headers
per avere un'intestazione di Authorization
con accessToken
in modo che tutte le richieste future della mia ResourceLibrary
siano autorizzate.
Una sezione successiva tratterà come autenticare un utente e impostare il token di accesso utilizzando solo la classe di risorse User
.
L'ultimo passaggio authenticate
è una richiesta a User.find(id)
. Questo farà una richiesta a /api/v1/users/:id
e la risposta potrebbe assomigliare a:
{ "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false } } }
La risposta di authenticate
sarà un'istanza della classe User
. Da qui è possibile accedere ai vari attributi dell'utente autenticato, se si desidera visualizzarli da qualche parte nell'applicazione.
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 } */
Ciascuno dei nomi degli attributi diventerà camelCased, per adattarsi agli standard tipici di JavaScript. Puoi ottenerli direttamente come proprietà dell'oggetto user
o ottenere tutti gli attributi chiamando user.attributes()
.
Aggiunta di un indice di risorsa
Prima di aggiungere altre risorse relative alla classe User
, come le notifiche, dovremmo aggiungere un file, src/resources/index.js
, che indicizzerà tutte le nostre risorse. Questo ha due vantaggi:
- Pulirà le nostre importazioni consentendoci di destrutturare
src/resources
per più risorse in un'unica istruzione di importazione invece di utilizzare più istruzioni di importazione. - Inizializzerà tutte le risorse sulla
ResourceLibrary
che creeremo chiamandolibrary.createResource
su ciascuna, che è necessaria per ActiveResource.js per creare relazioni.
// /src/resources/index.js import User from './User'; export { User };
Aggiunta di una risorsa correlata
Ora creiamo una risorsa correlata per l' User
, una Notification
. Per prima cosa crea una classe di Notification
che belongsTo
alla classe User
:
// /src/resources/Notification.js import library from './library'; class Notification extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Notification);
Quindi lo aggiungiamo all'indice delle risorse:
// /src/resources/index.js import Notification from './Notification'; import User from './User'; export { Notification, User };
Quindi, correla le notifiche alla classe User
:
// /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); } }
Ora, una volta ripristinato l'utente da authenticate
, possiamo caricare e visualizzare tutte le sue notifiche:
let notifications = await user.notifications().load(); console.log(notifications.map(notification => notification.message));
Possiamo anche includere notifiche nella nostra richiesta originale per l'utente autenticato:
async function authenticate(email, password) { /* ... */ return await User.includes('notifications').find(userId); }
Questa è una delle tante opzioni disponibili nella DSL.
Revisione della DSL
Copriamo ciò che è già possibile richiedere solo dal codice che abbiamo scritto finora.
È possibile eseguire query su una raccolta di utenti o su un singolo utente.
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' });
È possibile modificare la query utilizzando metodi relazionali concatenabili:
// 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();
Nota che puoi comporre la query utilizzando qualsiasi quantità di modificatori concatenati e che puoi terminare la query con .all()
() , .first( .first()
, .last()
o .each()
.
Puoi creare un utente localmente o crearne uno sul server:
let user = User.build(attributes); user = await User.create(attributes);
Una volta che hai un utente persistente, puoi inviargli le modifiche da salvare sul server:
user.email = '[email protected]'; await user.save(); /* or */ await user.update({ email: '[email protected]' });
Puoi anche eliminarlo dal server:
await user.destroy();
Questo DSL di base si estende anche alle risorse correlate, come dimostrerò nel resto del tutorial. Ora possiamo applicare rapidamente ActiveResource.js alla creazione del resto del CMS: post e commenti.
Creazione di post
Crea una classe risorsa per Post
e associala alla classe User
:
// /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'); } }
Aggiungi anche il Post
all'indice delle risorse:
// /src/resources/index.js import Notification from './Notification'; import Post from './Post'; import User from './User'; export { Notification, Post, User };
Quindi collega la risorsa Post
in un modulo per consentire agli utenti di creare e modificare post. Quando l'utente visita per la prima volta il modulo per la creazione di un nuovo post, verrà creata una risorsa Post
e ogni volta che il modulo viene modificato, applichiamo la modifica al Post
:

import Post from '/src/resources/Post'; let post = Post.build({ user: authenticatedUser }); onChange = (event) => { post.content = event.target.value; };
Quindi, aggiungi un callback onSubmit
al modulo per salvare il post sul server e gestisci gli errori se il tentativo di salvataggio fallisce:
onSubmit = async () => { try { await post.save(); /* successful, redirect to edit post form */ } catch { post.errors().each((field, error) => { console.log(field, error.message) }); } }
Modifica dei post
Una volta che il post è stato salvato, sarà collegato alla tua API come risorsa sul tuo server. Puoi sapere se una risorsa è persistente sul server chiamando persisted
:
if (post.persisted()) { /* post is on server */ }
Per le risorse persistenti, ActiveResource.js supporta gli attributi sporchi, in quanto è possibile verificare se qualsiasi attributo di una risorsa è stato modificato rispetto al suo valore sul server.
Se chiami save()
su una risorsa persistente, farà una richiesta PATCH
contenente solo le modifiche apportate alla risorsa, invece di inviare l'intero insieme di attributi e relazioni della risorsa al server inutilmente.
È possibile aggiungere attributi tracciati a una risorsa utilizzando la dichiarazione degli attributes
. Monitoriamo le modifiche a post.content
:
// /src/resources/Post.js class Post extends library.Base { static define() { this.attributes('content'); /* ... */ } }
Ora, con un post persistente sul server, possiamo modificare il post e, quando si fa clic sul pulsante di invio, salvare le modifiche sul server. Possiamo anche disabilitare il pulsante di invio se non sono state ancora apportate modifiche:
onEdit = (event) => { post.content = event.target.value; } onSubmit = async () => { try { await post.save(); } catch { /* display edit errors */ } } disableSubmitButton = () => { return !post.changed(); }
Esistono metodi per gestire una relazione singolare come post.user()
, se volessimo cambiare l'utente associato a un post:
await post.updateUser(user);
Ciò equivale a:
await post.update({ user });
La risorsa per i commenti
Ora crea una classe di risorse Comment
e collegala a Post
. Ricorda il nostro requisito secondo cui i commenti possono essere in risposta a un post o a un altro commento, quindi la risorsa pertinente per un commento è polimorfica:
// /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);
Assicurati di aggiungere anche il Comment
a /src/resources/index.js
.
Dovremo anche aggiungere una riga alla classe Post
:
// /src/resources/Post.js class Post extends library.Base { static define() { /* ... */ this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } }
L'opzione inverseOf
passata alla definizione hasMany
per le replies
indica che tale relazione è l'inverso della definizione polimorfica di belongsTo
per resource
. La proprietà inverseOf
delle relazioni viene utilizzata frequentemente quando si eseguono operazioni tra relazioni. In genere, questa proprietà viene determinata automaticamente tramite il nome della classe, ma poiché le relazioni polimorfiche possono essere una di più classi, è necessario definire l'opzione inverseOf
per fare in modo che le relazioni polimorfiche abbiano tutte le stesse funzionalità di quelle normali.
Gestione dei commenti sui post
La stessa DSL che si applica alle risorse vale anche per la gestione delle relative risorse. Ora che abbiamo impostato le relazioni tra post e commenti, ci sono diversi modi in cui possiamo gestire questa relazione.
Puoi aggiungere un nuovo commento a un post:
onSubmitComment = async (event) => { let comment = await post.replies().create({ content: event.target.value, user: user }); }
Puoi aggiungere una risposta a un commento:
onSubmitReply = async (event) => { let reply = await comment.replies().create({ content: event.target.value, user: user }); }
Puoi modificare un commento:
onEditComment = async (event) => { await comment.update({ content: event.target.value }); }
Puoi rimuovere un commento da un post:
onDeleteComment = async (comment) => { await post.replies().delete(comment); }
Visualizzazione di post e commenti
L'SDK può essere utilizzato per visualizzare un elenco impaginato di post e, quando si fa clic su un post, il post viene caricato su una nuova pagina con tutti i suoi commenti:
import { Post } from '/src/resources'; let postsPage = await Post .order({ createdAt: 'desc' }) .select('content') .perPage(10) .all();
La query precedente recupererà i 10 post più recenti e, per ottimizzare, l'unico attributo caricato è il loro content
.
Se un utente fa clic su un pulsante per passare alla pagina successiva dei post, un gestore delle modifiche recupererà la pagina successiva. Qui disabilitiamo anche il pulsante se non ci sono pagine successive.
onClickNextPage = async () => { postsPage = await postsPage.nextPage(); if (!postsPage.hasNextPage()) { /* disable next page button */ } };
Quando si fa clic su un collegamento a un post, apriamo una nuova pagina caricando e visualizzando il post con tutti i suoi dati, inclusi i commenti, noti come risposte, e le risposte a tali risposte:
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() ); }); }
La chiamata .target()
su una relazione hasMany
come post.replies()
restituirà un ActiveResource.Collection
di commenti che sono stati caricati e archiviati localmente.
Questa distinzione è importante, perché post.replies().target().first()
restituirà il primo commento caricato. Al contrario, post.replies().first()
restituirà una promessa per un commento richiesto da GET /api/v1/posts/:id/replies
.
Potresti anche richiedere le risposte per un post separatamente dalla richiesta per il post stesso, il che ti consente di modificare la tua query. Puoi concatenare modificatori come order
, select
, includes
, where
, perPage
, page
quando esegui query hasMany
relazioni proprio come puoi quando esegui query sulle risorse stesse.
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()); }
Modifica delle risorse dopo che sono state richieste
A volte si desidera prelevare i dati dal server e modificarli prima di utilizzarli. Ad esempio, puoi avvolgere post.createdAt
in un oggetto moment()
in modo da poter visualizzare un datetime intuitivo per l'utente su quando è stato creato il post:
// /src/resources/Post.js import moment from 'moment'; class Post extends library.Base { static define() { /* ... */ this.afterRequest(function() { this.createdAt = moment(this.createdAt); }); } }
Immutabilità
Se lavori con un sistema di gestione dello stato che favorisce gli oggetti immutabili, tutto il comportamento in ActiveResource.js può essere reso immutabile configurando la libreria di risorse:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary( 'http://example.com/api/v1', { immutable: true } ); export default library;
Tornando indietro: collegamento del sistema di autenticazione
Per concludere, ti mostrerò come integrare il tuo sistema di autenticazione utente nel tuo User
ActiveResource
.
Sposta il tuo sistema di autenticazione del token sull'endpoint API /api/v1/tokens
. Quando l'e-mail e la password di un utente vengono inviate a questo endpoint, i dati dell'utente autenticato più il token di autorizzazione verranno inviati in risposta.
Crea una classe di risorse Token
che appartiene a User
:
// /src/resources/Token.js import library from './library'; class Token extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Token);
Aggiungi Token
a /src/resources/index.js
.
Quindi, aggiungi un metodo statico per authenticate
alla tua classe di risorse User
e correla User
a 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; } }
Questo metodo utilizza resourceLibrary.interface()
, che in questo caso è l'interfaccia JSON:API, per inviare un utente a /api/v1/tokens
. Ciò è valido: un endpoint in JSON:API non richiede che gli unici tipi inviati e inviati siano quelli da cui prende il nome. Quindi la richiesta sarà:
{ "data": { "type": "users", "attributes": { "email": "[email protected]", "password": "password" } } }
La risposta sarà l'utente autenticato con il token di autenticazione incluso:
{ "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 } }] }
Quindi utilizziamo token.id
per impostare l'intestazione di Authorization
della nostra libreria e restituire l'utente, che è lo stesso che richiede all'utente tramite User.find()
come facevamo prima.
Ora, se chiami User.authenticate(email, password)
, riceverai in risposta un utente autenticato e tutte le richieste future saranno autorizzate con un token di accesso.
ActiveResource.js consente lo sviluppo rapido dell'SDK JavaScript
In questo tutorial, abbiamo esplorato i modi in cui ActiveResource.js può aiutarti a creare rapidamente un SDK JavaScript per gestire le tue risorse API e le loro varie, a volte complicate, risorse correlate. Puoi vedere tutte queste funzionalità e altre ancora documentate nel README per ActiveResource.js.
Spero che ti sia piaciuta la facilità con cui queste operazioni possono essere eseguite e che utilizzerai (e forse anche contribuirai) alla mia libreria per i tuoi progetti futuri, se si adatta alle tue esigenze. Nello spirito dell'open source, i PR sono sempre i benvenuti!