前调:一些吐槽
因为开发过大大大大大大量的 Restful API
,越来越厌烦那种一成不变的代码组织方式。
以 egg
为例,要增加一个接口,需要经历繁琐的操作
[路由里注册] ---> [编写 controller] ---> [编写 service]
哪怕我只是想实现一个 a+b => c
!
除此以外,我还调研过某几个比较大的 函数计算 服务商,看看有没有什么好的途径,可以更简便地进行简单接口开发。
让人很失望,天下技术一大抄,它们无一例外都是这样的开发模型:
function(event, context, callback) {}
或者
function(event, context) {}
于是你不得不这样写你的代码
// 举例
function(event, context, callback) {
const { a, b } = event.arguments;
callback(a+b);
}
这算个什么鬼函数?!
你必须改变第一直觉,严格按照厂商的要求来写你的代码,并且寄希望于有人站出来要求它们统一event
、context
这些旁系知识的实现细节。
并且!
你很难进行本地测试!
你要构造奇奇怪怪的对象作为参数。哪怕只是测试两数相加.
OK! 如果你说,稍微学习一下,其实还是可以用的吧。
可以用就是我们的追求吗?要 应然 还是 实然 ?
难倒我们期望的样子,不应该是这样吗?
// 🔥🔥
function (a, b) {
return a+b;
}
正文开始:
所以,如果我需要实现一个 a+b => c
的接口,
我期望我的代码是这样的:
export default (a: number, b: number): number => {
return a + b;
}
我期望我的测试是这样的:
describe('sum', () => {
it('1+1 is 2', () => {
assert.equal(sum(1,1), 2);
});
});
十分明显好吗!
它并不受制于具体平台实现;
它并不需要你学习其他旁系知识,只关注你的功能本身;
它方便测试,因为它就是一个不能再普通的函数;
它可复用!
多说无益,直接动手
export default (a: number, b: number): number => {
return a + b;
}
有过上一篇的基础,我们直奔抽象语法树:
// 由于只有一行代码,结构就简单多了
{
type: 'ExportDefaultDeclaration',
declaration: {
type: 'ArrowFunctionExpression',
params: [ 下文展开 ],
body: { 不重要 },
returnType: { 下文展开 }
}
}
我们还是从整体上先看一下,上面这个结构充分表达了源代码的意图:
- 这是一个
export default
声明 (ExportDefaultDeclaration
) - 声明的内容是一个箭头函数 (
ArrowFunctionExpression
)
考虑到,我们希望从这一行代码里面,生成提供HTTP服务的基础代码,
那么我们的主要任务就是,从以上有限的信息当中,提取出Restful API需要的基础信息:
- Method
- Path
- 请求参数
🤖 我们一个一个来解决:
-
HTTP Method
首选是要确定这个接口,最终通过什么
Method
对外服务,在这里源代码并不能提供任何有效信息。那么我们就默认用 GET 好了
🔥其他的 Method,会在文末提及
-
Path, 或者叫 URL
对于一个接口来说,这也是十分关键的信息,这里我们有两种解决方案
- 使用函数名,如果有的话
- 使用当前文件名
我们姑且把 URL 定为
/sum
好了🔥 笔者的一个实验性项目里,使用的是文件名
-
请求参数
如果说前面两点都是基于约定,或者一些简单的手段,
那么在请求参数这个环节,我们必须上价值了,要让
ast
发挥作用!我们先展开一下上面
ast
里关于函数参数的部分:// params 部分 [ { name: 'a', typeAnnotation: { type: 'TSNumberKeyword' } }, { name: 'b', typeAnnotation: { type: 'TSNumberKeyword' } }, ]
好家伙,可以拿到参数名
a
和b
了结合
TSNumberKeyword
信息,我们甚至能笃定这两个参数是数字类型。知道类型,就可以做参数校验!
🔥通过原始函数定义的参数类型生成 http 参数校验逻辑,比起手工编写
Joi
配置要可靠得多。等等,还有一个问题没解决
参数名和参数类型都有了,缺的就是参数位置以及
content-type
。约定大法好,根据多年开发总结得出:
总所周知, GET 请求的参数就放在 queryString 里吧。
至于
content-type
统一使用application/json
不接受反驳
齐活了
结合上面收集到的信息,我们可以想象最终的场景是:
- step 1 : 按上面的方式,编写一个不能再普通的函数
- step 2: 使用我们编写好的工具,运行这个函数
- step 3: 可以通过
http://127.0.0.1:3000/sum?a=11&b=22
来访问这个接口,并且得到结果 33
通过这个截图可知,虽然实现细节比较多,但是可行性是没问题的。
并且笔者已经做出了一个实验性项目。
如果对这种模式比较感兴趣,欢迎前来讨论,在这里就不打广告了。
FAQ 环节
-
更多的 HTTP_METHOD 怎么办?总不能都用 GET 吧
目前我选能用的方案是,如果没有声明,就用一个默认的
GET
,因为要尽量选一个能覆盖 90% 情况的作为默认值。
当我需要使用其他 Method ,我的方案是显式指定,比如
export const method = 'POST'; export default (a: number, b: number): number => { return a + b; }
这个方案可以使信息更紧凑,同时不影响函数逻辑跑本地单元测试。
-
上面提到的入参参数位置,什么时候在
queryString
,什么时候在其他?遵从大多数的案例,GET 的情况下使用
queryString
, 其他情况下在body
当然还有完全自定义的方案,为避广告嫌疑就不展开了,基本原则还是不影响函数核心逻辑和单元测试。
-
上面提到的 入参参数类型 ,有什么应用场景?
有了这个信息,如果你喜欢
Joi
, 应该能很容易生成参数校验的代码了。或者有自己想法的话,和我一样,自己实现一套
validator
也不是什么难事。