面向body编程的新框架apie
轮子还是要造的,万一好用呢?哈哈
面向body编程
最近要写一个非常好用的api-mock的软件,但是目前的所有mock实现都不太符合我的期望。
- 简单
- 可配置
- 复用已有知识
- 如果可以,也可以进行逻辑处理
基于以上几点,我不得不再次审视http协议和已有的这些Node.js Web 框架,它们合适么?答案肯定是不合适的,虽然express、koa已经很简单了,但上面的要点都无法完成。
那么基于约定呢?
我们知道整个http请求过程里,在Mock的时候,请求是你来做,而结果是Mock Server来做。所以在设计Mock Server的时候,核心是Response,而Request只是约束条件,如果req符合条件,才允许给res。
那么常见的写法是什么样的呢?
{
"req": {
"method": "get",
"path": "/home"
},
"res": {
"status": 200,
"body": "Hello world!",
"headers": {
"Content-Type": "text/plain"
}
}
}
这样就基本满足了我们的描述式配置写法。大部分人都能按照这个写的,无论语义还是结构,都是不错的。我们来对比一下req和res里的属性,重合的几率有多大?常用的除了headers,基本没有太多了,那么我们是否能够将这些属性提到根节点呢?
{
"method": "get",
"path": "/home"
"status": 200,
"body": "Hello world!",
"req": {
"headers": {}
},
"res": {
"headers": {
"Content-Type": "text/plain"
}
}
}
这样是不是更加简单?再进行约定, 貌似除了body外,其他都是可选的
- method可选,默认为get
- path可选,默认以文件路径为路由,之前的mount-routes等就是这样实现,配置的path比文件路径优先级高
- status可选,默认200
所以body就成整个抽象的核心。那么body是哪些可能性呢?
- body是字符串类型,直接返回字符串
- body是字符串类型,且是存在的json文件,读取并返回json
- body是plain old object,返回json
- body是函数就允许用户自己写代码
- body是数组,约定定为中间件数组
这样就可以涵盖绝大多数场景了。如果还有啥express-generator支持的功能
- public静态Server(无须处理,指定目录即可)
- res.render视图渲染,模板引擎集成(可能要配置,安装对应的模板引擎模块)
- 中间件约定,配置(约定目录,自动挂载,按名字匹配)
Response.body Oriented Programming(ROP)
我将这种ROP编程命名为面向body编程
Apie
基于以上想法,设计了apie和apik框架。这里以Apie为例,进行用法说明。
Apie支持2种写法
- .json:配置文件,mock接口最方便
- .js:业务逻辑支持
准备
只需三步
$ git clone https://github.com/apiejs/apie-demo.git
$ cd apie-demo
$ npm start
接下你就专注于写你的api就好了
文件即路由
继承mount-routes,指定目录下的所有文件即对应路由。文件的名称即默认的请求path。可以无限目录级别的处理,只要是目录下的所有带有body属性的js和json都是路由。
比如我新建一个simple.js
,
module.exports = {
// "path": "/cnode",
"body": "Hello world!"
}
此时
- path 是
/simple
- method 是
GET
- body 是返回值字符串
"Hello world!"
如果想改为/cnode
,只需要修改配置
module.exports = {
"path": "/cnode",
"body": "Hello world!"
}
是不是很简单?
区分一下
- 文件名不支持复杂路由,比如具名路由,正则路由等
- 配置文件的path支持express里的所有路由方式
Mock-Api
采用JSON作为配置文件,是最常见的编写Mock接口的方式
hello world
比如我新建一个sang.json
,
{
"body": {
"json": "Hello Sang!"
}
}
此时
- path 是
/sang
- method 是
GET
- body 是返回值
body type
上面说了body支持5种类型,对于简单的mock-api说能用的大致有3种
- body是字符串类型,直接返回字符串
- body是字符串类型,且是存在的json文件,读取并返回json
- body是plain old object,返回json
举例1: 返回字符串
module.exports = {
"body": "Hello String"
}
举例2: 返回JSON
module.exports = {
"body": {
"json": "Hello world!"
}
}
这种规则理解起来应该是非常容易理解的。
json file
举例3:根据json文件,返回JSON,由于此种较为特殊,所以单独列出来讲解。
比如jsonfile.js里配置如下
module.exports = {
"path": "/jsonfile",
"body": "demo.json"
}
此时
- path 是
/sang
- method 是
GET
- body 是返回值
"demo.json"
文件的内容,类型为json
demo.json
{
"path": "demo.json",
"demo": {
"json": "Hello world!"
}
}
这种jsonfile使用场景
- 多个api复用同一个json文件
- 返回的json非常复杂
业务逻辑支持
body是中间件函数
举例function.js
module.exports = {
"body": function(req, res){
res.json("Hello world!")
}
}
body是express中间件函数,允许用户自己写代码,至于你想要啥逻辑,自己动手就好。
- 1)针对req里的params、query、body、header、cookie等进行处理
- 2)按需进行response响应
结合node的强大模块生态,在此处想做啥都可以,比如数据库连接,文件读写等,可以说你能想到的,都可以实现。
body是中间件数组
如果大家express路由比较熟悉,会知道路由上可以有n个中间件,n>1,这个的表现形式其实用数组更合适。
- body是数组,约定定为中间件数组
这样就可以涵盖绝大多数场景了。
举例
module.exports = {
"body": [function(req, res, next) {
next()
}, function(req, res){
res.json("Hello world!")
}]
}
是不是感觉和express一模一样?其实它就是换种写法而已,抽象一下就简单多了。
视图渲染
通过res.render视图渲染,模板引擎集成,安装对应的模板引擎模块,默认是pug
module.exports = {
"body": function (req, res) {
res.render('index', { title: 'Express' });
}
}
剩下的就是模板引擎用法了,这里就不过多介绍了。
中间件约定
在写express的时候有一个习惯,建立middlewares目录专门存储中间件,很是方便。这里也一样,apie会自动把middlewares目录里的中间件挂在到app.middlewares对象上,继而我们就可以通过app.middlewares[key]
的方式调用里。
目录
├── middlewares
├── a.js
└── b.js
a.js
module.exports = function a (req, res, next) {
console.log('a')
next()
}
用法,在配置里增加middlewares属性
单个中间件
module.exports = {
"path": "/middleware",
"middlewares": 'a',
"body": {
"json": "Hello middleware world!"
}
}
多个中间件
module.exports = {
"path": "/middlewares",
"middlewares": ['a', 'b'],
"body": {
"json": "Hello middlewares world!"
}
}
复用所有express插件
比如session、jwt、redis、ratelimit、数据库等等
集成方式
- 1)放到middlewares里,自己包一下,主要是针对参数。然后配置的熟悉里配置依赖
- 2)放到body是function或数组的依赖里
静态Server
按照约定,假定app是根目录,那么app/public就是静态server对应的文件夹。其他用法和express了的一样。
调用方式
安装依赖
$ npm install --save apie
方式1,直接启动,自嗨模式
var apie = require('apie')
apie('./routes', 3000);
返回express的app实例,可以像express一样启动,也可以作为express的子项目提供引用。
var apie = require(apie).app;
// return an express app
var app = apie('./routes');
// start server
app.listen(3000)
Project Directory
$ tree .
.
├── LICENSE
├── README.md
├── app
│ ├── all.js
│ ├── api
│ │ ├── index.js
│ │ └── user.js
│ ├── demo.json
│ ├── home.js
│ ├── json.js
│ ├── jsonfile.js
│ ├── middleware.js
│ ├── middlewares
│ │ ├── a.js
│ │ └── b.js
│ ├── middlewares.js
│ ├── movies.js
│ ├── post.js
│ ├── public
│ │ ├── images
│ │ ├── javascripts
│ │ └── stylesheets
│ ├── sang.json
│ ├── simple.js
│ ├── users.js
│ ├── view.js
│ └── views
│ ├── error.pug
│ ├── index.pug
│ └── layout.pug
├── app.js
└── package.json
这是一个简单的demo,为了和express-generator生产的一致。
性能测试
以同样简单的json api和view作为测试结果,性能比express-generator生成的低了不到5%,属于正常范围,是可以接受的。
总结
apie使用场景
- 对于mock数据尤其友好、简单,可配置
- 支持express/koa支持的所有的业务逻辑处理
- 对express/koa的插件、经验完美继承
目前欠缺的是对app生命周期的封装,如果加上,基本就完美了。
如果那apie和restify、hapi等比较,明显apie更简单,但插件机制上,没有实现,算一个小缺憾吧。
总之,这是一次尝试行的实践,就目前的结果来看,还是非常值得投入的。apie写了不到半天,apik写了不到1个小时,目前代码还比较粗糙,大家可以先看用法和设计。