前言前言前言为了能够更好地处理异步流程,一般开发者会选择 async 语法。在 express 框架中可以直接利用 async 来声明中间件方法,但是对于该中间件的错误,无法通过错误捕获中间件来劫持到。错误处理中间件
错误处理中间件错误处理中间件
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {

const message = doSomething();

res.send(message);
});

// 错误处理中间件
app.use(function (err, req, res, next) {

return res.status(500).send('内部错误!');
});

app.listen(PORT, () => console.log(`app listening on port ${PORT}`));


const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {

const message = doSomething();

res.send(message);
});

// 错误处理中间件
app.use(function (err, req, res, next) {

return res.status(500).send('内部错误!');
});

app.listen(PORT, () => console.log(`app listening on port ${PORT}`));

以上述代码为例,中间件方法并没有通过 async 语法来声明,如果 doSomething 方法内部抛出异常,那么就可以在错误处理中间件中捕获到错误,从而进行相应地异常处理。
app.get('/', async (req, res) => {

const message = doSomething();

res.send(message);
});


app.get('/', async (req, res) => {

const message = doSomething();

res.send(message);
});

而采用 async 语法来声明中间件时,一旦 doSomething 内部抛出异常,则错误处理中间件无法捕获到。虽然可以利用 process 监听 unhandledRejection 事件来捕获,但是无法正确地处理后续流程。try/catch
try/catchtry/catch对于 async 声明的函数,可以通过 try/catch 来捕获其内部的错误,再使用 next 函数将错误递交给错误处理中间件,即可处理该场景:
app.get('/', async (req, res, next) => {

try {

const message = doSomething();

res.send(message);

} catch(err) {

next(err);

}
});


app.get('/', async (req, res, next) => {

try {

const message = doSomething();

res.send(message);

} catch(err) {

next(err);

}
});

「 这种写法简单易懂,但是满屏的 try/catch 语法,会显得非常繁琐且不优雅。 」高阶函数
高阶函数高阶函数对于基础扎实的开发来说,都知道 async 函数最终返回一个 Promise 对象,而对于 Promsie 对象应该利用其提供的 catch 方法来捕获异常。那么在将 async 语法声明的中间件方法传入 use 之前,需要包裹一层 Promise 函数的异常处理逻辑,这时就需要利用高阶函数来完成这样的操作。
function asyncUtil(fn) {

return function asyncUtilWrap(...args) {

const fnReturn = fn(args);

const next = args[args.length - 1];

return Promise.resolve(fnReturn).catch(next);

}
}

app.use(asyncUtil(async (req, res, next) => {

const message = doSomething();

res.send(message);
}));

function asyncUtil(fn) {

return function asyncUtilWrap(...args) {

const fnReturn = fn(args);

const next = args[args.length - 1];

return Promise.resolve(fnReturn).catch(next);

}
}

app.use(asyncUtil(async (req, res, next) => {

const message = doSomething();

res.send(message);
}));
相比较第一种方法, 「 高阶函数减少了冗余代码,在一定程度上提高了代码的可读性。 」高阶函数减少了冗余代码,在一定程度上提高了代码的可读性。上述两种方案基于扎实的 JavaScript 基础以及 Express 框架的熟练使用,接下来从源码的角度思考合适的解决方案。中间件机制
中间件机制中间件机制Express 中主要包含三种中间件:

应用级别中间件

路由级别中间件

错误处理中间件
应用级别中间件路由级别中间件错误处理中间件
app.use = function use(fn) {
var path = '/';

// 省略参数处理逻辑
...

// 初始化内置中间件
this.lazyrouter();
var router = this._router;

fns.forEach(function (fn) {

// non-express app

if (!fn || !fn.handle || !fn.set) {

return router.use(path, fn);

}


...

}, this);

return this;
};


app.use = function use(fn) {
var path = '/';

// 省略参数处理逻辑
...

// 初始化内置中间件
this.lazyrouter();
var router = this._router;

fns.forEach(function (fn) {

// non-express app

if (!fn || !fn.handle || !fn.set) {

return router.use(path, fn);

}


...

}, this);

return this;
};

应用级别中间件通过 app.use 方法注册, 「 其本质上也是调用路由对象上的中间件注册方法,只不过其默认路由为 '/' 」 。其本质上也是调用路由对象上的中间件注册方法,只不过其默认路由为 '/'
proto.use = function use(fn) {
var offset = 0;
var path = '/';

// 省略参数处理逻辑
...

var callbacks = flatten(slice.call(arguments, offset));

for (var i = 0; i < callbacks.length; i++) {

var fn = callbacks[i];


...


// add the middleware

debug('use %o %s', path, fn.name || '')


var layer = new Layer(path, {

sensitive: this.caseSensitive,

strict: false,

end: false

}, fn);


layer.route = undefined;


this.stack.push(layer);
}

return this;
};


proto.use = function use(fn) {
var offset = 0;
var path = '/';

// 省略参数处理逻辑
...

var callbacks = flatten(slice.call(arguments, offset));

for (var i = 0; i < callbacks.length; i++) {

var fn = callbacks[i];


...


// add the middleware

debug('use %o %s', path, fn.name || '')


var layer = new Layer(path, {

sensitive: this.caseSensitive,

strict: false,

end: false

}, fn);


layer.route = undefined;


this.stack.push(layer);
}

return this;
};

中间件的所有注册方式最终会调用上述代码,根据 path 和中间件处理函数生成 layer 实例,再通过栈来维护这些 layer 实例。
// 部分核心代码
proto.handle = function handle(req, res, out) {
var self = this;
var idx = 0;
var stack = self.stack;

next();

function next(err) {

var layerError = err === 'route'

? null

: err;



if (idx >= stack.length) {

return;

}


var path = getPathname(req);


// 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 (match !== true) {

continue;

}


}


// no match

if (match !== true) {

return done(layerError);

}


// 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 = function handle(req, res, out) {
var self = this;
var idx = 0;
var stack = self.stack;

next();

function next(err) {

var layerError = err === 'route'

? null

: err;



if (idx >= stack.length) {

return;

}


var path = getPathname(req);


// 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 (match !== true) {

continue;

}


}


// no match

if (match !== true) {

return done(layerError);

}


// 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);

}
}
};

Express 内部通过 handle 方法来处理中间件执行逻辑,其利用 「 闭包的特性 」 缓存 idx 来记录当前遍历的状态。 闭包的特性该方法内部又实现了 next 方法来匹配当前需要执行的中间件,从遍历的代码可以明白 「 中间件注册的顺序是非常重要的 」 。 中间件注册的顺序是非常重要的 如果该流程存在异常,则调用 layer 实例的 handle.error 方法,这里仍然是 「 遵循了 Node.js 错误优先的设计理念 」 :遵循了 Node.js 错误优先的设计理念
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
var fn = this.handle;

if (fn.length !== 4) {

// not a standard error handler

return next(error);
}

try {

fn(error, req, res, next);
} catch (err) {

next(err);
}
};


Layer.prototype.handle_error = function handle_error(error, req, res, next) {
var fn = this.handle;

if (fn.length !== 4) {

// not a standard error handler

return next(error);
}

try {

fn(error, req, res, next);
} catch (err) {

next(err);
}
};

「内部通过判断函数的形参个数过滤掉非错误处理中间件」。
如果 next 函数内部没有异常情况,则调用 layer 实例的 handle_request 方法:内部通过判断函数的形参个数过滤掉非错误处理中间件
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;

if (fn.length > 3) {

// not a standard request handler

return next();
}

try {

fn(req, res, next);
} catch (err) {

next(err);
}
};


Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;

if (fn.length > 3) {

// not a standard request handler

return next();
}

try {

fn(req, res, next);
} catch (err) {

next(err);
}
};

 「 handle 方法初始化执行了一次 next 方法,但是该方法每次调用最多只能匹配一个中间件 」 ,所以在执行 handle_error 和 handle_request 方法时,会将 next 方法透传给中间件,这样开发者就可以通过手动调用 next 方法的方式来执行接下来的中间件。 handle 方法初始化执行了一次 next 方法,但是该方法每次调用最多只能匹配一个中间件 从上述中间件的执行流程中可以知晓, 「 用户注册的中间件方法在执行的时候都会包裹一层 try/catch,但是 try/catch 无法捕获 async 函数内部的异常,这也就是为什么 Express 中无法通过注册错误处理中间件来拦截到 async 语法声明的中间件的异常的原因 」 。用户注册的中间件方法在执行的时候都会包裹一层 try/catch,但是 try/catch 无法捕获 async 函数内部的异常,这也就是为什么 Express 中无法通过注册错误处理中间件来拦截到 async 语法声明的中间件的异常的原因修改源码
修改源码修改源码找到本质原因之后,可以通过修改源码的方法来进行适配:
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;

if (fn.length > 3) {

// not a standard request handler

return next();
}
// 针对 async 语法函数特殊处理
if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') {

return fn(req, res, next).catch(next);
}

try {

fn(req, res, next);
} catch (err) {

next(err);
}
};


Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;

if (fn.length > 3) {

// not a standard request handler

return next();
}
// 针对 async 语法函数特殊处理
if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') {

return fn(req, res, next).catch(next);
}

try {

fn(req, res, next);
} catch (err) {

next(err);
}
};

上述代码在 handle_request 方法内部判断了中间件方法通过 async 语法声明的情况,从而采用 Promise 对象的 catch 方法来向下传递异常。「 这种方式可以减少上层冗余的代码,但是实现该方式,可能需要 fork 一份 Express4.x 的源码,然后发布一个修改之后的版本,后续还要跟进官方版本的新特性,相应的维护成本非常高。 」 这种方式可以减少上层冗余的代码,但是实现该方式,可能需要 fork 一份 Express4.x 的源码,然后发布一个修改之后的版本,后续还要跟进官方版本的新特性,相应的维护成本非常高。express5.x 中将 router 部分剥离出了单独的路由库 -- routerAOP(面向切面编程)
AOP(面向切面编程)
AOP(面向切面编程)
为了解决上述方案存在的问题,我们可以尝试利用 AOP 技术在不修改源码的基础上对已有方法进行增强。
app.use(async function () {
const message = doSomething();
res.send(message);
})


app.use(async function () {
const message = doSomething();
res.send(message);
})

以注册应用级别中间件为例,可以对 app.use 方法进行 AOP 增强:
const originAppUseMethod = app.use.bind(app);
app.use = function (fn) {
if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') {

const asyncWrapper = function(req, res, next) {

fn(req, res, next).then(next).catch(next);

}

return originAppUseMethod(asyncWrapper);
}
return originAppUseMethod(fn);
}


const originAppUseMethod = app.use.bind(app);
app.use = function (fn) {
if (Object.prototype.toString.call(fn) === '[object AsyncFunction]') {

const asyncWrapper = function(req, res, next) {

fn(req, res, next).then(next).catch(next);

}

return originAppUseMethod(asyncWrapper);
}
return originAppUseMethod(fn);
}

前面源码分析的过程中,app.use 内部是有 this 调用的,所以这里需要 「 利用 bind 方法来避免后续调用过程中 this 指向出现问题。 」利用 bind 方法来避免后续调用过程中 this 指向出现问题。 然后就是利用 AOP 的核心思想,重写原始的 app.use 方法,通过不同的分支逻辑代理到原始的 app.use 方法上。「 该方法相比较修改源码的方式,维护成本低。但是缺点也很明显,需要重写所有可以注册中间件的方法,不能够像修改源码那样一步到位。 」该方法相比较修改源码的方式,维护成本低。但是缺点也很明显,需要重写所有可以注册中间件的方法,不能够像修改源码那样一步到位。写在最后
写在最后写在最后本文介绍了 Express 中使用 async 语法的四种解决方案:

try/catch

高阶函数

修改源码

AOP
try/catch高阶函数修改源码AOP除了 try/catch 方法性价比比较低,其它三种方法都需要根据实际情况去取舍,举个栗子:如果你需要写一个 Express 中间件提供给各个团队使用,那么修改源码的方式肯定走不通,而 AOP 的方式对于你的风险太大,相比较下,第二种方案是最佳的实践方案。