ActiveResource.js: JSON API'niz İçin Güçlü Bir JavaScript SDK'sı Oluşturma, Hızlı
Yayınlanan: 2022-03-11Şirketiniz API'sini yeni başlattı ve şimdi çevresinde bir kullanıcı topluluğu oluşturmak istiyor. API'nizin sağladığı hizmetler, müşterilerin her şeyi kendileri yazmak yerine web uygulamaları oluşturmasını kolaylaştırdığından, müşterilerinizin çoğunun JavaScript'te çalışacağını biliyorsunuz - Twilio buna iyi bir örnektir.
Ayrıca, RESTful API'niz ne kadar basit olursa olsun, kullanıcıların kendileri için tüm ağır yükleri kaldıracak bir JavaScript paketi bırakmak isteyeceklerini de biliyorsunuz. API'nizi öğrenmek ve ihtiyaç duydukları her isteği kendileri oluşturmak istemeyeceklerdir.
Yani API'niz etrafında bir kitaplık oluşturuyorsunuz. Veya belki de kendi dahili API'nizle etkileşime giren bir web uygulaması için bir durum yönetim sistemi yazıyorsunuz.
Her iki durumda da, API kaynaklarınızdan birini veya daha kötüsü, bu kaynaklarla ilgili bir kaynağı CRUD'yi her CRUD yaptığınızda kendinizi tekrar tekrar tekrarlamak istemezsiniz. Bu, uzun vadede büyüyen bir SDK'yı yönetmek için iyi değildir ve zamanınızı iyi kullanmaz.
Bunun yerine, API'lerle etkileşim için bir JavaScript ORM sistemi olan ActiveResource.js'yi kullanabilirsiniz. Bunu bir projedeki ihtiyacımızı karşılamak için oluşturdum: Mümkün olduğunca az satırda bir JavaScript SDK'sı oluşturmak. Bu, bizim ve geliştirici topluluğumuz için maksimum verimlilik sağladı.
Ruby on Rails'in basit ActiveRecord ORM'sinin arkasındaki ilkelere dayanmaktadır.
JavaScript SDK İlkeleri
ActiveResource.js'nin tasarımına rehberlik eden iki Ruby on Rails fikri vardır:
- "Konfigürasyon üzerinden kural:" API'nin uç noktalarının doğası hakkında bazı varsayımlarda bulunun. Örneğin, bir
Product
kaynağınız varsa,/products
uç noktasına karşılık gelir. Bu şekilde, SDK'nızın API'nize yönelik isteklerinin her birini tekrar tekrar yapılandırmak için zaman harcanmaz. Geliştiriciler, büyüyen SDK'nıza saatler değil dakikalar içinde karmaşık CRUD sorgularıyla yeni API kaynakları ekleyebilir. - “Güzel kodu yüceltin:” Rails'in yaratıcısı DHH bunu en iyi şekilde söyledi - güzel kodun kendi iyiliği için harika bir yanı var. ActiveResource.js bazen çirkin istekleri güzel bir dış görünümle tamamlar. Artık filtreler ve sayfalandırma eklemek için özel kod yazmanız ve GET isteklerine ilişkiler üzerine yerleştirilmiş ilişkileri dahil etmeniz gerekmez. Ayrıca, bir nesnenin özelliklerinde değişiklik yapan ve bunları güncelleme için sunucuya gönderen POST ve PATCH istekleri oluşturmanız da gerekmez. Bunun yerine, bir ActiveResource'da bir yöntemi çağırmanız yeterlidir: İstediğiniz isteği almak için JSON ile daha fazla oynamanıza gerek yok, yalnızca bir sonraki için tekrar yapmanız gerekiyor.
Başlamadan önce
Bu yazının yazıldığı sırada ActiveResource.js'nin yalnızca JSON:API standardına göre yazılmış API'lerle çalıştığını belirtmek önemlidir.
JSON:API hakkında bilginiz yoksa ve devam etmek istiyorsanız, bir JSON:API sunucusu oluşturmak için birçok iyi kitaplık vardır.
Bununla birlikte, ActiveResource.js, belirli bir API standardı için bir sarmalayıcıdan çok bir DSL'dir. API'nizle etkileşim kurmak için kullandığı arabirim genişletilebilir, böylece gelecekteki makaleler ActiveResource.js'nin özel API'nizle nasıl kullanılacağını kapsayabilir.
İşleri Ayarlama
Başlamak için projenize active-resource
kurun:
yarn add active-resource
İlk adım, API'niz için bir ResourceLibrary
oluşturmaktır. Tüm ActiveResource
dosyalarımı src/resources
klasörüne koyacağım:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary('http://example.com/api/v1'); export default library;
CreateResourceLibrary için gerekli olan tek parametre createResourceLibrary
kök URL'sidir.
Ne Yaratacağız
Bir içerik yönetim sistemi API'si için bir JavaScript SDK kitaplığı oluşturacağız. Bu, kullanıcılar, gönderiler, yorumlar ve bildirimler olacağı anlamına gelir.
Kullanıcılar gönderileri okuyabilecek, oluşturabilecek ve düzenleyebilecek; yorumları okuyun, ekleyin ve silin (gönderilere veya diğer yorumlara) ve yeni gönderiler ve yorumlarla ilgili bildirimler alın.
Görünümü (React, Angular, vb.) veya durumu (Redux, vb.) yönetmek için belirli bir kitaplık kullanmayacağım, bunun yerine öğreticiyi ActiveResource
s aracılığıyla yalnızca API'nizle etkileşime girecek şekilde soyutlayacağım.
İlk Kaynak: Kullanıcılar
CMS kullanıcılarını yönetmek için bir User
kaynağı oluşturarak başlayacağız.
İlk olarak, bazı attributes
sahip bir User
kaynağı sınıfı oluşturuyoruz:
// /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);
Şimdilik, bir kullanıcı e-postasını ve parolasını gönderdiğinde, bir erişim belirteci ve kullanıcının kimliğini döndüren bir kimlik doğrulama uç noktanız olduğunu varsayalım. Bu uç nokta, requestToken
bazı işlevler tarafından yönetilir. Kimliği doğrulanmış kullanıcı kimliğini aldıktan sonra, kullanıcının tüm verilerini yüklemek istersiniz:
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); }
accessToken
library.headers
bir Authorization
başlığına sahip olacak şekilde ayarladım, böylece ResourceLibrary
gelecekteki tüm istekleri yetkilendirilir.
Daha sonraki bir bölüm, bir kullanıcının kimliğinin nasıl doğrulanacağını ve yalnızca User
kaynak sınıfını kullanarak erişim belirtecinin nasıl ayarlanacağını ele alacaktır.
authenticate
son adımı, User.find(id)
için bir istektir. Bu, /api/v1/users/:id
için bir istekte bulunacaktır ve yanıt şöyle görünebilir:
{ "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false } } }
authenticate
gelen yanıt, User
sınıfının bir örneği olacaktır. Uygulamada bir yerde görüntülemek istiyorsanız, buradan kimliği doğrulanmış kullanıcının çeşitli özelliklerine erişebilirsiniz.
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 } */
Öznitelik adlarının her biri, tipik JavaScript standartlarına uyması için camelCased olacaktır. Bunların her birini doğrudan user
nesnesinin özellikleri olarak alabilir veya user.attributes()
çağırarak tüm nitelikleri alabilirsiniz.
Kaynak Dizini Ekleme
Bildirimler gibi User
sınıfıyla ilgili daha fazla kaynak eklemeden önce, tüm kaynaklarımızı dizine ekleyecek bir dosya, src/resources/index.js
eklemeliyiz. Bunun iki faydası vardır:
- Birden çok içe aktarma ifadesi kullanmak yerine, birden çok kaynak için
src/resources
tek bir içe aktarma ifadesinde yok etmemize izin vererek, içe aktarmalarımızı temizleyecektir. - ActiveResource.js'nin ilişkiler kurması için gerekli olan her birinde
library.createResource
çağırarak oluşturacağımızResourceLibrary
üzerindeki tüm kaynakları başlatacaktır.
// /src/resources/index.js import User from './User'; export { User };
İlgili Kaynak Ekleme
Şimdi User
için ilgili bir kaynak, bir Notification
oluşturalım. Önce User
belongsTo
ait bir Notification
sınıfı oluşturun:
// /src/resources/Notification.js import library from './library'; class Notification extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Notification);
Ardından onu kaynaklar dizinine ekliyoruz:
// /src/resources/index.js import Notification from './Notification'; import User from './User'; export { Notification, User };
Ardından, bildirimleri User
sınıfıyla ilişkilendirin:
// /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); } }
Şimdi, kullanıcıyı authenticate
geri aldığımızda, tüm bildirimlerini yükleyebilir ve görüntüleyebiliriz:
let notifications = await user.notifications().load(); console.log(notifications.map(notification => notification.message));
Kimliği doğrulanmış kullanıcı için orijinal isteğimize bildirimleri de dahil edebiliriz:
async function authenticate(email, password) { /* ... */ return await User.includes('notifications').find(userId); }
Bu, DSL'de bulunan birçok seçenekten biridir.
DSL'yi gözden geçirme
Şimdi sadece şu ana kadar yazdığımız koddan talep etmek mümkün olana değinelim.
Bir kullanıcı koleksiyonunu veya tek bir kullanıcıyı sorgulayabilirsiniz.
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' });
Zincirlenebilir ilişkisel yöntemleri kullanarak sorguyu değiştirebilirsiniz:
// 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();
Sorguyu istediğiniz miktarda zincirleme değiştirici kullanarak oluşturabileceğinize ve sorguyu .all( .all()
, .first()
, .last()
veya .each()
ile sonlandırabileceğinize dikkat edin.
Yerel olarak bir kullanıcı oluşturabilir veya sunucuda bir tane oluşturabilirsiniz:
let user = User.build(attributes); user = await User.create(attributes);
Kalıcı bir kullanıcınız olduğunda, sunucuya kaydedilmek üzere değişiklikleri gönderebilirsiniz:
user.email = '[email protected]'; await user.save(); /* or */ await user.update({ email: '[email protected]' });
Sunucudan da silebilirsiniz:
await user.destroy();
Bu temel DSL, öğreticinin geri kalanında göstereceğim gibi, ilgili kaynakları da kapsar. Artık ActiveResource.js'yi CMS'nin geri kalanını oluşturmak için hızla uygulayabiliriz: gönderiler ve yorumlar.
Gönderi Oluşturma
Post
için bir kaynak sınıfı oluşturun ve bunu User
sınıfıyla ilişkilendirin:
// /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'); } }
Kaynak dizinine de Post
ekleyin:
// /src/resources/index.js import Notification from './Notification'; import Post from './Post'; import User from './User'; export { Notification, Post, User };
Ardından, Post
kaynağını, kullanıcıların gönderi oluşturması ve düzenlemesi için bir forma bağlayın. Kullanıcı yeni bir gönderi oluşturmak için formu ilk kez ziyaret ettiğinde, bir Post
kaynağı oluşturulur ve form her değiştirildiğinde, değişikliği Post
uygularız:

import Post from '/src/resources/Post'; let post = Post.build({ user: authenticatedUser }); onChange = (event) => { post.content = event.target.value; };
Ardından, gönderiyi sunucuya kaydetmek ve kaydetme girişimi başarısız olursa hataları işlemek için forma bir onSubmit
geri araması ekleyin:
onSubmit = async () => { try { await post.save(); /* successful, redirect to edit post form */ } catch { post.errors().each((field, error) => { console.log(field, error.message) }); } }
Gönderileri Düzenleme
Gönderi kaydedildikten sonra, sunucunuzda bir kaynak olarak API'nize bağlanacaktır. Bir kaynağın sunucuda kalıcı olup olmadığını persisted
öğesini çağırarak anlayabilirsiniz:
if (post.persisted()) { /* post is on server */ }
Kalıcı kaynaklar için ActiveResource.js, bir kaynağın herhangi bir özniteliğinin sunucudaki değerinden değiştirilip değiştirilmediğini kontrol edebileceğiniz kirli öznitelikleri destekler.
Kalıcı bir kaynakta save()
öğesini çağırırsanız, kaynağın tüm özniteliklerini ve ilişkilerini gereksiz yere sunucuya göndermek yerine, yalnızca kaynakta yapılan değişiklikleri içeren bir PATCH
isteği yapacaktır.
attributes
bildirimini kullanarak bir kaynağa izlenen öznitelikler ekleyebilirsiniz. post.content
değişiklikleri izleyelim:
// /src/resources/Post.js class Post extends library.Base { static define() { this.attributes('content'); /* ... */ } }
Artık sunucu kalıcı bir gönderi ile gönderiyi düzenleyebiliriz ve gönder düğmesine tıklandığında değişiklikleri sunucuya kaydedebiliriz. Henüz herhangi bir değişiklik yapılmadıysa gönder düğmesini de devre dışı bırakabiliriz:
onEdit = (event) => { post.content = event.target.value; } onSubmit = async () => { try { await post.save(); } catch { /* display edit errors */ } } disableSubmitButton = () => { return !post.changed(); }
Bir gönderiyle ilişkili kullanıcıyı değiştirmek istiyorsak, post.user()
gibi tekil bir ilişkiyi yönetmek için yöntemler vardır:
await post.updateUser(user);
Bu şuna eşdeğerdir:
await post.update({ user });
Yorum Kaynağı
Şimdi bir kaynak sınıfı Comment
oluşturun ve bunu Post
ile ilişkilendirin. Yorumların bir gönderiye veya başka bir yoruma yanıt olarak olabileceği gereksinimimizi unutmayın, bu nedenle bir yorum için ilgili kaynak polimorfiktir:
// /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);
/src/resources/index.js
da Comment
eklediğinizden emin olun.
Post
sınıfına da bir satır eklememiz gerekecek:
// /src/resources/Post.js class Post extends library.Base { static define() { /* ... */ this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } }
replies
için hasMany
tanımına geçirilen inverseOf
seçeneği, bu ilişkinin resource
için belongsTo
ait olan polimorfikin tersi olduğunu gösterir. İlişkilerin inverseOf
özelliği, ilişkiler arasında işlemler yapılırken sıklıkla kullanılır. Tipik olarak, bu özellik sınıf adı aracılığıyla otomatik olarak belirlenir, ancak polimorfik ilişkiler birden fazla sınıftan biri olabileceğinden, polimorfik ilişkilerin normal olanlarla aynı işlevselliğe sahip olması için inverseOf
seçeneğini kendiniz tanımlamanız gerekir.
Gönderilerdeki Yorumları Yönetme
Kaynaklar için geçerli olan aynı DSL, ilgili kaynakların yönetimi için de geçerlidir. Artık gönderiler ve yorumlar arasındaki ilişkileri kurduğumuza göre, bu ilişkiyi yönetmenin birkaç yolu var.
Bir gönderiye yeni bir yorum ekleyebilirsiniz:
onSubmitComment = async (event) => { let comment = await post.replies().create({ content: event.target.value, user: user }); }
Bir yoruma yanıt ekleyebilirsiniz:
onSubmitReply = async (event) => { let reply = await comment.replies().create({ content: event.target.value, user: user }); }
Bir yorumu düzenleyebilirsiniz:
onEditComment = async (event) => { await comment.update({ content: event.target.value }); }
Bir gönderiden bir yorumu kaldırabilirsiniz:
onDeleteComment = async (comment) => { await post.replies().delete(comment); }
Gönderileri ve Yorumları Görüntüleme
SDK, gönderilerin sayfalandırılmış bir listesini görüntülemek için kullanılabilir ve bir gönderi tıklandığında, gönderi tüm yorumlarıyla birlikte yeni bir sayfaya yüklenir:
import { Post } from '/src/resources'; let postsPage = await Post .order({ createdAt: 'desc' }) .select('content') .perPage(10) .all();
Yukarıdaki sorgu en son 10 gönderiyi alacak ve optimize etmek için yüklenen tek öznitelik onların content
.
Bir kullanıcı bir sonraki gönderi sayfasına gitmek için bir düğmeyi tıklarsa, bir değişiklik işleyicisi sonraki sayfayı alır. Burada ayrıca bir sonraki sayfa yoksa düğmeyi devre dışı bırakıyoruz.
onClickNextPage = async () => { postsPage = await postsPage.nextPage(); if (!postsPage.hasNextPage()) { /* disable next page button */ } };
Bir gönderiye bir bağlantı tıklandığında, gönderiyi tüm verileriyle birlikte yükleyerek ve görüntüleyerek yeni bir sayfa açarız - yanıtlar olarak bilinir - ve bu yanıtlara verilen yanıtlar:
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() ); }); }
post.replies( .target()
gibi bir hasMany
ilişkisinde post.replies()
, yerel olarak yüklenmiş ve depolanmış bir ActiveResource.Collection
yorumunu döndürür.
Bu ayrım önemlidir, çünkü post.replies().target().first()
yüklenen ilk yorumu döndürür. Buna karşılık, post.replies().first()
, GET /api/v1/posts/:id/replies
replies'den istenen bir yorum için bir söz verir.
Ayrıca, bir gönderinin yanıtlarını, gönderinin kendisinden ayrı olarak talep edebilirsiniz; bu, sorgunuzu değiştirmenize olanak tanır. hasMany
ilişkilerini sorgularken order
, select
, includes
, where
, perPage
, page
gibi değiştiricileri zincirleyebilirsiniz, tıpkı kaynakları sorgularken yaptığınız gibi.
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()); }
Kaynakları Talep Edildikten Sonra Değiştirme
Bazen verileri sunucudan alıp kullanmadan önce değiştirmek istersiniz. Örneğin, post.createdAt
bir moment()
nesnesine sarabilirsiniz, böylece kullanıcı için gönderinin ne zaman oluşturulduğu hakkında kullanıcı dostu bir tarih saat görüntüleyebilirsiniz:
// /src/resources/Post.js import moment from 'moment'; class Post extends library.Base { static define() { /* ... */ this.afterRequest(function() { this.createdAt = moment(this.createdAt); }); } }
Değişmezlik
Değişmez nesneleri destekleyen bir durum yönetim sistemiyle çalışıyorsanız, ActiveResource.js'deki tüm davranışlar kaynak kitaplığı yapılandırılarak değişmez hale getirilebilir:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary( 'http://example.com/api/v1', { immutable: true } ); export default library;
Geri Dönme: Kimlik Doğrulama Sistemini Bağlama
Özetlemek gerekirse, kullanıcı doğrulama sisteminizi User
ActiveResource
nasıl entegre edeceğinizi göstereceğim.
Belirteç kimlik doğrulama sisteminizi /api/v1/tokens
API bitiş noktasına taşıyın. Bir kullanıcının e-postası ve parolası bu uç noktaya gönderildiğinde, kimliği doğrulanmış kullanıcının verileri ve yetkilendirme belirteci yanıt olarak gönderilir.
User
'a ait bir Token
kaynak sınıfı oluşturun:
// /src/resources/Token.js import library from './library'; class Token extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Token);
Token
/src/resources/index.js
.
Ardından, User
kaynak sınıfınıza statik bir authenticate
yöntemi ekleyin ve User
ile Token
arasında ilişki kurun:
// /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; } }
Bu yöntem, bir kullanıcıyı /api/v1/tokens
göndermek için bu durumda JSON:API arabirimi olan resourceLibrary.interface()
öğesini kullanır. Bu geçerlidir: JSON:API'deki bir uç nokta, kendisine gönderilen ve ondan gönderilen türlerin yalnızca adının verildiği türler olmasını gerektirmez. Yani istek şöyle olacaktır:
{ "data": { "type": "users", "attributes": { "email": "[email protected]", "password": "password" } } }
Yanıt, kimlik doğrulama belirtecinin dahil olduğu kimliği doğrulanmış kullanıcı olacaktır:
{ "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 } }] }
Ardından, token.id
kitaplığımızın Authorization
başlığını ayarlamak için kullanırız ve daha önce yaptığımız gibi kullanıcıyı User.find()
aracılığıyla istemekle aynı olan kullanıcıyı döndürürüz.
Şimdi, User.authenticate(email, password)
, yanıt olarak kimliği doğrulanmış bir kullanıcı alırsınız ve gelecekteki tüm istekler bir erişim belirteci ile yetkilendirilir.
ActiveResource.js, Hızlı JavaScript SDK Geliştirmesini Sağlar
Bu öğreticide, ActiveResource.js'nin API kaynaklarınızı ve bunların çeşitli, bazen karmaşık, ilgili kaynaklarını yönetmek için hızlı bir şekilde bir JavaScript SDK'sı oluşturmanıza nasıl yardımcı olabileceğini araştırdık. Tüm bu özellikleri ve daha fazlasını ActiveResource.js için README belgesinde görebilirsiniz.
Umarım bu işlemlerin kolaylıkla yapılabildiğini ve ihtiyaçlarınızı karşılarsa gelecekteki projeleriniz için kitaplığımı kullanacağınızı (ve belki de katkıda bulunacağınızı) beğenmişsinizdir. Açık kaynak ruhuyla, PR'lara her zaman açığız!