个性化阅读
专注于IT技术分析

ActiveResource.js:快速为你的JSON API构建强大的JavaScript SDK

本文概述

你的公司刚刚启动了它的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端点的性质进行一些假设。例如, 如果你具有产品资源, 则该资源对应于/ 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。我将所有ActiveResources放在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等), 而是将本教程抽象为仅通过ActiveResources与你的API进行交互。

第一资源:用户

我们将首先创建一个用户资源来管理CMS的用户。

首先, 我们创建一个具有一些属性的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.headers设置为具有带有accessToken的Authorization标头, 因此我的ResourceLibrary以后的所有请求都得到了授权。

下一部分将介绍如何仅使用User资源类对用户进行身份验证和设置访问令牌。

认证的最后一步是对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.attributes()来获取所有属性。

添加资源索引

在添加与User类相关的更多资源(例如通知)之前, 我们应该添加文件src / resources / index.js, 该文件将对所有资源进行索引。这有两个好处:

  1. 通过允许我们在一个import语句中分解多个资源的src / resources而不是使用多个import语句, 将清理导入。
  2. 它将通过在每个资源库上调用library.createResource来初始化我们将在ResourceLibrary上创建的所有资源, 这对于ActiveResource.js建立关系是必需的。
// /src/resources/index.js

import User from './User';

export {
  User
};

添加相关资源

现在, 我们为用户创建一个相关资源, 即通知。首先创建一个Notification类, 它属于User类:

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

现在, 一旦我们从身份验证中恢复了用户身份, 我们就可以加载并显示其所有通知:

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()、. 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:

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请求, 其中仅包含对资源所做的更改, 而不是不必要地将资源的全部属性和关系提交给服务器。

你可以使用属性声明将跟踪的属性添加到资源。让我们跟踪对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。

我们也需要在Post类中添加一行:

// /src/resources/Post.js

class Post extends library.Base {
  static define() {
    /* ... */

    this.hasMany('replies', {
      as: 'resource', className: 'Comment', inverseOf: 'resource'
    });
  }
}

传递给hasMany定义以进行回复的inverseOf选项指示该关系与资源的多态属植物归属定义相反。关系之间的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个最新的帖子, 并且为了进行优化, 唯一加载的属性是它们的内容。

如果用户单击按钮以转到帖子的下一页, 则更改处理程序将检索下一页。如果没有下一页, 在这里我们也会禁用该按钮。

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关系时, 可以链接修饰符, 例如order, select, includes, perPage, page等, 就像查询资源本身一样。

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。当用户的电子邮件和密码发送到此端点时, 将作为响应发送经过身份验证的用户数据以及授权令牌。

创建属于用户的令牌资源类:

// /src/resources/Token.js

import library from './library';

class Token extends library.Base {
  static define() {
    this.belongsTo('user');
  }
}

export default library.createResource(Token);

将令牌添加到/src/resources/index.js。

然后, 将静态方法authenticate添加到你的User资源类, 并将User与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;
  }
}

此方法使用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(电子邮件, 密码), 你将收到经过身份验证的用户作为响应, 并且以后所有的请求都将使用访问令牌进行授权。

ActiveResource.js支持快速JavaScript SDK开发

在本教程中, 我们探讨了ActiveResource.js可以帮助你快速构建JavaScript SDK来管理API资源及其各种(有时是复杂的)相关资源的方法。你可以查看所有这些功能以及ActiveResource.js的README中记录的更多功能。

希望你能轻松完成这些操作, 并希望在满足你需要的情况下将我的库用于未来的项目(甚至为之贡献)。本着开放源代码的精神, 始终欢迎PR!

赞(0)
未经允许不得转载:srcmini » ActiveResource.js:快速为你的JSON API构建强大的JavaScript SDK

评论 抢沙发

评论前必须登录!