ActiveResource.js:JSONAPI用の強力なJavaScriptSDKを高速で構築

公開: 2022-03-11

あなたの会社はAPIを立ち上げたばかりで、その周りにユーザーのコミュニティを構築したいと考えています。 APIが提供するサービスにより、顧客がすべてを自分で作成するのではなく、Webアプリケーションを簡単に構築できるため、ほとんどの顧客がJavaScriptで作業することをご存知でしょう。Twilioはその良い例です。

また、RESTful APIと同じくらい単純な場合でも、ユーザーはJavaScriptパッケージをドロップして、すべての面倒な作業を実行したいと思うでしょう。 彼らはあなたのAPIを学び、彼ら自身が必要とする各リクエストを構築することを望んでいません。

つまり、APIを中心にライブラリを構築しているのです。 あるいは、独自の内部APIと相互作用するWebアプリケーションの状態管理システムを作成しているだけかもしれません。

いずれにせよ、APIリソースの1つをCRUDするたびに、またはさらに悪いことに、それらのリソースに関連するリソースをCRUDするたびに、何度も繰り返したくないでしょう。 これは、成長するSDKを長期的に管理するのには適していません。また、時間を有効に活用することもできません。

代わりに、APIと対話するためのJavaScriptORMシステムであるActiveResource.jsを使用できます。 プロジェクトのニーズを満たすために作成しました。JavaScriptSDKをできるだけ少ない行で作成するためです。 これにより、私たちと開発者コミュニティの効率が最大になりました。

これは、RubyonRailsの単純なActiveRecordORMの背後にある原則に基づいています。

JavaScriptSDKの原則

ActiveResource.jsの設計を導いた2つのRubyonRailsのアイデアがあります。

  1. 「設定より規約」: APIのエンドポイントの性質についていくつかの仮定を立てます。 たとえば、 Productリソースがある場合、それは/productsエンドポイントに対応します。 そうすれば、APIに対するSDKの各リクエストを繰り返し構成するために時間が費やされることはありません。 開発者は、複雑なCRUDクエリを含む新しいAPIリソースを、数時間ではなく数分で成長するSDKに追加できます。
  2. 「美しいコードを称賛する:」 Railsの作成者であるDHHは、それを最もよく言っています。それ自体のために、美しいコードには素晴らしいものがあります。 ActiveResource.jsは、時々醜いリクエストを美しい外観でラップします。 フィルタとページ付けを追加し、GETリクエストへの関係にネストされた関係を含めるために、カスタムコードを作成する必要がなくなりました。 また、オブジェクトのプロパティに変更を加えて更新のためにサーバーに送信するPOSTおよびPATCHリクエストを作成する必要もありません。 代わりに、ActiveResourceのメソッドを呼び出すだけです。必要なリクエストを取得するためにJSONをいじくり回す必要はなく、次のリクエストのためにもう一度実行する必要があります。

始める前に

この記事の執筆時点では、ActiveResource.jsはJSON:API標準に従って作成されたAPIでのみ機能することに注意してください。

JSON:APIに慣れておらず、フォローしたい場合は、JSON:APIサーバーを作成するための優れたライブラリが多数あります。

とはいえ、ActiveResource.jsは、特定のAPI標準のラッパーというよりもDSLです。 APIとのやり取りに使用するインターフェースを拡張できるため、今後の記事では、カスタムAPIでActiveResource.jsを使用する方法について説明する可能性があります。

セットアップ

まず、プロジェクトにactive-resourceをインストールします。

 yarn add active-resource

最初のステップは、APIのResourceLibraryを作成することです。 すべてのActiveResourcesrc/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用のJavaScriptSDKライブラリを作成します。 つまり、ユーザー、投稿、コメント、通知があります。

ユーザーは投稿を読んだり、作成したり、編集したりできるようになります。 コメントを読んだり、追加したり、(投稿や他のコメントに)削除したり、新しい投稿やコメントの通知を受け取ったりします。

ビュー(React、Angularなど)または状態(Reduxなど)を管理するために特定のライブラリを使用するのではなく、 ActiveResourceを介してAPIとのみ対話するようにチュートリアルを抽象化します。

最初のリソース:ユーザー

まず、CMSのユーザーを管理するためのUserリソースを作成します。

まず、いくつかのattributesを持つUserリソースクラスを作成します。

 // /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.headersaccessTokenを含むAuthorizationヘッダーを設定して、 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の一般的な標準に適合するように、camelCasedになります。 それぞれをuserオブジェクトのプロパティとして直接取得することも、 user.attributes()を呼び出してすべての属性を取得することもできます。

リソースインデックスの追加

通知など、 Userクラスに関連するリソースを追加する前に、すべてのリソースにインデックスを付けるファイルsrc/resources/index.jsを追加する必要があります。 これには2つの利点があります。

  1. 複数のインポートステートメントを使用する代わりに、1つのインポートステートメントで複数のリソースのsrc/resourcesを分解できるようにすることで、インポートをクリーンアップします。
  2. ActiveResource.jsが関係を構築するために必要な、それぞれでlibrary.createResourceを呼び出すことにより、作成するResourceLibrary上のすべてのリソースを初期化します。
 // /src/resources/index.js import User from './User'; export { User };

関連リソースの追加

次に、 Userに関連するリソースであるNotificationを作成しましょう。 まず、 UserクラスにbelongsTo 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で利用できる多くのオプションの1つです。

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

任意の数の連鎖修飾子を使用してクエリを作成でき、 .last() 、または.each() .first() .all()を終了できることに注意してください。

ローカルでユーザーを作成することも、サーバー上でユーザーを作成することもできます。

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

必ず/src/resources/index.jsにもCommentを追加してください。

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プロパティは、リレーションシップ間の操作を行うときに頻繁に使用されます。 通常、このプロパティはクラス名によって自動的に決定されますが、ポリモーフィック関係は複数のクラスの1つである可能性があるため、ポリモーフィック関係が通常の関係とすべて同じ機能を持つようにするには、 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() ); }); }

.target()のようなhasMany関係でpost.replies() ()を呼び出すと、ローカルにロードおよび保存されたコメントのActiveResource.Collectionが返されます。

post.replies().target().first()はロードされた最初のコメントを返すため、この区別は重要です。 対照的に、 post.replies().first()は、 GET /api/v1/posts/:id/replies repliesからリクエストされた1つのコメントのpromiseを返します。

投稿自体のリクエストとは別に、投稿への返信をリクエストすることもできます。これにより、クエリを変更できます。 リソース自体をクエリするときと同じように、 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.createdAtmoment()オブジェクトでラップして、投稿が作成された日時に関するユーザーにとってわかりやすい日時を表示できます。

 // /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に移動します。 ユーザーの電子メールとパスワードがこのエンドポイントに送信されると、認証されたユーザーのデータと認証トークンが応答として送信されます。

Userに属するTokenリソースクラスを作成します:

 // /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に追加します。

次に、静的メソッドauthenticateUserリソースクラスに追加し、 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は迅速なJavaScriptSDK開発を可能にします

このチュートリアルでは、ActiveResource.jsを使用してJavaScript SDKをすばやく構築し、APIリソースとそのさまざまな(場合によっては複雑な)関連リソースを管理する方法について説明しました。 これらのすべての機能と、ActiveResource.jsのREADMEに詳細が記載されています。

これらの操作が簡単に行えることを楽しんでいただければ幸いです。また、必要に応じて、将来のプロジェクトで私のライブラリを使用する(そしておそらく貢献する)ことを願っています。 オープンソースの精神で、PRはいつでも歓迎です!