ActiveResource.js: szybkie tworzenie potężnego zestawu SDK JavaScript dla interfejsu API JSON

Opublikowany: 2022-03-11

Twoja firma właśnie uruchomiła swoje API i chce teraz zbudować wokół niego społeczność użytkowników. Wiesz, że większość Twoich klientów będzie pracować w języku JavaScript, ponieważ usługi oferowane przez Twój interfejs API ułatwiają klientom tworzenie aplikacji internetowych zamiast pisania wszystkiego samodzielnie — Twilio jest tego dobrym przykładem.

Wiesz również, że tak proste, jak Twój RESTful API może być, użytkownicy będą chcieli wrzucić pakiet JavaScript, który wykona za nich całą ciężką pracę. Nie będą chcieli uczyć się Twojego interfejsu API i samodzielnie tworzyć każdego żądania, którego potrzebują.

Więc budujesz bibliotekę wokół swojego API. A może po prostu piszesz system zarządzania stanem dla aplikacji internetowej, która współdziała z Twoim własnym wewnętrznym API.

Tak czy inaczej, nie chcesz powtarzać się w kółko za każdym razem, gdy CRUD jeden ze swoich zasobów API lub, co gorsza, CRUD zasób związany z tymi zasobami. Nie jest to dobre dla zarządzania rosnącym SDK na dłuższą metę, ani nie jest to dobre wykorzystanie twojego czasu.

Zamiast tego możesz użyć ActiveResource.js, systemu ORM JavaScript do interakcji z interfejsami API. Stworzyłem go, aby zaspokoić potrzebę, którą mieliśmy w projekcie: stworzyć JavaScript SDK w jak najmniejszej liczbie linijek. Umożliwiło to nam i naszej społeczności programistów maksymalną wydajność.

Opiera się na zasadach prostego ORM ActiveRecord w Ruby on Rails.

Zasady JavaScript SDK

Istnieją dwa pomysły Ruby on Rails, które kierowały projektowaniem ActiveResource.js:

  1. „Konwencja nad konfiguracją”: Zrób pewne założenia dotyczące natury punktów końcowych interfejsu API. Na przykład, jeśli masz zasób Product , odpowiada on punktowi końcowemu /products . W ten sposób nie będziesz tracić czasu na wielokrotne konfigurowanie każdego żądania pakietu SDK do interfejsu API. Deweloperzy mogą dodawać nowe zasoby API ze skomplikowanymi zapytaniami CRUD do rozwijającego się pakietu SDK w ciągu kilku minut, a nie godzin.
  2. „Exalt beautiful code”: twórca Rails, DHH, ujął to najlepiej — jest po prostu coś wspaniałego w pięknym kodzie dla samego w sobie. ActiveResource.js otacza czasami brzydkie żądania pięknym wyglądem. Nie musisz już pisać niestandardowego kodu, aby dodawać filtry i paginację oraz dołączać relacje zagnieżdżone w relacjach do żądań GET. Nie musisz też tworzyć żądań POST i PATCH, które wprowadzają zmiany we właściwościach obiektu i wysyłają je do serwera w celu aktualizacji. Zamiast tego po prostu wywołaj metodę na ActiveResource: koniec zabawy z JSON, aby uzyskać żądane żądanie, tylko po to, aby zrobić to ponownie dla następnego.

Zanim zaczniemy

Należy zauważyć, że w chwili pisania tego tekstu ActiveResource.js działa tylko z interfejsami API napisanymi zgodnie ze standardem JSON:API.

Jeśli nie znasz JSON:API i chcesz kontynuować, istnieje wiele dobrych bibliotek do tworzenia serwera JSON:API.

To powiedziawszy, ActiveResource.js jest bardziej DSL niż opakowaniem dla jednego konkretnego standardu API. Interfejs, którego używa do interakcji z interfejsem API, można rozszerzyć, więc przyszłe artykuły mogą opisywać, jak używać ActiveResource.js z niestandardowym interfejsem API.

Konfigurowanie rzeczy

Aby rozpocząć, zainstaluj active-resource w swoim projekcie:

 yarn add active-resource

Pierwszym krokiem jest utworzenie ResourceLibrary dla Twojego interfejsu API. Zamierzam umieścić wszystkie moje ActiveResource w folderze src/resources :

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

Jedynym parametrem wymaganym do createResourceLibrary jest główny adres URL Twojego interfejsu API.

Co stworzymy

Zamierzamy stworzyć bibliotekę JavaScript SDK dla API systemu zarządzania treścią. Oznacza to, że będą użytkownicy, wpisy, komentarze i powiadomienia.

Użytkownicy będą mogli czytać, tworzyć i edytować posty; czytać, dodawać i usuwać komentarze (do postów lub innych komentarzy) oraz otrzymywać powiadomienia o nowych postach i komentarzach.

Nie zamierzam używać żadnej konkretnej biblioteki do zarządzania widokiem (React, Angular itp.) Lub stanem (Redux itp.), zamiast tego streszczam samouczek do interakcji tylko z interfejsem API za pośrednictwem ActiveResource s.

Pierwszy zasób: Użytkownicy

Zaczniemy od utworzenia zasobu User do zarządzania użytkownikami CMS.

Najpierw tworzymy klasę zasobów User z pewnymi 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);

Załóżmy na razie, że masz punkt końcowy uwierzytelniania, który po przesłaniu przez użytkownika adresu e-mail i hasła zwraca token dostępu i identyfikator użytkownika. Ten punkt końcowy jest zarządzany przez jakąś funkcję requestToken . Po uzyskaniu uwierzytelnionego identyfikatora użytkownika chcesz załadować wszystkie dane użytkownika:

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

Ustawiłem library.headers tak, aby posiadał nagłówek Authorization z accessToken , aby wszystkie przyszłe żądania przez moją bibliotekę ResourceLibrary były autoryzowane.

W dalszej części omówimy, jak uwierzytelnić użytkownika i ustawić token dostępu przy użyciu tylko klasy zasobów User .

Ostatnim krokiem authenticate jest żądanie do User.find(id) . Spowoduje to wysłanie żądania do /api/v1/users/:id , a odpowiedź może wyglądać mniej więcej tak:

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

Odpowiedź z authenticate będzie instancją klasy User . Z tego miejsca możesz uzyskać dostęp do różnych atrybutów uwierzytelnionego użytkownika, jeśli chcesz je wyświetlić gdzieś w aplikacji.

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

Każda z nazw atrybutów stanie się camelCased, aby dopasować się do typowych standardów JavaScript. Każdy z nich można uzyskać bezpośrednio jako właściwości obiektu user lub uzyskać wszystkie atrybuty, wywołując user.attributes() .

Dodawanie indeksu zasobów

Zanim dodamy więcej zasobów związanych z klasą User , takich jak powiadomienia, powinniśmy dodać plik src/resources/index.js , który zindeksuje wszystkie nasze zasoby. Ma to dwie zalety:

  1. Wyczyści nasze importy, umożliwiając nam destrukturyzację src/resources dla wielu zasobów w jednej instrukcji import zamiast używania wielu instrukcji import.
  2. Zainicjuje wszystkie zasoby w ResourceLibrary , które utworzymy, wywołując na każdym z nich library.createResource , co jest niezbędne do budowania relacji przez ActiveResource.js.
 // /src/resources/index.js import User from './User'; export { User };

Dodawanie powiązanego zasobu

Teraz utwórzmy powiązany zasób dla User , Notification . Najpierw utwórz klasę Notification , która belongsTo do klasy User :

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

Następnie dodajemy go do indeksu zasobów:

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

Następnie powiąż powiadomienia z klasą User :

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

Teraz, gdy odzyskamy użytkownika z authenticate , możemy załadować i wyświetlić wszystkie jego powiadomienia:

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

Możemy również uwzględnić powiadomienia w naszym pierwotnym żądaniu dla uwierzytelnionego użytkownika:

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

Jest to jedna z wielu opcji dostępnych w DSL.

Przeglądanie DSL

Omówmy to, czego już można zażądać tylko z kodu, który do tej pory napisaliśmy.

Możesz wysłać zapytanie do kolekcji użytkowników lub pojedynczego użytkownika.

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

Możesz modyfikować zapytanie za pomocą metod relacyjnych, które można łańcuchować:

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

Zwróć uwagę, że możesz utworzyć zapytanie, używając dowolnej liczby modyfikatorów połączonych łańcuchem, i że możesz zakończyć zapytanie za pomocą .all() , .first() , .last() lub .each .each() .

Możesz zbudować użytkownika lokalnie lub utworzyć go na serwerze:

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

Po uzyskaniu trwałego użytkownika możesz wysłać do niego zmiany, które zostaną zapisane na serwerze:

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

Możesz go również usunąć z serwera:

 await user.destroy();

Ta podstawowa technologia DSL obejmuje również powiązane zasoby, co zademonstruję w dalszej części samouczka. Teraz możemy szybko zastosować ActiveResource.js do tworzenia reszty CMS: postów i komentarzy.

Tworzenie postów

Utwórz klasę zasobów dla Post i skojarz ją z klasą 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'); } }

Dodaj Post również do indeksu zasobów:

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

Następnie powiąż zasób Post z formularzem, aby użytkownicy mogli tworzyć i edytować posty. Gdy użytkownik po raz pierwszy odwiedzi formularz w celu utworzenia nowego posta, zostanie zbudowany zasób Post i za każdym razem, gdy formularz zostanie zmieniony, zastosujemy zmianę do Post :

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

Następnie dodaj wywołanie zwrotne onSubmit do formularza, aby zapisać post na serwerze i obsłużyć błędy, jeśli próba zapisania się nie powiedzie:

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

Edytowanie postów

Gdy post zostanie zapisany, zostanie połączony z Twoim API jako zasób na Twoim serwerze. Możesz stwierdzić, czy zasób jest utrwalony na serwerze, wywołując persisted :

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

W przypadku zasobów utrwalonych ActiveResource.js obsługuje brudne atrybuty, dzięki czemu można sprawdzić, czy jakikolwiek atrybut zasobu został zmieniony w stosunku do jego wartości na serwerze.

Jeśli wywołasz save() na utrwalonym zasobie, spowoduje to żądanie PATCH zawierające tylko zmiany wprowadzone w zasobie, zamiast niepotrzebnego przesyłania całego zestawu atrybutów i relacji zasobu do serwera.

Możesz dodać śledzone atrybuty do zasobu za pomocą deklaracji attributes . Śledźmy zmiany w post.content :

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

Teraz, z postem utrwalonym na serwerze, możemy go edytować, a po kliknięciu przycisku Prześlij zapisać zmiany na serwerze. Możemy również wyłączyć przycisk przesyłania, jeśli nie dokonano jeszcze żadnych zmian:

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

Istnieją metody zarządzania pojedynczą relacją, takie jak post.user() , gdybyśmy chcieli zmienić użytkownika powiązanego z postem:

 await post.updateUser(user);

Odpowiada to:

 await post.update({ user });

Zasób komentarzy

Teraz utwórz klasę zasobów Comment i powiąż ją z Post . Pamiętaj o naszym wymaganiu, aby komentarze mogły być odpowiedzią na post lub inny komentarz, więc odpowiedni zasób dla komentarza jest polimorficzny:

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

Upewnij się, że dodałeś również Comment do /src/resources/index.js .

Musimy również dodać linię do klasy Post :

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

Opcja inverseOf przekazana do definicji hasMany dla replies wskazuje, że ta relacja jest odwrotnością polimorficznej definicji belongsTo dla resource . Właściwość inverseOf relacji jest często używana podczas wykonywania operacji między relacjami. Zazwyczaj ta właściwość będzie automatycznie określana przez nazwę klasy, ale ponieważ relacje polimorficzne mogą być jedną z wielu klas, musisz samodzielnie zdefiniować opcję inverseOf , aby relacje polimorficzne miały taką samą funkcjonalność jak normalne.

Zarządzanie komentarzami w postach

To samo DSL, które dotyczy zasobów, dotyczy również zarządzania powiązanymi zasobami. Teraz, gdy mamy już skonfigurowane relacje między postami i komentarzami, istnieje kilka sposobów zarządzania tą relacją.

Możesz dodać nowy komentarz do posta:

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

Możesz dodać odpowiedź do komentarza:

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

Możesz edytować komentarz:

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

Możesz usunąć komentarz z posta:

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

Wyświetlanie postów i komentarzy

SDK może być używany do wyświetlania podzielonej na strony listy postów, a kiedy post zostanie kliknięty, post jest ładowany na nową stronę ze wszystkimi komentarzami:

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

Powyższe zapytanie pobierze 10 najnowszych postów, a w celu optymalizacji jedynym atrybutem, który zostanie załadowany, jest ich content .

Jeśli użytkownik kliknie przycisk, aby przejść do następnej strony postów, osoba obsługująca zmiany pobierze następną stronę. Tutaj również wyłączamy przycisk, jeśli nie ma następnych stron.

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

Po kliknięciu linku do wpisu otwieramy nową stronę, ładując i wyświetlając wpis ze wszystkimi jego danymi, w tym komentarzami — znanymi jako odpowiedzi — oraz odpowiedziami na te odpowiedzi:

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

Wywołanie .target() w relacji hasMany , takiej jak post.replies() , zwróci ActiveResource.Collection komentarzy, które zostały załadowane i przechowywane lokalnie.

To rozróżnienie jest ważne, ponieważ post.replies().target().first() zwróci pierwszy załadowany komentarz. Natomiast post.replies().first() zwróci obietnicę dla jednego komentarza żądanego z GET /api/v1/posts/:id/replies .

Możesz również poprosić o odpowiedzi na post niezależnie od prośby o sam post, co pozwala na modyfikację zapytania. Możesz łączyć modyfikatory, takie jak order , select , includes , where , perPage , page podczas zapytań o relacje hasMany , tak jak w przypadku zapytań o same zasoby.

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

Modyfikowanie zasobów po ich zażądaniu

Czasami chcesz pobrać dane z serwera i zmodyfikować je przed użyciem. Na przykład, możesz zapakować post.createdAt w obiekt moment() , aby wyświetlić przyjazną dla użytkownika datę i godzinę, informującą o tym, kiedy post został utworzony:

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

Niezmienność

Jeśli pracujesz z systemem zarządzania stanem, który faworyzuje niezmienne obiekty, wszystkie zachowania w ActiveResource.js można uczynić niezmiennymi, konfigurując bibliotekę zasobów:

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

Wracając: łączenie systemu uwierzytelniania

Na zakończenie pokażę, jak zintegrować system uwierzytelniania User z ActiveResource użytkownika.

Przenieś system uwierzytelniania tokenów do punktu końcowego interfejsu API /api/v1/tokens . Gdy adres e-mail i hasło użytkownika zostaną wysłane do tego punktu końcowego, w odpowiedzi zostaną wysłane dane uwierzytelnionego użytkownika oraz token autoryzacji.

Utwórz klasę zasobów Token , która należy do User :

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

Dodaj Token do /src/resources/index.js .

Następnie dodaj statyczną metodę authenticate do swojej klasy zasobów User i powiąż User z 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; } }

Ta metoda używa resourceLibrary.interface() , który w tym przypadku jest interfejsem JSON:API, do wysłania użytkownika do /api/v1/tokens . Jest to prawidłowe: punkt końcowy w JSON:API nie wymaga, aby jedynymi typami wysyłanymi do i z niego były te, od których ma nazwę. Tak więc prośba będzie:

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

Odpowiedzią będzie uwierzytelniony użytkownik z dołączonym tokenem uwierzytelniającym:

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

Następnie używamy token.id , aby ustawić nagłówek Authorization naszej biblioteki i zwrócić użytkownika, co jest tym samym, co żądanie użytkownika przez User.find() , tak jak robiliśmy to wcześniej.

Teraz, jeśli wywołasz User.authenticate(email, password) , otrzymasz w odpowiedzi uwierzytelnionego użytkownika, a wszystkie przyszłe żądania będą autoryzowane za pomocą tokena dostępu.

ActiveResource.js umożliwia szybkie opracowywanie SDK JavaScript

W tym samouczku zbadaliśmy, w jaki sposób ActiveResource.js może pomóc w szybkim zbudowaniu zestawu SDK JavaScript do zarządzania zasobami API i ich różnymi, czasami skomplikowanymi, powiązanymi zasobami. Możesz zobaczyć wszystkie te funkcje i więcej udokumentowane w README dla ActiveResource.js.

Mam nadzieję, że spodobała Ci się łatwość, z jaką można wykonać te operacje, i że będziesz używać (a może nawet przyczynić się do) mojej biblioteki do swoich przyszłych projektów, jeśli będzie odpowiadała Twoim potrzebom. W duchu open source PR-owcy są zawsze mile widziani!