ActiveResource.js: Membangun SDK JavaScript yang Kuat untuk API JSON Anda, Cepat
Diterbitkan: 2022-03-11Perusahaan 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:
- “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. - “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:
- 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. - Ini akan menginisialisasi semua sumber daya di
ResourceLibrary
yang akan kita buat dengan memanggillibrary.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!