ActiveResource.js:為您的 JSON API 快速構建強大的 JavaScript SDK

已發表: 2022-03-11

你的公司剛剛推出了它的 API,現在想圍繞它建立一個用戶社區。 您知道您的大多數客戶將使用 JavaScript,因為您的 API 提供的服務使客戶更容易構建 Web 應用程序,而不是自己編寫所有東西——Twilio 就是一個很好的例子。

您還知道,儘管您的 RESTful API 可能很簡單,但用戶將希望放入一個 JavaScript 包,該包將為他們完成所有繁重的工作。 他們不想學習您的 API 並自己構建他們需要的每個請求。

所以你正在圍繞你的 API 構建一個庫。 或者,您可能只是在為與您自己的內部 API 交互的 Web 應用程序編寫狀態管理系統。

無論哪種方式,您都不想在每次 CRUD 一個 API 資源時一遍又一遍地重複自己,或者更糟糕的是,CRUD 與這些資源相關的資源。 從長遠來看,這不利於管理不斷增長的 SDK,也不是很好地利用您的時間。

相反,您可以使用 ActiveResource.js,這是一個用於與 API 交互的 JavaScript ORM 系統。 我創建它是為了滿足我們對項目的需求:用盡可能少的行創建 JavaScript SDK。 這為我們和我們的開發者社區帶來了最大的效率。

它基於 Ruby on Rails 的簡單 ActiveRecord ORM 背後的原理。

JavaScript SDK 原則

有兩個 Ruby on Rails 理念指導了 ActiveResource.js 的設計:

  1. “約定優於配置”:對 API 端點的性質做出一些假設。 例如,如果您有一個Product資源,它對應於/products端點。 這樣就不會花費時間重複配置每個 SDK 對 API 的請求。 開發人員可以在幾分鐘而不是幾小時內將具有復雜 CRUD 查詢的新 API 資源添加到您不斷增長的 SDK 中。
  2. “讚美優美的代碼:” Rails 的創建者 DHH 說得最好——優美的代碼本身就是一件很棒的事情。 ActiveResource.js 有時將醜陋的請求包裝在漂亮的外觀中。 您不再需要編寫自定義代碼來添加過濾器和分頁,並將嵌套在關係上的關係包含到 GET 請求中。 您也不必構造 POST 和 PATCH 請求來更改對象的屬性並將它們發送到服務器進行更新。 相反,只需在 ActiveResource 上調用一個方法:不再使用 JSON 來獲取您想要的請求,只需為下一個請求再次執行此操作。

在我們開始之前

需要注意的是,在撰寫本文時,ActiveResource.js 僅適用於根據 JSON:API 標準編寫的 API。

如果您不熟悉 JSON:API 並想繼續學習,有很多用於創建 JSON:API 服務器的優秀庫。

也就是說,ActiveResource.js 更像是一種 DSL,而不是一個特定 API 標準的包裝器。 它用於與您的 API 交互的接口可以擴展,因此以後的文章可能會介紹如何將 ActiveResource.js 與您的自定義 API 一起使用。

設置東西

首先,在您的項目中安裝active-resource

 yarn add active-resource

第一步是為您的 API 創建一個ResourceLibrary 。 我將把我所有的ActiveResource放在src/resources文件夾中:

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

createResourceLibrary唯一需要的參數是 API 的根 URL。

我們將創造什麼

我們將為內容管理系統 API 創建一個 JavaScript SDK 庫。 這意味著會有用戶、帖子、評論和通知。

用戶將能夠閱讀、創建和編輯帖子; 閱讀、添加和刪除評論(對帖子或其他評論),並接收新帖子和評論的通知。

我不會使用任何特定的庫來管理視圖(React、Angular 等)或狀態(Redux 等),而是將教程抽象為僅通過ActiveResource與您的 API 交互。

第一個資源:用戶

我們將首先創建一個User資源來管理 CMS 的用戶。

首先,我們創建一個具有一些attributesUser資源類:

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

現在讓我們假設您有一個身份驗證端點,一旦用戶提交了他們的電子郵件和密碼,它就會返回一個訪問令牌和用戶的 ID。 這個端點由一些函數requestToken管理。 獲得經過身份驗證的用戶 ID 後,您希望加載所有用戶的數據:

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

我將library.headers設置為具有帶有accessTokenAuthorization標頭,因此我的ResourceLibrary的所有未來請求都被授權。

後面的部分將介紹如何僅使用User資源類對用戶進行身份驗證和設置訪問令牌。

authenticate的最後一步是對User.find(id)的請求。 這將向/api/v1/users/:id發出請求,響應可能類似於:

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

來自authenticate的響應將是User類的一個實例。 如果您想在應用程序的某處顯示它們,您可以從這裡訪問經過身份驗證的用戶的各種屬性。

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

每個屬性名稱都將變成駝峰式,以符合 JavaScript 的典型標準。 您可以直接獲取它們中的每一個作為user對象的屬性,或者通過調用user.attributes()獲取所有屬性。

添加資源索引

在我們添加更多與User類相關的資源(例如通知)之前,我們應該添加一個文件src/resources/index.js ,它將索引我們所有的資源。 這有兩個好處:

  1. 它將通過允許我們在一個導入語句中為多個資源解構src/resources來清理我們的導入,而不是使用多個導入語句。
  2. 它將通過在每個資源上調用library.createResource來初始化我們將創建的ResourceLibrary上的所有資源,這是 ActiveResource.js 建立關係所必需的。
 // /src/resources/index.js import User from './User'; export { User };

添加相關資源

現在讓我們為User創建一個相關的資源,一個Notification 。 首先創建一個belongsTo User類的Notification類:

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

然後我們將其添加到資源索引中:

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

然後,將通知與User類相關聯:

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

現在,一旦我們從authenticate中取回用戶,我們就可以加載並顯示其所有通知:

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

我們還可以在對經過身份驗證的用戶的原始請求中包含通知:

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

這是 DSL 中可用的眾多選項之一。

審查 DSL

讓我們討論一下僅從我們迄今為止編寫的代碼中已經可以請求的內容。

您可以查詢一組用戶或單個用戶。

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

您可以使用可鏈接的關係方法修改查詢:

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

請注意,您可以使用任意數量的鍊式修飾符組成查詢,並且您可以使用.all() () 、 .first( .first().last().each()結束查詢。

你可以在本地建立一個用戶,也可以在服務器上建立一個:

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

擁有持久用戶後,您可以將更改發送到它以保存在服務器上:

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

您也可以從服務器中刪除它:

 await user.destroy();

這個基本的 DSL 也擴展到相關資源,我將在本教程的其餘部分進行演示。 現在我們可以快速應用 ActiveResource.js 來創建 CMS 的其餘部分:帖子和評論。

創建帖子

Post創建一個資源類並將其與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'); } }

Post也添加到資源索引中:

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

然後將Post資源綁定到一個表單中,供用戶創建和編輯帖子。 當用戶第一次訪問創建新帖子的表單時,會構建一個Post資源,每次表單更改時,我們都會將更改應用到Post

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

接下來,向表單添加一個onSubmit回調以將帖子保存到服務器,並在保存嘗試失敗時處理錯誤:

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

編輯帖子

保存帖子後,它將作為服務器上的資源鏈接到您的 API。 您可以通過調用persisted來判斷資源是否在服務器上持久化:

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

對於持久化資源,ActiveResource.js 支持臟屬性,因為您可以檢查資源的任何屬性是否已從服務器上的值更改。

如果您在持久化資源上調用save() ,它將發出僅包含對資源所做更改的PATCH請求,而不是不必要地將資源的整個屬性和關係集提交給服務器。

您可以使用attributes聲明將跟踪的屬性添加到資源。 讓我們跟踪post.content的變化:

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

現在,使用服務器持久化的帖子,我們可以編輯帖子,當點擊提交按鈕時,將更改保存到服務器。 如果尚未進行任何更改,我們還可以禁用提交按鈕:

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

如果我們想更改與帖子關聯的用戶,可以使用類似post.user()類的單一關係管理方法:

 await post.updateUser(user);

這相當於:

 await post.update({ user });

評論資源

現在創建一個資源類Comment並將其與Post相關聯。 請記住我們的要求,即評論可以是對帖子或其他評論的回應,因此評論的相關資源是多態的:

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

確保也將Comment添加到/src/resources/index.js

我們還需要在Post類中添加一行:

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

傳遞給replieshasMany定義的inverseOf選項​​表明該關係與resource的多態belongsTo定義相反。 在進行關係之間的操作時,經常使用關係的inverseOf屬性。 通常,此屬性將通過類名自動確定,但由於多態關係可以是多個類之一,因此您必須自己定義inverseOf選項​​,以使多態關係具有與正常關係相同的功能。

管理帖子的評論

適用於資源的 DSL 也適用於相關資源的管理。 現在我們已經建立了帖子和評論之間的關係,我們可以通過多種方式來管理這種關係。

您可以在帖子中添加新評論:

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

您可以在評論中添加回复:

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

您可以編輯評論:

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

您可以從帖子中刪除評論:

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

顯示帖子和評論

SDK 可用於顯示帖子的分頁列表,當點擊帖子時,帖子會加載到新頁面上,其中包含所有評論:

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

上面的查詢將檢索 10 個最近的帖子,並且為了優化,加載的唯一屬性是它們的content

如果用戶單擊按鈕轉到下一頁的帖子,則更改處理程序將檢索下一頁。 如果沒有下一頁,我們也會在此處禁用該按鈕。

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

當點擊帖子的鏈接時,我們會通過加載和顯示帖子及其所有數據來打開一個新頁面,包括其評論(稱為回复)以及對這些回复的回复:

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

hasMany關係(如post.replies() .target()返回已在本地加載和存儲的評論的ActiveResource.Collection

這種區別很重要,因為post.replies().target().first()將返回加載的第一個評論。 相反, post.replies().first()將為從GET /api/v1/posts/:id/replies請求的一條評論返回一個承諾。

您還可以單獨請求帖子的回复,而不是請求帖子本身,這樣您就可以修改查詢。 在查詢hasMany關係時,您可以像查詢資源本身一樣鏈接修飾符,如orderselectincludeswhereperPagepage

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

請求資源後修改資源

有時您想從服務器獲取數據並在使用之前對其進行修改。 例如,您可以將post.createdAt包裝在一個moment()對像中,這樣您就可以為用戶顯示一個用戶友好的日期時間,了解帖子的創建時間:

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

不變性

如果您使用支持不可變對象的狀態管理系統,則可以通過配置資源庫使 ActiveResource.js 中的所有行為不可變:

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

循環往復:鏈接認證系統

最後,我將向您展示如何將您的用戶身份驗證系統集成到您的User ActiveResource中。

將您的令牌身份驗證系統移動到 API 端點/api/v1/tokens 。 當用戶的電子郵件和密碼發送到此端點時,將發送經過身份驗證的用戶數據和授權令牌作為響應。

創建一個屬於UserToken資源類:

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

然後,向您的User資源類添加一個靜態方法authenticate ,並將UserToken相關聯:

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

此方法使用resourceLibrary.interface() (在本例中為 JSON:API 接口)將用戶發送到/api/v1/tokens 。 這是有效的:JSON:API 中的端點不要求發佈到它和從它發布的唯一類型是那些以它命名的類型。 所以請求將是:

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

響應將是經過身份驗證的用戶,其中包含身份驗證令牌:

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

然後我們使用token.id設置我們庫的Authorization頭,並返回用戶,這與我們之前通過User.find()請求用戶相同。

現在,如果您調用User.authenticate(email, password) ,您將收到一個經過身份驗證的用戶作為響應,並且所有未來的請求都將使用訪問令牌進行授權。

ActiveResource.js 支持快速 JavaScript SDK 開發

在本教程中,我們探索了 ActiveResource.js 可以幫助您快速構建 JavaScript SDK 來管理您的 API 資源及其各種(有時是複雜的)相關資源的方式。 您可以在 ActiveResource.js 的自述文件中查看所有這些功能以及更多文檔。

我希望您喜歡這些操作可以輕鬆完成,並且如果它適合您的需要,您將使用(甚至可能貢獻)我的庫用於您未來的項目。 本著開源精神,PR 總是受歡迎的!