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 总是受欢迎的!