前调:一些吐槽

因为开发过大大大大大大量的 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);
}

这算个什么鬼函数?!

你必须改变第一直觉,严格按照厂商的要求来写你的代码,并且寄希望于有人站出来要求它们统一eventcontext这些旁系知识的实现细节

并且!

你很难进行本地测试!

你要构造奇奇怪怪的对象作为参数。哪怕只是测试两数相加.

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
  • 请求参数

🤖 我们一个一个来解决:

  1. HTTP Method

    首选是要确定这个接口,最终通过什么 Method对外服务,在这里源代码并不能提供任何有效信息。

    那么我们就默认用 GET 好了

    🔥其他的 Method,会在文末提及

  2. Path, 或者叫 URL

    对于一个接口来说,这也是十分关键的信息,这里我们有两种解决方案

    • 使用函数名,如果有的话
    • 使用当前文件名

    我们姑且把 URL 定为 /sum 好了

    🔥 笔者的一个实验性项目里,使用的是文件名

  3. 请求参数

    如果说前面两点都是基于约定,或者一些简单的手段,

    那么在请求参数这个环节,我们必须上价值了,要让 ast 发挥作用!

    我们先展开一下上面 ast 里关于函数参数的部分:

    // params 部分
    [
    	{
    		name: 'a',
    		typeAnnotation: { type: 'TSNumberKeyword' }
    	},
    	{
    		name: 'b',
    		typeAnnotation: { type: 'TSNumberKeyword' }
    	},
    ]
    

    好家伙,可以拿到参数名 ab

    结合 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 也不是什么难事。