ActiveResource.js: Construirea unui SDK JavaScript puternic pentru API-ul dvs. JSON, rapid

Publicat: 2022-03-11

Compania ta tocmai și-a lansat API-ul și acum vrea să creeze o comunitate de utilizatori în jurul acesteia. Știți că majoritatea clienților dvs. vor lucra în JavaScript, deoarece serviciile oferite de API-ul dvs. le facilitează clienților să construiască aplicații web în loc să scrie totul ei înșiși — Twilio este un bun exemplu în acest sens.

De asemenea, știți că, oricât de simplu ar fi API-ul dvs. RESTful, utilizatorii vor dori să introducă un pachet JavaScript care va face toată munca grea pentru ei. Nu vor dori să învețe API-ul dvs. și să construiască fiecare cerere de care au nevoie.

Deci construiești o bibliotecă în jurul API-ului tău. Sau poate doar scrieți un sistem de management de stat pentru o aplicație web care interacționează cu propriul API intern.

Oricum, nu doriți să vă repetați de fiecare dată când CRUDați una dintre resursele dvs. API sau, mai rău, CRUD-ul unei resurse legate de acele resurse. Acest lucru nu este bun pentru gestionarea unui SDK în creștere pe termen lung și nici nu este o utilizare bună a timpului dvs.

În schimb, puteți utiliza ActiveResource.js, un sistem ORM JavaScript pentru interacțiunea cu API-urile. L-am creat pentru a satisface o nevoie pe care o aveam într-un proiect: să creăm un SDK JavaScript în cât mai puține rânduri. Acest lucru a permis eficiență maximă pentru noi și pentru comunitatea noastră de dezvoltatori.

Se bazează pe principiile din spatele ActiveRecord ORM simplu de la Ruby on Rails.

Principii JavaScript SDK

Există două idei Ruby on Rails care au ghidat proiectarea ActiveResource.js:

  1. „Convenție peste configurare:” faceți câteva ipoteze despre natura punctelor finale ale API. De exemplu, dacă aveți o resursă Product , aceasta corespunde punctului final /products . În acest fel, timpul nu este cheltuit în mod repetat configurând fiecare dintre solicitările SDK-ului dvs. la API. Dezvoltatorii pot adăuga noi resurse API cu interogări CRUD complicate la SDK-ul dvs. în creștere în câteva minute, nu în ore.
  2. „Exaltă codul frumos:” Creatorul Rails, DHH, a spus-o cel mai bine – există doar ceva grozav în codul frumos de dragul său. ActiveResource.js împachetează cereri uneori urâte într-un exterior frumos. Nu mai trebuie să scrieți cod personalizat pentru a adăuga filtre și paginare și pentru a include relații imbricate în relațiile cu cererile GET. Nici nu trebuie să construiți cereri POST și PATCH care preiau modificări la proprietățile unui obiect și le trimit la server pentru actualizare. În schimb, apelați doar o metodă pe o resurse Active: nu mai jucați cu JSON pentru a obține cererea pe care o doriți, doar pentru a trebui să o faceți din nou pentru următoarea.

Înainte de a începe

Este important de reținut că la momentul scrierii acestui articol, ActiveResource.js funcționează numai cu API-uri scrise conform standardului JSON:API.

Dacă nu sunteți familiarizat cu JSON:API și doriți să urmați, există multe biblioteci bune pentru a crea un server JSON:API.

Acestea fiind spuse, ActiveResource.js este mai mult un DSL decât un wrapper pentru un anumit standard API. Interfața pe care o folosește pentru a interacționa cu API-ul dvs. poate fi extinsă, astfel încât articolele viitoare ar putea acoperi cum să utilizați ActiveResource.js cu API-ul dvs. personalizat.

Configurarea lucrurilor

Pentru a începe, instalați active-resource în proiectul dvs.:

 yarn add active-resource

Primul pas este să creați o bibliotecă de ResourceLibrary pentru API-ul dvs. Voi pune toate ActiveResource -urile mele în folderul src/resources :

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

Singurul parametru necesar pentru a createResourceLibrary este adresa URL rădăcină a API-ului dvs.

Ce vom crea

Vom crea o bibliotecă JavaScript SDK pentru un sistem API de management al conținutului. Asta înseamnă că vor exista utilizatori, postări, comentarii și notificări.

Utilizatorii vor putea citi, crea și edita postări; citiți, adăugați și ștergeți comentarii (la postări sau la alte comentarii) și primiți notificări cu privire la postări și comentarii noi.

Nu voi folosi nicio bibliotecă specifică pentru gestionarea vizualizării (React, Angular, etc.) sau a stării (Redux, etc.), în schimb abstragând tutorialul pentru a interacționa numai cu API-ul dvs. prin ActiveResource s.

Prima resursă: utilizatorii

Vom începe prin a crea o resursă de User pentru a gestiona utilizatorii CMS.

Mai întâi, creăm o clasă de resurse User cu câteva 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);

Să presupunem deocamdată că aveți un punct final de autentificare care, odată ce un utilizator își trimite e-mailul și parola, returnează un token de acces și ID-ul utilizatorului. Acest punct final este gestionat de o funcție requestToken . După ce obțineți ID-ul de utilizator autentificat, doriți să încărcați toate datele utilizatorului:

 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); }

Am setat library.headers să aibă un antet de Authorization cu accessToken , astfel încât toate solicitările viitoare ale ResourceLibrary să fie autorizate.

O secțiune ulterioară va trata cum să autentificați un utilizator și să setați jetonul de acces folosind doar clasa de resurse User .

Ultimul pas de authenticate este o solicitare către User.find(id) . Aceasta va face o solicitare către /api/v1/users/:id , iar răspunsul ar putea arăta ceva de genul:

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

Răspunsul de la authenticate va fi o instanță a clasei User . De aici, puteți accesa diversele atribute ale utilizatorului autentificat, dacă doriți să le afișați undeva în aplicație.

 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 } */

Fiecare dintre numele atributelor va deveni camelCased, pentru a se potrivi cu standardele tipice JavaScript. Puteți obține fiecare dintre ele direct ca proprietăți ale obiectului user sau puteți obține toate atributele apelând user.attributes() .

Adăugarea unui index de resurse

Înainte de a adăuga mai multe resurse care se referă la clasa User , cum ar fi notificările, ar trebui să adăugăm un fișier, src/resources/index.js , care va indexa toate resursele noastre. Aceasta are două beneficii:

  1. Ne va curăța importurile, permițându-ne să destructuram src/resources pentru mai multe resurse într-o singură instrucțiune de import în loc să folosim mai multe instrucțiuni de import.
  2. Va inițializa toate resursele din ResourceLibrary pe care le vom crea apelând library.createResource pe fiecare, ceea ce este necesar pentru ca ActiveResource.js să construiască relații.
 // /src/resources/index.js import User from './User'; export { User };

Adăugarea unei resurse conexe

Acum să creăm o resursă asociată pentru User , o Notification . Mai întâi creați o clasă de Notification care aparține belongsTo de User :

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

Apoi îl adăugăm la indexul de resurse:

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

Apoi, raportați notificările la clasa de User :

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

Acum, odată ce recuperăm utilizatorul de la authenticate , putem încărca și afișa toate notificările sale:

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

De asemenea, putem include notificări în cererea noastră inițială pentru utilizatorul autentificat:

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

Aceasta este una dintre multele opțiuni disponibile în DSL.

Revizuirea DSL-ului

Să acoperim ceea ce este deja posibil de solicitat doar din codul pe care l-am scris până acum.

Puteți interoga o colecție de utilizatori sau un singur utilizator.

 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' });

Puteți modifica interogarea folosind metode relaționale înlănțuite:

 // 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();

Observați că puteți compune interogarea folosind orice număr de modificatori înlănțuiți și că puteți încheia interogarea cu .all( .all() , .first() , .last() sau .each() .

Puteți crea un utilizator local sau puteți crea unul pe server:

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

Odată ce aveți un utilizator persistent, puteți trimite modificări la acesta pentru a fi salvate pe server:

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

De asemenea, îl puteți șterge de pe server:

 await user.destroy();

Acest DSL de bază se extinde și la resursele conexe, așa cum voi demonstra în restul tutorialului. Acum putem aplica rapid ActiveResource.js pentru a crea restul CMS-ului: postări și comentarii.

Crearea de postări

Creați o clasă de resurse pentru Post și asociați-o cu clasa 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'); } }

Adăugați Post și la indexul de resurse:

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

Apoi legați resursa Post într-un formular pentru ca utilizatorii să creeze și să editeze postări. Când utilizatorul vizitează pentru prima dată formularul pentru crearea unei noi postări, se va construi o resursă Post și de fiecare dată când se schimbă formularul, aplicăm modificarea Post :

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

Apoi, adăugați un apel invers onSubmit la formular pentru a salva postarea pe server și gestionați erorile dacă încercarea de salvare eșuează:

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

Editarea postărilor

Odată ce postarea a fost salvată, aceasta va fi conectată la API-ul tău ca resursă pe serverul tău. Puteți spune dacă o resursă este persistată pe server apelând persisted :

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

Pentru resursele persistente, ActiveResource.js acceptă atribute murdare, prin aceea că puteți verifica dacă vreun atribut al unei resurse a fost modificat de la valoarea sa de pe server.

Dacă apelați save() pe o resursă persistentă, aceasta va face o cerere PATCH care conține doar modificările făcute resursei, în loc să trimită întregul set de atribute și relații ale resursei la server în mod inutil.

Puteți adăuga atribute urmărite la o resursă folosind declarația de attributes . Să urmărim modificările aduse post.content :

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

Acum, cu o postare persistentă pe server, putem edita postarea, iar când se face clic pe butonul de trimitere, salvăm modificările pe server. De asemenea, putem dezactiva butonul de trimitere dacă nu s-au făcut încă modificări:

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

Există metode pentru gestionarea unei relații singulare precum post.user() , dacă dorim să schimbăm utilizatorul asociat unei postări:

 await post.updateUser(user);

Aceasta este echivalentă cu:

 await post.update({ user });

Resursa de comentarii

Acum creați o clasă de resurse Comment și relaționați-o cu Post . Amintiți-vă cerința noastră ca comentariile să poată fi ca răspuns la o postare sau la un alt comentariu, astfel încât resursa relevantă pentru un comentariu este polimorfă:

 // /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);

Asigurați-vă că adăugați Comment și la /src/resources/index.js .

Va trebui să adăugăm și o linie la clasa Post :

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

Opțiunea inverseOf transmisă definiției hasMany pentru replies indică faptul că acea relație este inversa definiției polimorfice belongsTo pentru resource . Proprietatea inverseOf a relațiilor sunt frecvent utilizate atunci când se efectuează operații între relații. De obicei, această proprietate va fi determinată automat prin numele clasei, dar deoarece relațiile polimorfe pot fi una dintre mai multe clase, trebuie să definiți singur opțiunea inverseOf pentru ca relațiile polimorfe să aibă toate aceeași funcționalitate ca și cele normale.

Gestionarea comentariilor la postări

Același DSL care se aplică resurselor se aplică și gestionării resurselor aferente. Acum că am stabilit relațiile dintre postări și comentarii, există o serie de moduri în care putem gestiona această relație.

Puteți adăuga un comentariu nou la o postare:

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

Puteți adăuga un răspuns la un comentariu:

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

Puteți edita un comentariu:

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

Puteți elimina un comentariu dintr-o postare:

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

Afișarea postărilor și comentariilor

SDK-ul poate fi folosit pentru a afișa o listă paginată de postări, iar când se face clic pe o postare, postarea este încărcată pe o pagină nouă cu toate comentariile sale:

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

Interogarea de mai sus va prelua cele mai recente 10 postări și, pentru optimizare, singurul atribut care este încărcat este content lor .

Dacă un utilizator face clic pe un buton pentru a merge la următoarea pagină de postări, un handler de modificări va prelua pagina următoare. Aici dezactivăm și butonul dacă nu există pagini următoare.

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

Când se face clic pe un link către o postare, deschidem o pagină nouă prin încărcarea și afișarea postării cu toate datele sale, inclusiv comentariile sale - cunoscute sub numele de răspunsuri - precum și răspunsurile la acele răspunsuri:

 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() ); }); }

Apelarea .target() pe o relație hasMany precum post.replies() va returna o ActiveResource.Collection de comentarii care au fost încărcate și stocate local.

Această distincție este importantă, deoarece post.replies().target().first() va returna primul comentariu încărcat. În schimb, post.replies().first() va returna o promisiune pentru un comentariu solicitat de la GET /api/v1/posts/:id/replies .

De asemenea, puteți solicita răspunsurile pentru o postare separat de solicitarea pentru postarea în sine, ceea ce vă permite să vă modificați interogarea. Puteți înlănțui modificatori cum ar fi order , select , includes , where , perPage , page atunci când interogați hasMany relații la fel ca atunci când interogați resursele în sine.

 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()); }

Modificarea resurselor după ce sunt solicitate

Uneori doriți să luați datele de pe server și să le modificați înainte de a le folosi. De exemplu, puteți post.createdAt într-un obiect moment() astfel încât să puteți afișa o dată și oră ușor de utilizat pentru utilizator despre momentul în care a fost creată postarea:

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

Imuabilitate

Dacă lucrați cu un sistem de management al stării care favorizează obiectele imuabile, tot comportamentul din ActiveResource.js poate fi făcut imuabil prin configurarea bibliotecii de resurse:

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

Încercarea înapoi: Conectarea sistemului de autentificare

Pentru a încheia, vă voi arăta cum să vă integrați sistemul de autentificare a utilizatorului în User ActiveResource .

Mutați sistemul de autentificare cu simboluri la punctul final API /api/v1/tokens . Când e-mailul și parola unui utilizator sunt trimise la acest punct final, datele utilizatorului autentificat plus tokenul de autorizare vor fi trimise ca răspuns.

Creați o clasă de resurse Token care aparține User :

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

Adăugați Token la /src/resources/index.js .

Apoi, adăugați o metodă statică de authenticate la clasa dvs. de resurse de User și relaționați User cu 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; } }

Această metodă utilizează resourceLibrary.interface() , care în acest caz este interfața JSON:API, pentru a trimite un utilizator la /api/v1/tokens . Acest lucru este valid: un punct final în JSON:API nu necesită ca singurele tipuri postate către și de la acesta să fie cele după care este numit. Deci cererea va fi:

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

Răspunsul va fi utilizatorul autentificat cu simbolul de autentificare inclus:

 { "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 } }] }

Apoi folosim token.id pentru a seta antetul Authorization al bibliotecii noastre și returnăm utilizatorul, ceea ce este la fel cu solicitarea utilizatorului prin User.find() așa cum am făcut înainte.

Acum, dacă apelați User.authenticate(email, password) , veți primi un utilizator autentificat ca răspuns și toate cererile viitoare vor fi autorizate cu un token de acces.

ActiveResource.js Permite dezvoltarea rapidă a SDK JavaScript

În acest tutorial, am explorat modalitățile prin care ActiveResource.js vă poate ajuta să construiți rapid un SDK JavaScript pentru a vă gestiona resursele API și diversele, uneori complicate, resurse asociate acestora. Puteți vedea toate aceste caracteristici și mai multe documentate în README pentru ActiveResource.js.

Sper că v-ați bucurat de ușurința în care se pot face aceste operațiuni și că veți folosi (și poate chiar contribuiți la) biblioteca mea pentru proiectele dumneavoastră viitoare, dacă se potrivește nevoilor dumneavoastră. În spiritul open source, PR-urile sunt întotdeauna binevenite!