如何编写声明式的Koa 2.x中间件
痛点
比如https://github.com/koajs/favicon/tree/v2.x
const Koa = require('koa');
const favicon = require('koa-favicon');
const app = new Koa();
app.use(favicon(__dirname + '/public/favicon.ico'));
API
favicon(path, [options])
Returns a middleware serving the favicon found on the given path
.
options
maxAge
cache-control max-age directive in ms, defaulting to 1 day.
如果非常多的中间件。。。。就会又臭又长
更好的解决方式
把参数变成配置,改成声明式的,不是就非常简单了么?
比如定义 config.json
{
"koa-favicon": {
"path": "sss",
"options": {
"maxAge": 1
}
},
"koa-etag":{
}
}
然后app.js
const Koa = require('koa');
const app = new Koa();
const path = require('path')
const load_koa_middlewares = require('load_koa_middlewares')
const config = path.join(__dirname, '../conf.yml')
app.use(load_koa_middlewares(config))
app.listen(3005);
这样是不是可以非常简单的实现,无论你有多少中间件,写到配置文件里就好了
如何实现load_koa_middlewares呢?
- 如何把参数,变成json配置,即调用函数使用配置,而不是使用参数
- 调用模块(node module)函数使用配置,而不是使用参数
- 把Koa 2.x的多个中间件合并,然后app.use,这样才比较方便,即load Koa 2.x middlewares with config
- 简化koa_middlewares_with_config,直接读取配置文件就好,更简单点
调用函数使用配置,而不是使用参数
比如favicon函数
function favicon(path, options){
console.log('favicon = ' + JSON.stringify(arguments));
console.log('favicon path= ' + path)
console.dir(options)
return JSON.stringify(arguments)
}
参数是path, options
调用的时候
favicon('./s.ico')
favicon('./s.ico', {
})
无论如何,第一个参数是path,第二个参数是options。
那问题就简单了
favicon.apply(null, ['./s.ico'])
favicon.apply(null, ['./s.ico', {
}])
剩下的就是正则处理的事儿。见https://github.com/i5ting/call_with_config/tree/v2
关于api设计
call_with_config(key, config, cumtomKey)
- key = module name or local module
- config for param
- if cumtomKey exist, load config from config[cumtomKey]
var call_with_config = require('.')
var r = call_with_config('./favicon', {
'./favicon':{
'path': 'sss'
},
'empty-favicon':{
}
}, 'empty-favicon');
console.dir(r.toString())
'./favicon’是key,如果有自定义的key,那么就从config[‘empty-favicon’]里加载
or
var call_with_config = require('.')
var r = call_with_config('koa-favicon', {
'koa-favicon':{
'path': 'sss',
'options': {
'maxAge': 1
}
}
});
console.dir(r.toString())
'koa-favicon’是key,如果没有自定义的key,那么就从config[key]里加载
这样就可以非常灵活了。
调用模块(node module)函数使用配置,而不是使用参数
var conf = {
'koa-favicon': {
'path': 'sss',
'options': {
'maxAge': 1
}
},
'koa-etag':{
}
}
var call_module_with_config = require('call_module_with_config')
// call('./favicon', conf, true)
call_module_with_config(['koa-favicon', 'koa-etag'], conf)
模块如’koa-favicon’, ‘koa-etag’,如果要是一个,内部会自动转成数据。
核心逻辑还是上面的call_with_config模块。
这样做的好处是把模块和配置结合在一起了。可以随意组合
把Koa 2.x的多个中间件合并,然后app.use,这样才比较方便
解决了函数和模块与配置的关系,剩下的就是如何和koa 2.x结合。
koa的中间件基本都是独立模块,独立模块和配置之间已经在上一步解决了。
app.use([a,b])
koa中的use支持单个或多个中间件,为了简便,把我们加载的多个中间件的结果转成一个中间件,这样用的人才更简单。
这其实是koa-compose的用途。
所以这里主要是集成compose模块
const debug = require('debug')('koa_middlewares_with_config')
const compose = require('koa-compose')
const call_module_with_config = require('call_module_with_config')
module.exports = function (arr, conf) {
var middlewares = call_module_with_config(arr, conf)
debug('middlewares = %s' + middlewares)
return compose(middlewares)
}
用法如下
var conf = {
'koa-favicon': {
'path': 'sss',
'options': {
'maxAge': 1
}
},
'koa-etag':{
}
}
var middlewares = require('koa_middlewares_with_config')(['koa-favicon', 'koa-etag'], conf)
app.use(middlewares)
可是你不觉得,这样写起来还是太麻烦么?明明[‘koa-favicon’, ‘koa-etag’]里的内容在配置里都有,为什么还要再写一遍呢?还容易出错,是不是傻?
简化koa_middlewares_with_config,直接读取配置文件就好,更简单点
通过读取配置文件(json或yml),找出配置的keys,即
var conf = {
'koa-favicon': {
'path': 'sss',
'options': {
'maxAge': 1
}
},
'koa-etag':{
}
}
keys = [‘koa-favicon’, ‘koa-etag’]
剩下不就是
var keys = ['koa-favicon', 'koa-etag']
var middlewares = require('koa_middlewares_with_config')(keys, conf)
app.use(middlewares)
这样就可以推导出
config.json
{
"koa-favicon": {
"path": "sss",
"options": {
"maxAge": 1
}
},
"koa-etag":{
}
}
具体用法
var load_koa_middlewares = require('load_koa_middlewares')
var config = path.join(__dirname, '../conf.json')
app.use(load_koa_middlewares(config))
这样就是最终用法
- load_koa_middlewares(config)返回的compose的koa 2.x中间件
- 然后app.use即可,至于use位置可以按需定制
- 其他一切中间件都在配置文件里
有木有觉得有点意思了么?
其实配置还是有很多,比如yaml,toml等,这里yaml可以非常转成json对象,所以集成yaml就是几分钟的事儿
config_obj = YAML.load(file);
至此就完成了所有设计。
能否让这些中间件的配置丢到package.json里呢?
类似于gitbook的设计。
- 特别复杂的中间件配置必须在js里,json里是无法完成的。于是有了jwf和get_closest_package_json
- 改在load_koa_middlewares,接受参数为2种:file 或配置对象
- 使用get_closest_package_json和load_koa_middlewares封装成load_koa_middlewares_with_key
Install
$ npm i -S load_koa_middlewares_with_key
Usages
app.use(require('load_koa_middlewares_with_key')())
or
app.use(require('load_koa_middlewares_with_key')('config'))
config in package.json
"config": {
"koa-favicon": {
"path": "sss",
"options": {
"maxAge": 1
}
},
"koa-etag": {}
},
All
- load_koa_middlewares_with_key在package.json里配置中间件
- get_closest_package_json 支持jwf解析的package.json
- jwf如果配置项里有文件,直接读取
- load_koa_middlewares使用配置文件加载中间件
- koa_middlewares_with_config根据配置对象加载中间件
- call_module_with_config根据配置调用模块
- call_with_config把参数改成配置项,然后调用
- call_module_with_config根据配置调用模块
- koa_middlewares_with_config根据配置对象加载中间件
- get_closest_package_json 支持jwf解析的package.json
总结
中间件定义
favicon(path, [options])
- 参数必须一致,path和options必须一样
- 配置的key必须模块名称,比如koa-favicon
这样是有点也是缺点。还好的是目前的koa的插件写的还好,命名等都比较规范,大家自己写插件的时候也尽量规范就好。
这里举例只是加载某些中间件,那么能不能分组,在app.js里随处use呢?大家自己发挥吧。
这里的compose打包实现似乎有一点问题 compose核心实现是类似
fn = Promise.resoleve(async function(ctx, next){
//中间件1代码
await Promise.resolve(async function(ctx, next){
//中间件2代码
}())
}());
那么在application.js原本结束的标识就是最外部的Promise.resolve返回,很显然,由于每嵌套一个中间件,都是使用await/yield 做暂停,所以可以保证每一个中间件按照顺序加载。 但是下面问题来了,您这边打包函数:
module.exports = function (arr, conf) {
var middlewares = call_module_with_config(arr, conf)
debug('middlewares = %s' + middlewares)
return compose(middlewares)
}
并且使用返回的compose(middlewares)作为打包后的中间件直接app.use,那么compose返回的其实仅仅是一个Promise.resolve(async function(){}());这样就等于出现了,在application.js中的fn变成了
fn = Promise.resolve(async function(){
Promise.resolve(async function(){
//打包的中间件
}());
}());
可以很明显看到,由于少了一个await或者yield关键字,导致fn().then中的then调用时间点变的不可预期了,可能会造成中间件加载意外不符合自己的预期的现象,当然更详细的需要测试下
我刚才简单测试了下,很有意思,如果中间件都是普通函数(既不是async函数,也不是co.wrap包装的Generator函数),这样打包的中间件加载是没问题的:
function pro() {
let fn = Promise.resolve(function () {
console.log(1);
Promise.resolve(function () {
console.log(2);
Promise.resolve();
console.log(3)
}());
console.log(4)
}());
return fn;
}
pro().then(()=>console.log(5));
//输出:1,2,3,4,5,是符合Koa的中间件加载顺序的
然后一旦中间出现了co.wrap打包的中间件,如下:
function pro1() {
let fn = Promise.resolve(function () {
console.log(1);
Promise.resolve(co.wrap(function * () {
console.log(2);
yield Promise.resolve(123);
console.log(3);
})());
console.log(4)
}());
return fn;
}
//输出: 1,2,4,3,5,中间件加载顺序已经出现了改变
看现象适合Promise.resolve实现有关系,具体原因还要看看