开奖啦!(文章末尾)-程序员宅基地

技术标签: java  编程语言  mysql  数据库  javascript  

奇技 · 指南

今天小编为大家分享一篇关于如何使用 ThinkJS 优雅的编写 RESTful API的文章。希望能对大家有所帮助。

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-rest1,只需要在 Controller 文件中使用 _REST 静态属性标记一下就可以将其转换成 RESTful 路由了。

//src/controller/user.js
module.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 in set (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 in set (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 in set (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 会存在一层 Logic2 逻辑校验层。Logic 与 Controller 一一映射,并提供了一些常用的校验方法,我们可以将权限校验,参数校验,参数处理等逻辑放在这里,让 Controller 只做真正的业务逻辑。

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

//src/logic/base.js
module.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.js
const Base = require('./base.js');
module.exports = class extends Base {}

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

问:所有的请求都会实例化类,所以 contructor 本质上也会在所有的 Action 之前执行,那为什么还需要 __before() 魔术方法的存在呢?答:before在保证顺序下执行异步操作

善用继承

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

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

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

//src/logic/base.js
module.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.js
const 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.js
const 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.js
module.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.js
module.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 基础实践》,这里就不多做补充了。

文内链接

  1. https://github.com/thinkjs/think-router-rest

  2. https://thinkjs.org/zh-cn/doc/3.0/logic.html

  3. https://thinkjs.org/zh-cn/doc/3.0/controller.html#toc-083

国庆小长假过的太快了

虽然万分不舍,但也要和假期说再见了

国庆期间的抽奖活动

获得礼品的同学见下图

在公众号后台留下你的联系方式

晚点小编会安排寄出礼品哦

5天内有效!

往期精彩回顾

Golang 项目布局浅析

一种通过云配置处理应用权限弹框的方案

360Stack裸金属服务器部署实践

360技术公众号

技术干货|一手资讯|精彩活动

扫码关注我们

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qihoo_tech/article/details/109040092

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法