对于Express与Koa他们的Api很相似,但是其执行机制还是有所不同的,有时间的同学还是推荐去读一些源码,Express源码感觉比较复杂的,Koa的源码相对比较精简,那么下面我们主要从两个方面去来看一下。
Express 是线性的,那么看一下下面的代码:
const Express = require('express')
const app = new Express();
const sleep = () => new Promise(resolve => setTimeout(function(){resolve(1)}, 2000))
const port = 3000
function f1(req, res, next) {
console.log('f1 start ->');
next();
console.log('f1 end <-');
}
function f2(req, res, next) {
console.log('f2 start ->');
next();
console.log('f2 end <-');
}
async function f3(req, res) {
await sleep();
console.log('f3 service...');
res.send('Hello World!')
}
app.use(f1);
app.use(f2);
app.use(f3);
app.get('/', f3)
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
输出结果:
f1 start ->
f2 start ->
f2 end <-
f1 end <-
f3 service...
那么可以得出,Express 中间件实现是基于 Callback 回调函数同步的,它不会去等待异步(Promise)完成,这也解释了为什么加入了异步操作顺序就被改变了。
初始化时的源码如下:
proto.use = function use(fn) {
var offset = 0;
var path = '/';
// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
// 定义callbacks
var callbacks = flatten(slice.call(arguments, offset));
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
// 依次读取callbacks数组中的函数
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}
// add the middleware
debug('use %o %s', path, fn.name || '' )
// 将取出的fn函数,传入到Layer中,进行new一个实例
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
// 将layer压入到stack栈中
this.stack.push(layer);
};
对于Express中源码的执行是在proto.handle中执行的,我们分析下源码:
proto.handle = function handle(req, res, out) {
var self = this;
...
next();
function next(err) {
...
// find next matching layer
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++]; // 取出中间件函数
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
...
}
...
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next);
}
trim_prefix(layer, layerError, layerPath, path);
});
}
function trim_prefix(layer, layerError, layerPath, path) {
...
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
// 这里进行函数调用,且递归
layer.handle_request(req, res, next);
}
}
};
proto.handle 方法的核心实现定义了 next 函数递归调用取出需要执行的中间件,因为没有promise等可以支持异步操作的,所以express的中间件不会等待异步的操作。
下面我们看一下中间件是如何挂载的,当我们调用use函数去注册中间件的时候,此时会将对应的函数加入到middleware这个数组中,然后在callback这个函数中使用compose传入这个middleware。callback这个函数是在listen的这个函数进行调用的,将this.callback传入到http.createServer的这个函数中。下面我们看下源码:
// 开启服务
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
// 注册中间件
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
// 将函数加入到中间件数组中
this.middleware.push(fn)
return this
}
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
callback () {
// 将这个中间件数组使用this.compose,实际是使用了koa-compose这个库
const fn = this.compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
那么我们主要去研究下中间件的机制,所以我们看一下koa-compose的机制,源码如下:
源码地址:https://github.com/koajs/compose/blob/master/index.js
module.exports = compose
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
通过此段源码分析到,每次中间件调用的next(),实际是调用的是dispatch.bind(null, i + 1),利用闭包和递归的性质,一个个执行,并且每次执行都是返回promise,所以这也就是我们为什么可以在中间件中使用异步了。
Express的中间件机制是基于Callback回调函数同步的,他不会等待异步操作的完成,Koa的中间件中的next返回的是一个Promise,我们可以通过async和await用同步的方式去写异步操作。