热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

接口没获取到就被使用_如何使用ThinkJS优雅的编写RESTfulAPI

RESTful是目前比较主流的一种用来设计和编排服务端API的一种规范。在RESTfulAPI中,所有的接口操作都被认为是对资源的CRUD,使用URI来
47d47aac13ce8ab56d4bdbd7d8ef8bc5.png

RESTful 是目前比较主流的一种用来设计和编排服务端 API 的一种规范。在 RESTful API 中,所有的接口操作都被认为是对资源的 CRUD,使用 URI 来表示操作的资源,请求方法表示具体的操作,响应状态码表示操作结果。之前使用 RESTful 的规范写过不少 API 接口,我个人认为它最大的好处就是帮助我们更好的去规划整理接口,如果还是按照以前根据需求来写接口的话接口的复用率不高不说,整个项目也会变得非常的杂乱。

文件即路由是 ThinkJS 的一大特色,比如 /user 这个路由等价于 /user/index,会对应到 src/controller/user.js 中的 indexAction 方法。那么就以 /user 这个 API 为例,在 ThinkJS 中要创建 RESTful 风格的 API 需要以下两个步骤:

  1. 运行命令 thinkjs controller user -r 会创建路由文件 src/controller/user.js
  2. 在 src/config/router.js 中使用自定义路由标记该路由为 RESTful 路由 //src/config/router.js module.exports = [ ['/user/:id?', 'rest'] ];

这样我们就完成了一个 RESTful 路由的初始化,这个资源的所有操作都会被映射成路由文件中对应请求方法的 Action 函数中,例如:

  • GET /user 获取用户列表,对应 getAction 方法
  • GET /user/:id 获取某个用户的详细信息,也对应getAction` 方法
  • POST /user 添加一位用户,对应 postAction 方法
  • PUT /user/:id 更新一位用户资料,对应 putAction 方法
  • DELETE /user/:id 删除一位用户,对应 deleteAction 方法

然而每个 RESTful 路由都需要去 router.js 中写一遍自定义路由未免过于麻烦。所以我写了一个中间件 think-router-rest,只需要在 Controller 文件中使用 _REST 静态属性标记一下就可以将其转换成 RESTful 路由了。

//src/controller/user.jsmodule.exports = class extends think.Controller { static get _REST() { return true; } getAction() {} postAction() {} putAction() {} deleteAction() {}}

简单的了解了一些入门知识之后,下面我就讲一些我平常开发 RESTful 接口时对我有帮助的一些知识点,希望对大家开发项目会有所帮助。

表结构梳理

拿到需求之后千万不要急着先敲键盘,一定要把表结构整理好。其实说是表结构,实际上就是对资源的整理。以 MySQL 为例,一般一类资源就会是一张表,比如 user 用户表,post 文章表等。当你把表罗列出来之后那么其实你的 RESTful 接口就已经七七八八了。比如你有一张 post 文章表,那么之后你的接口肯定会有:

  • GET /post 获取文章列表
  • GET /post/1 获取 id=1 的文章信息
  • POST /post 添加文章
  • PUT /post/1 修改 id=1 的文章信息
  • DELETE /post/1 删除 id=1 的文章

当然不是所有的事情都这么完美,有时候接口的操作可能五花八门,这种时候我们就要尽量的去思考接口行为的本质是什么。比如说我们要迁移文章给其它用户,这时候你就要思考它其实本质上就是修改 post 文章资源的 user_id 属性,最终还是会映射到 PUT /post/1 接口中来。

想清楚有哪些资源能帮助你更好的创建表,接下来就要想清楚资源之间的关系了,它能帮助你更好的创建表结构。一般资源之间会存在以下几类关系:

  • 一对一:如果一位 user 只能创建一篇 post 文章,则是一对一的关系。在 post 中可以使用 user_id 字段来关联对应的 user 数据,在 user 中也可以使用 post_id 来关联对应的文章数据。
  • 一对多:如果一位 user 能创建多篇 post 文章,则是一对多的关系。在 post 中可以使用 user_id 字段来关联对应的 user 数据。
  • 多对多:如果一位 user 可以创建多篇 post 文章,一篇 post 文章也可以有多位 user,则是多对多的关系。多对多关系没办法通过一个字段来表示,这时候为了描述清楚多对多的关系,就需要一张中间表 user_post,用来做 user 和 post 表的关系映射。表内部的 user_id 表示 user 表 ID,post_id 则表示 post 表对应数据 ID。

mysql> DESCRIBE user;+-------+--------------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+-------+--------------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || name | varchar(100) | YES | | NULL | |+-------+--------------+------+-----+---------+----------------+2 rows inset (0.01 sec)mysql> DESCRIBE post;+-------+---------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+-------+---------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || title | text | YES | | NULL | |+-------+---------+------+-----+---------+----------------+2 rows inset (0.00 sec)mysql> DESCRIBE user_post;+---------+---------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+---------+---------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || user_id | int(11) | NO | | NULL | || post_id | int(11) | NO | | NULL | |+---------+---------+------+-----+---------+----------------+3 rows inset (0.00 sec)

作为一款约定大于配置的 Web 框架,ThinkJS 默认规定了请求 RESTful 资源的时候,会根据当前资源 URI 找到对应的资源表,比如 GET /post 会找到 post 表。然后再进行查询的之后会进行自动的关联查询。例如当你在模型里标记了 post 和 user 是一对多的关系,且 post 表中存在 user_id 字段(也就是关联表表名 + _id),会自动关联获取到 project 对应的 user 数据。这在进行数据操作的时候会节省非常多的工作量。

登录登出

当我第一次写 RESTful API 的时候,我就碰到了这个难题,平常大家都是使用 /login, /logout 来表示登录和登出操作的,如何使用资源的形式来表达就成了问题。后来想了下登录操作中涉及到的资源其实就是登录后的 Token 凭证,本质上登录就是凭证的创建与获取,登出就是凭证的删除。

  • GET /token:获取凭证,用来判断是否登录
  • POST /token:创建凭证,用来进行登录操作
  • DELETE /token:删除凭证,用来进行登出操作

权限校验

我们平常写接口逻辑,其实会有很大一部分的工作量是用来做用户请求的处理。包括用户权限的校验和用户参数的校验处理等,这些逻辑其实和主业务场景没有太大的关系。为了将这些逻辑与主业务场景进行解耦,基于 Controller 层之上,ThinkJS 会存在一层 Logic 逻辑校验层。Logic 与 Controller 一一映射,并提供了一些常用的校验方法,我们可以将权限校验,参数校验,参数处理等逻辑放在这里,让 Controller 只做真正的业务逻辑。

在 Logic 和 Controller 中,都存在 __before() 魔术方法,当前 Controller 内所有的 Action 执行之前都会先执行 __before() 操作。利用这个特性,我们可以将一些通用的权限校验逻辑放在这里,比如最平常的登录判断逻辑,这样就不需要在每个地方都做判断了。

//src/logic/base.jsmodule.exports = class extends think.Logic { async __before() { //接口 CSRF 校验 if (!this.isCli && !this.isGet) { const referrer = this.referrer(true); if (!/^xxx.com$/.test(referrer)) { return this.fail('请不要在非其它网站中使用该接口!'); } } // 非登录接口需要做登录校验 const userInfo = await this.session('userInfo') || {}; if(think.isEmpty(userInfo) && !//(?:token).js/.test(this.__filename)) { return this.ctx.throw(401, 'UnAuthorized'); } }}//src/logic/user.jsconst Base = require('./base.js');module.exports = class extends Base {}

创建一个 Base 基类,所有的 Logic 通过继承该基类就都能享受到 CSRF 和登录校验了。

问:所有的请求都会实例化类,所以 contructor 本质上也会在所有的 Action 之前执行,那为什么还需要 __before() 魔术方法的存在呢?

答:constructor 构造函数虽然有前置执行的特性,但是无法在保证顺序的情况下执行异步操作。构造函数前是不能使用 async 标记的,而 __before() 是可以的,这也是它存在的原因。

善用继承

在 RESTful API 中,我们其实会发现很多资源是具有从属关系的。比如一个项目下的用户对应的文章,这句话中的三种资源 项目,用户 和 文章 就是从属关系。在从属关系中包括权限、数据操作等也都是具有从属关系的。比如说文章属于用户,非该用户的话自然是无法看到对应的文章的。而用户又从属于项目,其它项目的人是无法操作该项目下的用户的。这就是所谓的从属关系。

确立了从属关系之后我们会发现越到下级的资源在对其操作的时候要判断的权限就越多。以刚才的例子为例,如果说我们对项目资源进行操作的话,我们需要判断该用户是否在项目中。而如果要对项目下的用户文章进行操作的话,除了需要判断用户是否在项目中,还需要判断该文章是否是当前用户的。

在这个例子中我们可以发现:资源关系从属的话权限校验也会是从属关系,从属关系中级别越深的资源需要判断的权限越多。面向对象语言中,继承是一个比较重要的功能,它最大的好处就是能帮助我们进行逻辑的复用。通过继承,我们能直接在子资源中复用父资源的校验逻辑,避免重复劳动。

//src/logic/base.jsmodule.exports = class extends think.Logic { async __before() { const userInfo = this.session('userInfo') || {}; this.userInfo = this.ctx.state.userInfo = userInfo; if(think.isEmpty(userInfo)) { return this.ctx.throw(401); } }}//src/logic/project/base.jsconst Base = require('../base.js');module.exports = class extends Base {async __before() { await super.__before(); const {team_id} = this.get(); const {id: user_id} = this.userInfo; const permission = await this.model('team_user').where({team_id, user_id}).find(); const {controller} = this.ctx; // 团队接口中只有普通用户只有权限调用获取邀请链接详细信息和接受邀请链接两个接口 if(controller !== 'team/invitation' && (this.isGet && !this.id)) { if(think.isEmpty(permission)) { return this.fail('你没有权限操作该团队'); } } this.userInfo.role_id = permission.role_id; }}//src/logic/project/user/base.jsconst Base = require('../base');module.eports = class extends Base { async __before() { await super.__before(); const {role_id} = this.userInfo; if(!global.EDITOR.is(role_id)) { return this.fail('你没有权限操作该文章'); } }}

通过创建三个 Base 基类,我们将权限校验进行了合理的拆分同时又能保证校验的完整性。同级别的路由只要继承当前层级的 Base 基类就能享受到通用的校验逻辑。

  • /project 路由对应的 Logic 因为继承了 src/logic/base.js 所以实现了登录校验。
  • /project/1/user 路由对应的 Logic 因为继承了 src/logic/project/base.js 所以实现了登录校验以及是否在是项目成员的校验。
  • /project/1/user/1/post 路由对应的 Logic 因为继承了 src/logic/project/user/base.js 所以实现了登录校验、项目成员校验以及项目成员权限的校验。

瞧,套娃就这么简单!

数据库操作

从属的资源在表结构上也有一定的反应。还是以之前的项目、用户和文章为例,一般来说你的文章表里会存在 project_id 和 user_id 两个关联字段来表示文章与用户和项目资源的关系(简单假设都是一对多的关系)。那么这时候实际上你对项目下的文章操作实际上都需要传入 project_id 和 user_id 这两个 WHERE 条件。

ThinkJS 内部使用 think-model 来进行 SQL 数据库操作。它有一个特性是支持链式调用,我们可以这样写一个查询操作。

//src/controller/project/user/post.jsmodule.exports = class extends think.Controller { async indexAction() { const ret = await this.model('post').where({project_id: 1}).where({user_id: 2}).select(); return this.success(ret); }}

利用这个特性,我们可以对操作进行优化,在 constructor 的时候将当前 Controller 下的通用 WHERE 条件 project_id 和 user_id 传入。这样我们在其它的 Action 操作的时候就不用每个都传一变了,同时也一定规避了可能会漏传限制条件的风险。

//src/controller/project/user/post.jsmodule.exports = class extends think.Controller { constructor(ctx) { super(ctx); const {project_id, user_id} = this.get(); this.modelInstance = this.model('post').where({project_id, user_id}); } async getAction() { const ret = await this.modelInstance.select(); return this.success(ret); }}

后记

RESTful API 除了以上说的一些特性之外,它对响应状态码、接口的版本也有一定的规范定义。像 Github 这种 RESTful 实现比较好的网站还会实现 Hypermedia API 规范,在每个接口中会返回操作其它资源时需要的 RESTful 路由地址,方便调用者进行链式调用。

当然 RESTful 只是实现 API 的一种规范,还有其它的一些实现规范,比如 GraphQL。关于 GraphQL 可以看看之前的文章《GraphQL 基础实践》,这里就不多做补充了。



推荐阅读
  • 在Android 4.4系统中,通过使用 `Intent` 对象并设置动作 `ACTION_GET_CONTENT` 或 `ACTION_OPEN_DOCUMENT`,可以从相册中选择图片并获取其路径。具体实现时,需要为 `Intent` 添加相应的类别,并处理返回的 Uri 以提取图片的文件路径。此方法适用于需要从用户相册中选择图片的应用场景,能够确保兼容性和用户体验。 ... [详细]
  • Vue应用预渲染技术详解与实践 ... [详细]
  • 在腾讯云服务器上部署Nginx的详细指南中,首先需要确保安装必要的依赖包。如果这些依赖包已安装,可直接跳过此步骤。具体命令包括 `yum -y install gcc gcc-c++ wget net-tools pcre-devel zlib-devel`。接下来,本文将详细介绍如何下载、编译和配置Nginx,以确保其在腾讯云服务器上顺利运行。此外,还将提供一些优化建议,帮助用户提升Nginx的性能和安全性。 ... [详细]
  • 本文探讨了Android系统中支持的图像格式及其在不同版本中的兼容性问题,重点涵盖了存储、HTTP传输、相机功能以及SparseArray的应用。文章详细分析了从Android 10 (API 29) 到Android 11 的存储规范变化,并讨论了这些变化对图像处理的影响。此外,还介绍了如何通过系统升级和代码优化来解决版本兼容性问题,以确保应用程序在不同Android版本中稳定运行。 ... [详细]
  • 在处理大图片时,PHP 常常会遇到内存溢出的问题。为了避免这种情况,建议避免使用 `setImageBitmap`、`setImageResource` 或 `BitmapFactory.decodeResource` 等方法直接加载大图。这些函数在处理大图片时会消耗大量内存,导致应用崩溃。推荐采用分块处理、图像压缩和缓存机制等策略,以优化内存使用并提高处理效率。此外,可以考虑使用第三方库如 ImageMagick 或 GD 库来处理大图片,这些库提供了更高效的内存管理和图像处理功能。 ... [详细]
  • 作为软件工程专业的学生,我深知课堂上教师讲解速度之快,很多时候需要课后自行消化和巩固。因此,撰写这篇Java Web开发入门教程,旨在帮助初学者更好地理解和掌握基础知识。通过详细记录学习过程,希望能为更多像我一样在基础方面还有待提升的学员提供有益的参考。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 本文详细介绍了使用 Python 进行 MySQL 和 Redis 数据库操作的实战技巧。首先,针对 MySQL 数据库,通过 `pymysql` 模块展示了如何连接和操作数据库,包括建立连接、执行查询和更新等常见操作。接着,文章深入探讨了 Redis 的基本命令和高级功能,如键值存储、列表操作和事务处理。此外,还提供了多个实际案例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
  • 手指触控|Android电容屏幕驱动调试指南
    手指触控|Android电容屏幕驱动调试指南 ... [详细]
  • 在Kohana 3框架中,实现最优的即时消息显示方法是许多开发者关注的问题。本文将探讨如何高效、优雅地展示flash消息,包括最佳实践和技术细节,以提升用户体验和代码可维护性。 ... [详细]
  • 本文详细探讨了MySQL数据库实例化参数的优化方法及其在实例查询中的应用。通过具体的源代码示例,介绍了如何高效地配置和查询MySQL实例,为开发者提供了有价值的参考和实践指导。 ... [详细]
  • HBase Java API 进阶:过滤器详解与应用实例
    本文详细探讨了HBase 1.2.6版本中Java API的高级应用,重点介绍了过滤器的使用方法和实际案例。首先,文章对几种常见的HBase过滤器进行了概述,包括列前缀过滤器(ColumnPrefixFilter)和时间戳过滤器(TimestampsFilter)。此外,还详细讲解了分页过滤器(PageFilter)的实现原理及其在大数据查询中的应用场景。通过具体的代码示例,读者可以更好地理解和掌握这些过滤器的使用技巧,从而提高数据处理的效率和灵活性。 ... [详细]
  • 利用树莓派畅享落网电台音乐体验
    最近重新拾起了闲置已久的树莓派,这台小巧的开发板已经沉寂了半年多。上个月闲暇时间较多,我决定将其重新启用。恰逢落网电台进行了改版,回忆起之前在树莓派论坛上看到有人用它来播放豆瓣音乐,便萌生了同样的想法。通过一番调试,终于实现了在树莓派上流畅播放落网电台音乐的功能,带来了全新的音乐享受体验。 ... [详细]
  • Spring框架中的面向切面编程(AOP)技术详解
    面向切面编程(AOP)是Spring框架中的关键技术之一,它通过将横切关注点从业务逻辑中分离出来,实现了代码的模块化和重用。AOP的核心思想是将程序运行过程中需要多次处理的功能(如日志记录、事务管理等)封装成独立的模块,即切面,并在特定的连接点(如方法调用)动态地应用这些切面。这种方式不仅提高了代码的可维护性和可读性,还简化了业务逻辑的实现。Spring AOP利用代理机制,在不修改原有代码的基础上,实现了对目标对象的增强。 ... [详细]
author-avatar
清晨竹林9_877
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有