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:

  1. "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.
  2. “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:

  1. 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.
  2. ActiveResource.js'nin ilişkiler kurması için gerekli olan her birinde library.createResource çağırarak oluşturacağımız ResourceLibrary ü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!