ActiveResource.js: Membangun SDK JavaScript yang Kuat untuk API JSON Anda, Cepat

Diterbitkan: 2022-03-11

Perusahaan Anda baru saja meluncurkan API-nya dan sekarang ingin membangun komunitas pengguna di sekitarnya. Anda tahu bahwa sebagian besar pelanggan Anda akan bekerja dalam JavaScript, karena layanan yang disediakan API Anda memudahkan pelanggan untuk membangun aplikasi web daripada menulis semuanya sendiri—Twilio adalah contoh yang bagus untuk hal ini.

Anda juga tahu bahwa sesederhana RESTful API Anda, pengguna akan ingin memasukkan paket JavaScript yang akan melakukan semua pekerjaan berat untuk mereka. Mereka tidak ingin mempelajari API Anda dan membuat setiap permintaan yang mereka butuhkan sendiri.

Jadi, Anda sedang membangun perpustakaan di sekitar API Anda. Atau mungkin Anda hanya menulis sistem manajemen status untuk aplikasi web yang berinteraksi dengan API internal Anda sendiri.

Either way, Anda tidak ingin mengulangi diri Anda berulang kali setiap kali Anda CRUD salah satu sumber daya API Anda, atau lebih buruk lagi, CRUD sumber daya yang terkait dengan sumber daya tersebut. Ini tidak baik untuk mengelola SDK yang berkembang dalam jangka panjang, juga tidak baik menggunakan waktu Anda.

Sebagai gantinya, Anda dapat menggunakan ActiveResource.js, sistem ORM JavaScript untuk berinteraksi dengan API. Saya membuatnya untuk memenuhi kebutuhan yang kami miliki di sebuah proyek: untuk membuat JavaScript SDK dalam baris sesedikit mungkin. Ini memungkinkan efisiensi maksimum bagi kami dan komunitas pengembang kami.

Ini didasarkan pada prinsip-prinsip di balik ORM ActiveRecord sederhana Ruby on Rails.

Prinsip JavaScript SDK

Ada dua ide Ruby on Rails yang memandu desain ActiveResource.js:

  1. “Konvensi tentang konfigurasi:” Buat beberapa asumsi tentang sifat titik akhir API. Misalnya, jika Anda memiliki sumber daya Product , itu sesuai dengan titik akhir /products . Dengan begitu, waktu tidak dihabiskan berulang kali untuk mengonfigurasi setiap permintaan SDK ke API Anda. Pengembang dapat menambahkan sumber daya API baru dengan kueri CRUD yang rumit ke SDK Anda yang berkembang dalam hitungan menit, bukan jam.
  2. “Kode yang sangat indah:” Pencipta Rails DHH mengatakannya dengan sangat baik—ada sesuatu yang hebat tentang kode yang indah untuk kepentingannya sendiri. ActiveResource.js terkadang membungkus permintaan yang jelek dengan tampilan luar yang indah. Anda tidak lagi harus menulis kode khusus untuk menambahkan filter dan pagination serta menyertakan hubungan yang bersarang pada hubungan ke permintaan GET. Anda juga tidak harus membuat permintaan POST dan PATCH yang mengambil perubahan pada properti objek dan mengirimkannya ke server untuk diperbarui. Sebagai gantinya, panggil saja metode pada ActiveResource: Tidak perlu lagi bermain-main dengan JSON untuk mendapatkan permintaan yang Anda inginkan, hanya perlu melakukannya lagi untuk permintaan berikutnya.

Sebelum kita mulai

Penting untuk dicatat bahwa pada saat penulisan ini, ActiveResource.js hanya berfungsi dengan API yang ditulis menurut standar JSON:API.

Jika Anda tidak terbiasa dengan JSON:API dan ingin mengikuti, ada banyak perpustakaan yang bagus untuk membuat server JSON:API.

Yang mengatakan, ActiveResource.js lebih merupakan DSL daripada pembungkus untuk satu standar API tertentu. Antarmuka yang digunakannya untuk berinteraksi dengan API Anda dapat diperluas, sehingga artikel mendatang dapat membahas cara menggunakan ActiveResource.js dengan API khusus Anda.

Mengatur Segalanya

Untuk memulai, instal active-resource di proyek Anda:

 yarn add active-resource

Langkah pertama adalah membuat ResourceLibrary untuk API Anda. Saya akan meletakkan semua ActiveResource s saya di folder src/resources :

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

Satu-satunya parameter yang diperlukan untuk createResourceLibrary adalah URL root API Anda.

Apa yang Akan Kami Buat

Kami akan membuat pustaka JavaScript SDK untuk API sistem manajemen konten. Artinya akan ada pengguna, postingan, komentar, dan notifikasi.

Pengguna akan dapat membaca, membuat, dan mengedit postingan; membaca, menambah, dan menghapus komentar (ke postingan, atau ke komentar lain), dan menerima notifikasi postingan dan komentar baru.

Saya tidak akan menggunakan pustaka khusus untuk mengelola tampilan (React, Angular, dll.) atau status (Redux, dll.), alih-alih mengabstraksi tutorial untuk berinteraksi hanya dengan API Anda melalui ActiveResource s.

Sumber Daya Pertama: Pengguna

Kita akan mulai dengan membuat sumber daya User untuk mengelola pengguna CMS.

Pertama, kita membuat kelas sumber daya User dengan beberapa 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);

Mari kita asumsikan untuk saat ini bahwa Anda memiliki titik akhir otentikasi yang, setelah pengguna mengirimkan email dan kata sandi mereka, mengembalikan token akses dan ID pengguna. Titik akhir ini dikelola oleh beberapa fungsi requestToken . Setelah Anda mendapatkan ID pengguna yang diautentikasi, Anda ingin memuat semua data pengguna:

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

Saya mengatur library.headers untuk memiliki header Authorization dengan accessToken sehingga semua permintaan di masa mendatang oleh ResourceLibrary saya diotorisasi.

Bagian selanjutnya akan membahas cara mengautentikasi pengguna dan menyetel token akses hanya menggunakan kelas sumber daya User .

Langkah terakhir dari authenticate adalah permintaan ke User.find(id) . Ini akan membuat permintaan ke /api/v1/users/:id , dan responsnya mungkin terlihat seperti:

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

Respons dari authenticate akan menjadi turunan dari kelas User . Dari sini, Anda dapat mengakses berbagai atribut pengguna yang diautentikasi, jika Anda ingin menampilkannya di suatu tempat di aplikasi.

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

Setiap nama atribut akan menjadi camelCased, agar sesuai dengan standar khas JavaScript. Anda bisa mendapatkan masing-masing secara langsung sebagai properti dari objek user , atau mendapatkan semua atribut dengan memanggil user.attributes() .

Menambahkan Indeks Sumber Daya

Sebelum kita menambahkan lebih banyak sumber daya yang berhubungan dengan kelas User , seperti pemberitahuan, kita harus menambahkan file, src/resources/index.js , yang akan mengindeks semua sumber daya kita. Ini memiliki dua manfaat:

  1. Ini akan membersihkan impor kami dengan memungkinkan kami untuk merusak src/resources untuk beberapa sumber daya dalam satu pernyataan impor alih-alih menggunakan beberapa pernyataan impor.
  2. Ini akan menginisialisasi semua sumber daya di ResourceLibrary yang akan kita buat dengan memanggil library.createResource pada masing-masing, yang diperlukan untuk ActiveResource.js untuk membangun hubungan.
 // /src/resources/index.js import User from './User'; export { User };

Menambahkan Sumber Daya Terkait

Sekarang mari kita buat resource terkait untuk User , sebuah Notification . Pertama buat kelas Notification belongsTo kelas User :

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

Kemudian kami menambahkannya ke indeks sumber daya:

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

Kemudian, hubungkan notifikasi ke kelas User :

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

Sekarang, setelah kami mendapatkan pengguna kembali dari authenticate , kami dapat memuat dan menampilkan semua notifikasinya:

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

Kami juga dapat menyertakan pemberitahuan dalam permintaan asli kami untuk pengguna yang diautentikasi:

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

Ini adalah salah satu dari banyak pilihan yang tersedia di DSL.

Meninjau DSL

Mari kita bahas apa yang sudah mungkin untuk diminta hanya dari kode yang telah kita tulis sejauh ini.

Anda dapat mengkueri kumpulan pengguna, atau satu pengguna.

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

Anda dapat mengubah kueri menggunakan metode relasional yang dapat dirantai:

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

Perhatikan bahwa Anda dapat membuat kueri menggunakan sejumlah pengubah berantai, dan Anda dapat mengakhiri kueri dengan .all() , .first() , .last() , atau .each() .

Anda dapat membuat pengguna secara lokal, atau membuatnya di server:

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

Setelah Anda memiliki pengguna yang bertahan, Anda dapat mengirim perubahan untuk disimpan di server:

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

Anda juga dapat menghapusnya dari server:

 await user.destroy();

DSL dasar ini meluas ke sumber daya terkait juga, seperti yang akan saya tunjukkan selama sisa tutorial. Sekarang kita dapat dengan cepat menerapkan ActiveResource.js untuk membuat sisa CMS: posting dan komentar.

Membuat Posting

Buat kelas sumber daya untuk Post dan kaitkan dengan kelas 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'); } }

Tambahkan Post ke indeks sumber daya juga:

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

Kemudian ikat sumber daya Post ke dalam formulir bagi pengguna untuk membuat dan mengedit postingan. Saat pengguna pertama kali mengunjungi formulir untuk membuat postingan baru, sumber daya Post akan dibuat, dan setiap kali formulir diubah, kami menerapkan perubahan pada Post :

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

Selanjutnya, tambahkan panggilan balik onSubmit ke formulir untuk menyimpan kiriman ke server, dan tangani kesalahan jika upaya penyimpanan gagal:

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

Mengedit Posting

Setelah posting disimpan, itu akan ditautkan ke API Anda sebagai sumber daya di server Anda. Anda dapat mengetahui apakah sumber daya tetap ada di server dengan memanggil persisted :

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

Untuk sumber daya yang bertahan, ActiveResource.js mendukung atribut kotor, di mana Anda dapat memeriksa apakah ada atribut sumber daya yang diubah dari nilainya di server.

Jika Anda memanggil save() pada sumber daya yang bertahan, itu akan membuat permintaan PATCH yang hanya berisi perubahan yang dibuat pada sumber daya, alih-alih mengirimkan seluruh rangkaian atribut dan hubungan sumber daya ke server secara tidak perlu.

Anda dapat menambahkan atribut terlacak ke sumber daya menggunakan deklarasi attributes . Mari lacak perubahan pada post.content :

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

Sekarang, dengan posting yang dipertahankan server, kita dapat mengedit posting, dan ketika tombol kirim diklik, simpan perubahan ke server. Kami juga dapat menonaktifkan tombol kirim jika belum ada perubahan:

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

Ada metode untuk mengelola hubungan tunggal seperti post.user() , jika kita ingin mengubah pengguna yang terkait dengan postingan:

 await post.updateUser(user);

Ini setara dengan:

 await post.update({ user });

Sumber Komentar

Sekarang buat kelas sumber daya Comment dan hubungkan dengan Post . Ingat persyaratan kami bahwa komentar dapat berupa tanggapan terhadap kiriman, atau komentar lain, sehingga sumber daya yang relevan untuk komentar adalah polimorfik:

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

Pastikan untuk menambahkan Comment ke /src/resources/index.js juga.

Kita juga perlu menambahkan baris ke kelas Post :

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

Opsi inverseOf yang diteruskan ke definisi hasMany untuk replies menunjukkan bahwa hubungan tersebut merupakan kebalikan dari definisi milik belongsTo untuk resource . Properti inverseOf dari relasi sering digunakan saat melakukan operasi antar relasi. Biasanya, properti ini akan ditentukan secara otomatis melalui nama kelas, tetapi karena hubungan polimorfik dapat berupa salah satu dari beberapa kelas, Anda harus menentukan sendiri opsi inverseOf agar hubungan polimorfik memiliki semua fungsi yang sama seperti yang normal.

Mengelola Komentar di Posting

DSL yang sama yang berlaku untuk sumber daya juga berlaku untuk pengelolaan sumber daya terkait. Sekarang setelah kita mengatur hubungan antara posting dan komentar, ada beberapa cara untuk mengelola hubungan ini.

Anda dapat menambahkan komentar baru ke postingan:

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

Anda dapat menambahkan balasan ke komentar:

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

Anda dapat mengedit komentar:

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

Anda dapat menghapus komentar dari postingan:

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

Menampilkan Postingan dan Komentar

SDK dapat digunakan untuk menampilkan daftar posting yang diberi halaman, dan ketika sebuah posting diklik, posting tersebut dimuat di halaman baru dengan semua komentarnya:

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

Kueri di atas akan mengambil 10 posting terbaru, dan untuk mengoptimalkan, satu-satunya atribut yang dimuat adalah content .

Jika pengguna mengklik tombol untuk membuka halaman posting berikutnya, pengendali perubahan akan mengambil halaman berikutnya. Di sini kami juga menonaktifkan tombol jika tidak ada halaman berikutnya.

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

Saat tautan ke kiriman diklik, kami membuka halaman baru dengan memuat dan menampilkan kiriman dengan semua datanya, termasuk komentarnya—dikenal sebagai balasan—serta balasan atas balasan tersebut:

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

Memanggil .target() pada hubungan hasMany seperti post.replies() akan mengembalikan ActiveResource.Collection komentar yang telah dimuat dan disimpan secara lokal.

Perbedaan ini penting, karena post.replies().target().first() akan mengembalikan komentar pertama yang dimuat. Sebaliknya, post.replies().first() akan mengembalikan janji untuk satu komentar yang diminta dari GET /api/v1/posts/:id/replies .

Anda juga dapat meminta balasan untuk sebuah postingan secara terpisah dari permintaan untuk postingan itu sendiri, yang memungkinkan Anda untuk mengubah kueri Anda. Anda dapat membuat rantai pengubah seperti order , select , includes , where , perPage , page saat menanyakan hubungan hasMany seperti yang Anda bisa lakukan saat menanyakan sumber daya itu sendiri.

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

Memodifikasi Sumber Daya Setelah Diminta

Terkadang Anda ingin mengambil data dari server dan memodifikasinya sebelum menggunakannya. Misalnya, Anda dapat membungkus post.createdAt dalam objek moment() sehingga Anda dapat menampilkan tanggal yang ramah pengguna untuk pengguna tentang kapan postingan dibuat:

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

Kekekalan

Jika Anda bekerja dengan sistem manajemen status yang mendukung objek yang tidak dapat diubah, semua perilaku di ActiveResource.js dapat dibuat tidak berubah dengan mengonfigurasi pustaka sumber daya:

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

Melingkar Kembali: Menghubungkan Sistem Otentikasi

Sebagai penutup, saya akan menunjukkan kepada Anda bagaimana mengintegrasikan sistem otentikasi pengguna Anda ke dalam User ActiveResource Anda.

Pindahkan sistem otentikasi token Anda ke titik akhir API /api/v1/tokens . Ketika email dan kata sandi pengguna dikirim ke titik akhir ini, data pengguna yang diautentikasi ditambah token otorisasi akan dikirim sebagai tanggapan.

Buat kelas sumber daya Token milik User :

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

Tambahkan Token ke /src/resources/index.js .

Kemudian, tambahkan authenticate metode statis ke kelas sumber daya User Anda, dan hubungkan User dengan 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; } }

Metode ini menggunakan resourceLibrary.interface() , yang dalam hal ini adalah antarmuka JSON:API, untuk mengirim pengguna ke /api/v1/tokens . Ini valid: Titik akhir di JSON:API tidak mengharuskan satu-satunya tipe yang diposting ke dan darinya adalah tipe yang diberi nama. Jadi permintaannya adalah:

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

Responsnya adalah pengguna yang diautentikasi dengan token otentikasi yang disertakan:

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

Kemudian kita menggunakan token.id untuk mengatur header Authorization perpustakaan kita, dan mengembalikan pengguna, yang sama seperti meminta pengguna melalui User.find() seperti yang kita lakukan sebelumnya.

Sekarang, jika Anda memanggil User.authenticate(email, password) , Anda akan menerima pengguna yang diautentikasi sebagai tanggapan, dan semua permintaan di masa mendatang akan diotorisasi dengan token akses.

ActiveResource.js Mengaktifkan Pengembangan SDK JavaScript Cepat

Dalam tutorial ini, kami menjelajahi cara ActiveResource.js dapat membantu Anda membangun JavaScript SDK dengan cepat untuk mengelola sumber daya API Anda dan berbagai sumber daya terkait yang terkadang rumit. Anda dapat melihat semua fitur ini dan lebih banyak lagi didokumentasikan di README untuk ActiveResource.js.

Saya harap Anda menikmati kemudahan operasi ini dapat dilakukan, dan bahwa Anda akan menggunakan (dan mungkin bahkan berkontribusi) perpustakaan saya untuk proyek masa depan Anda jika sesuai dengan kebutuhan Anda. Dalam semangat open source, PR selalu diterima!