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

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

post如何获取到referrer_如何使用 ThinkJS 优雅的编写 RESTful API相关推荐

  1. python flask restful api_python之restful api(flask)获取数据

    需要用到谷歌浏览器的扩展程序 Advanced Rest Client进行模拟请求 1.直接上代码 from flask import Flask from flask import request ...

  2. vue根据url获取内容axios_使用Vue.js和Axios从第三方API获取数据 — SitePoint

    更多的往往不是,建立你的JavaScript应用程序时,你会想把数据从远程源或消耗一个[ API ](https:/ /恩.维基百科.org /维基/ application_programming_ ...

  3. 前端websocket获取数据后需要存本地吗_是什么让我放弃了restful api?了解清楚后我全面拥抱GraphQL...

    GraphQL初步认识 背景 REST作为一种现代网络应用非常流行的软件架构风格,自从Roy Fielding博士在2000年他的博士论文中提出来到现在已经有了20年的历史.它的简单易用性,可扩展性, ...

  4. 前端websocket获取数据后需要存本地吗_是什么让我放弃了Restful API?了解清楚后我全面拥抱GraphQL!...

    背景 REST作为一种现代网络应用非常流行的软件架构风格,自从Roy Fielding博士在2000年他的博士论文中提出来到现在已经有了20年的历史.它的简单易用性,可扩展性,伸缩性受到广大Web开发 ...

  5. python 获取qq群成员信息_用Python编写工具获取QQ群成员的昵称和号码,使用,及,小...

    使用火狐浏览器进行数据的抓取,火狐浏览器对json比较友好 进入QQ群官网: 登录后,点击成员管理: 先随机选择一个群,抓取一下信息,找一下规律 打开浏览器控制台,重新刷新网页: 这是我们想要的一些信 ...

  6. ios 高德获取定位_更新日志-iOS 定位SDK | 高德地图API

    高德地图iOS 定位 SDK V2.6.7            2020-08-28 1.适配iOS14定位权限:新增"模糊定位"权限下的兼容策略: 2.修复bug,提升性能和稳 ...

  7. python 收发微信之二:获取微信上行信息(利用 flask 框架实现 Web API,获取 WxPusher 上行微信)

    目 录 〇.摘要 一.前言 二.实操 1. 找一台可以给互联网提供服务的计算机 2. 实现一个 flask 最小服务并在互联网上访问到 3. 根据 WxPusher 上行信息接口实现 POST 命令的 ...

  8. html怎么获取form表单数据,如何优雅的获取Form表单数据?

    先看一个简单的例子 比如这里有一个简单的 form 表单 r1 r2 r3 r4 c1 c2 c3 c4 login reset 页面如下 现在,随便输入点内容 如果提交,表单会触发默认行为,直接以g ...

  9. uniapp 获取定位(经纬度) 并且用腾讯地图api解析省市区门牌号

    1.首先去腾讯地图申请必要的key https://lbs.qq.com/ 根据项目需求勾选不同的方案,我写的是h5,如下 2.在项目中配置你的key 3.接下来就是代码了 必须引入腾讯的api,地址 ...

最新文章

  1. SAP MM 维护公司间STO报错-No delivery type defined for supplying plant NMI1 and document type NB-
  2. 让Apache支持Rewrite静态页面重写的方法
  3. 大神接棒,YOLOv4来了!
  4. boost::multi_array模块测试 index_gen 的代码
  5. 机器学习基石13-Hazard of Overfitting
  6. 酷黑风个人主页+引导页
  7. java 类里面对象共享_Java并发编程 - 对象的共享
  8. android卡片 弹簧滑动,如何在滚动android时拉类似于弹性类型的布局?
  9. 流媒体技术学习笔记之(十五)FFmpeg编码遇到的错误、警告、Debug记录
  10. nexus build docker private registry
  11. Android系统上使用ANMPP搭建Nginx+PHP+MySQL+FTP服务(以天猫魔盒TMB100A为例)搭建网站
  12. 电子工程师元器件应用必备宝典
  13. 证券交易系统术语介绍
  14. 数据时代建设医疗数据,主要有哪些意义?
  15. l2空间的完备性_话说泛函——Hilbert空间
  16. 2020-05-28
  17. HDFS HA机制 及 Secondary NameNode详解
  18. YY淘宝商品数据库设计(转)
  19. 正在存储windows支持软件_ibm/lenovoDS3500扩展柜存储管理软件-北京瑞腾世纪科技有限公司...
  20. 哈工大计算机系名单,哈工大计算机学部公布拟录取名单,计算机科学与技术仅录29人!...

热门文章

  1. code review手记3
  2. Multiple substitutions specified in non-positional format; did you mean to add the formatted=false
  3. linux复制内容的快捷键,Linux复制粘贴快捷键
  4. 计算机系毕业生自我评价,计算机系应届毕业生自我评价范文
  5. 计算机加经济学加自动化,MIT经济学家戳破机器人真相:除了能取代你,价值微乎其微...
  6. UI素材|管理系统数字可视化界面
  7. 电商促销海报BANNER设计要点,万能套用PSD分层模板,先收好!
  8. c语言第三章作业题答案,c语言第三章简单练习题及答案.doc
  9. net空间一次购买终身使用_net域名与org域名哪个投资价值更高?
  10. C++ Lambda表达式demo