awy是一个NodeJS环境的Web服务端框架,很简单,也很小。基于async/await关键字。其核心就是一个基于中间件模式根据路由分发请求的处理过程。
awy2.js是支持HTTP/2协议的版本,并且只能使用HTTP/2。npm安装:npm i awy2。接口和awy完全兼容。目前几乎所有的浏览器都支持HTTP/2,多数情况后端无需考虑协议降级。
- 中间件
- 路由
- 路由分组
- 解析Body数据
- 解析上传的文件
- 启用守护进程模式
- 配置HTTPS
rr.req.Param
URL参数,JSON键值对存储。
rr.req.BodyParam
POST/PUT提交的Body参数,根据content-type不同值,有不同的处理方式,键值对存储或者是其他格式化文本。
rr.req.RawBody
Body原始数据,binary编码。
rr.req.Args
路由参数。
rr.req.ROUTEPATH
请求的真正处理路由。比如带参数的路由/content/:id,实际访问的路径是/content/12。ROUTEPATH的值是/content/:id。
rr.req.ORGPATH
请求的原始路由,比如带参数的路由/content/:id,实际访问的路径是/content/12。则ORGPATH的值是/content/12。
rr.req.UploadFiles
上传文件数据,JSON格式存储,每个上传名指向一个数组,数组中的数据是JSON格式,包括content-type、fielname、data字段,其中data是实际文件的数据。
rr.req.GetFile和rr.req.MoveFile
辅助上传文件操作的接口,GetFile用于获取文件数据,MoveFile移动文件,实际就是对UploadFiles的操作。详细使用见后面的示例。
rr.res.Body
要返回的数据,只需要对rr.res.Body赋值即可。
const awy = require('awy');
var ar = new awy();
/*
一定要注意的是:
回调函数要写成async rr的形式。
rr是打包了request和response的对象:
{
req,
res
}
*/
ar.get('/', async rr => {
rr.res.Body = 'success';
});
ar.run('localhost', 8080);
curl 'http://localhost:8080/'
输出结果:
success
run接口的返回值是http.createServer的返回值,也就是http.Server的实例。以下示例设置超时时间为5秒。
对于awy2来说,是http2.createSecureServer的返回值http2.Http2SecureServer实例,同样有setTimeout方法。
const awy = require('awy');
var ar = new awy();
ar.get('/', async rr => {
rr.res.Body = 'ok';
});
ar.run('localhost', 8080).setTimeout(5000);
通过返回值获取实例可以使用Node.js原生提供的功能,这样框架做更少的事情,学习和使用成本相对也低。框架主要使用了request以及clientError事件,对于http2来说,主要使用了stream和sessionError事件,通过返回值开发者可以使用更多的事件做扩展处理。
HTTP模块的默认超时时间为120秒,awy框架默认设置为25秒,你可以通过run的返回值调用setTimeout方法设置一个合理的时间,或者通过请求请求参数rr.req以及rr.res设置具体请求的超时时间。
对于HTTP服务来说,一般正常的服务,请求处理时间都很短,不需要特别长的超时设置,对比较特殊的Web服务,比如上传和下载,可以设置单独的超时。
const awy = require('awy');
var ar = new awy();
ar.get('/test', async rr => {
var {name} = rr.req.Param;
console.log(name);
rr.res.Body = name;
});
ar.run('localhost', 8080);
curl 'http://localhost:8080/test?name=helo'
输出结果:
helo
const awy = require('awy');
var ar = new awy();
ar.post('/pt', async rr => {
var {username} = rr.req.BodyParam;
rr.res.Body = username;
});
ar.run('localhost', 8080);
curl 'http://localhost:8080/pt' -d 'username=albert'
返回结果:
albert
上传的文件会被解析到rr.req.UploadFiles,结构如下:
{
"image" : [
{
"filename" : "a.png",
"content-type" : "image/png",
"data" : "......"
},
{
"filename" : "b.png",
"content-type" : "image/png",
"data" : "......"
}
],
"file" : [
{
"filename" : "w.txt",
"content-type" : "text/plain",
"data" : "......"
},
{
"filename" : "x.c",
"content-type" : "text/plain",
"data" : "......"
}
]
}
/*
如果要POST上传文件,name属性设置为image,可以通过rr.req.UploadFiles['image'][0]访问第一个文件。
*/
const awy = require('awy');
var ar = new awy();
/*
开启解析文件数据选项,如果想使用其他处理方式,
可以设置为false,交给其他中间件处理。
可以通过或者req.RawBody
获取原始的上传数据。
*/
ar.config.parse_upload = true;
ar.post('/upload', async rr => {
//GetFile接受两个参数,第二个参数是索引值,默认为0。
//每次上传一个文件的情况居多,所以默认获取第一个。
//第一个参数是索引的名称。
//如果文件不存在返回null。
var uf = rr.req.GetFile('image');
if (!uf) {
rr.res.Body = 'Error: file not found';
return ;
}
await rr.req.MoveFile(uf, {
path : './upload/images'
})
.then(ret => {
rr.res.Body = ret;
}, err => {
rr.res.Body = err.message;
})
.catch(err => {
rr.res.Body = err.message;
});
});
ar.run('localhost', 8080);
//文件tmp/a.png替换为其他存在的文件。
curl 'http://localhost:8080/upload' -F 'image=@tmp/a.png'
awy的中间模式可以用下图描述:
awy中间件添加方式很简单。
const awy = require('awy');
var ar = new awy();
/*
next表示下一层中间件,
并且只需要await next(rr);
就可以等待执行下一层中间件。
之后还可以继续执行后面的操作,
如果没有await next(rr);
则请求到此结束。
中间件顺序:按照添加的顺序逆序执行。
所以先执行的中间件放在后面。
*/
ar.add(async (rr, next) => {
rr.res.Body += 'I am a middleware\n';
await next(rr);
rr.res.Body += '[OK]\n';
}, ['/', '/test']);
ar.get('/', async rr => {
rr.res.Body += 'Hello world\n';
});
ar.get('/test', async rr => {
rr.res.Body += 'This is test page\n';
});
ar.get('/notmid', async rr => {
rr.res.Body += 'No middleware running\n';
});
curl 'http://localhost:8080/'
I am a middleware
Hello world
[OK]
curl 'http://localhost:8080/test'
I am a middleware
This is test page
[OK]
curl 'http://localhost:8080/notmid'
No middleware running
awy的路由非常简单,除了基本的字符串,支持使用:表示变量,*匹配任意路径。
一定要注意的是:* 和 : 不能同时出现在路由字符串中。
/content/:id
访问方式:/content/123
/rs/:id/:group
此时如果访问路径为/rs/1234/linux,
则实际会执行/rs/:id/:group,
参数解析到req.Args:
{
id : "1234",
group : "linux"
}
/static/*
这种情况,/static/后面可以跟任意路径都会通过此路由绑定的函数处理。
但是/*则会匹配所有路由,请注意不要出现冲突的情况。
使用*可以实现扩展路由的功能,如果你使用 /* 接管所有的路径,之后可以在请求中进行更具体的路由派发。
或者使用中间件接管路由分发。
const awy = require('awy');
var ant = new awy();
ant.map(['GET', 'POST'], '/*', async rr => {
//进行路由分发。
//req.ORGPATH存储请求真实的路径信息。
await dispatchRoute(rr.req.ORGPATH);
});
路由分组是基于路径分隔符的第一个字符串,比如/api/a和/api/b都使用/api分组。框架的设计机制保证可以对中间件也进行分组,通过group接口返回的对象可以使用get、post、put、delete、options、any、add接口。这时候使用add添加的中间件只会在当前分组下执行。以下代码给出了完整示例:
const awy = require('awy');
var ar = new awy();
//使用api分组。
var api = ar.group('/api');
/*
分组下的路径仅仅就是把分组名和路径拼接到一起作为整体的路由。
最终执行就是/api/a或/api/b这样的形式。
*/
api.get('/a', async rr => {
rr.res.Body = 'a';
});
api.get('/b', async rr => {
rr.res.Body = 'b';
});
//此中间件只会在/api分组执行。
api.add(async (rr, next) => {
console.log('/api middleware');
});
//全局的路径,尽管是/api开头,但是不属于/api分组
ar.get('/api/xyz', async rr => {
rr.res.Body = 'xyz';
});
ar.run('localhost', 8080);
路由结构和通常的方式有些不同,主要结构如下所示:
{
"GET" : {
"/a" : {/*...*/},
"/b" : {/*...*/}
},
"POST" : {
"/a" : {/*...*/},
"/b" : {/*...*/},
"/xyz" : {/*...*/}
},
"PUT" : {
"/x" : {/*...*/}
}
}
在查找时,先根据请求方法确定要去哪里查找,所以如果存在路径/xyz属于POST请求,使用GET方式,会返回404。
const awy = require('awy');
var as = new awy();
as.get('/content/:id', async rr => {
//...
});
as.post('/content', async rr => {
//...
});
as.put('/content/:id', async rr => {
//...
});
as.delete('/resource/:id', async rr => {
//...
});
as.run('localhost', 1234);
框架使用一些选项控制某些功能,选项信息如下。
{
//此配置表示POST/PUT提交表单的最大字节数,也是上传文件的最大限制,
body_max_size : 8000000,
//开启守护进程,守护进程用于上线部署,要使用ants接口,run接口不支持
daemon : false,
//允许跨域的域名,使用*表示所有域名
cors : null,
/*
自动处理OPTIONS请求,这属于全局范围内的处理,
当浏览器发起OPTIONS预检请求,框架会自动返回,无需开发者处理。
*/
auto_options : false,
/*
暂时只是实现了mem模式,文件会被放在内存里。
事实上,这个选项目前并没有什么用处,
你可以通过关闭parse_upload选项,
通过rr.req.RawBody获取原生数据然后使用其他解析处理。
*/
upload_mode : 'mem',
//自动解析上传的数据
parse_upload : true,
//开启HTTPS
https_on : false,
//HTTPS密钥和证书的路径
https_options : {
key : '',
cert : ''
},
};
run接口运行后,只能作为当前shell的子进程执行,如果加上&运行命令,尽管现在最新的bash把后台进程作为单独的一个执行线程独立出去,但是实际测试发现,在执行一段时间后会退出。具体原因还没有深入研究。不过比较保险的做法是创建守护进程。使用C语言创建守护进程还是比较方便的,Node.js在这方面做了很多封装,反而有些复杂。不过框架提供了方便的处理接口。
这个操作使用ants接口可以快速实现,ants接口的前两个参数和run一致:接受host和port。第三个参数是一个整数表示要创建几个子进程处理请求,不填写默认为0,这种情况会根据CPU核心数创建子进程。ants接口使用了Cluster模块。
如果不需要Cluster模块,则可以使用shell命令setsid加上要运行的命令:
setsid node serv.js