如何编写声明式的Koa 2.x中间件
发布于 8 年前 作者 i5ting 5592 次浏览 来自 分享

如何编写声明式的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呢?

  1. 如何把参数,变成json配置,即调用函数使用配置,而不是使用参数
  2. 调用模块(node module)函数使用配置,而不是使用参数
  3. 把Koa 2.x的多个中间件合并,然后app.use,这样才比较方便,即load Koa 2.x middlewares with config
  4. 简化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把参数改成配置项,然后调用

总结

中间件定义

 favicon(path, [options])
  • 参数必须一致,path和options必须一样
  • 配置的key必须模块名称,比如koa-favicon

这样是有点也是缺点。还好的是目前的koa的插件写的还好,命名等都比较规范,大家自己写插件的时候也尽量规范就好。

这里举例只是加载某些中间件,那么能不能分组,在app.js里随处use呢?大家自己发挥吧。

2 回复

这里的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实现有关系,具体原因还要看看

回到顶部