# 请求处理
# 介绍
Handler用于根据客户端请求路径进行针对性处理。
客户端请求进入ServiceCore后,将先后经过全局拦截器和全局中间件管道。当全局中间件管道中的最后一个中间件执行完成后,ServiceCore将自动创建与请求路径匹配的Handler实例并引导请求进入其中进行后续处理。
提示
Handler拥有独立于ServiceCore的中间件系统。因此,我们在Handler维度指定的中间件只作用于符合请求路径规则的客户端请求。
另外,Handler的中间件系统兼容Express生态且支持动态中间件,我们可以根据客户端请求的实际上下文(比如:请求参数):
- 动态指定中间件列表
- 控制中间件执行规则(比如:跳过执行)
在Web服务-设置请求路径一章中我们已经了解到,自定义Handler需要实现一个继承自Core.Handler
的类。本质上Core.Handler
是一个包含了核心处理流程的抽象类,我们通过实现抽象类内的HOOK方法以定制请求处理流程的各个环节。
下面,我们将分类介绍Core.Handler
中推荐被重写的方法:
# 请求路径
static getRoutePath()
使用场景:设置Handler的请求路径规则。ServiceCore处理客户端请求时,将根据请求路径指定完成实际处理动作的Handler实例。
调用时机:ServiceCore执行实例方法
bind()
时调用此方法获取Handler的请求路径规则。注意事项:重写时无需执行
super
操作,使用return
返回请求路径规则。注意
ServiceCore将自动校正Handler的请求路径规则:当请求路径规则不以
'/'
开头时,附加'/'
作为前缀。因此,我们配置请求路径规则时应尽量以
'/'
开头。默认行为:设置请求路径规则为
'/'
。static getRoutePath() { return '/'; }
1
2
3
# 生命周期
initHandler(req, res, next)
使用场景:指定Handler初始化逻辑。
调用时机:ServiceCore处理每个客户端请求时,都将创建与请求路径匹配的Handler实例并调用此方法触发Handler初始化。
注意事项:重写时无需执行
super
操作,当执行过程中产生异常时将触发统一错误处理。提示
我们在实现Handler初始化逻辑时,通常不直接操作客户端返回实例
res
,而是通过流程控制函数控制请求处理链路。另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为
Function
或AsyncFunction
。默认行为:直接调用流程控制函数进入下一处理阶段。
initHandler(req, res, next) { next(); }
1
2
3
destroyHandler(req, res)
# 中间件系统
getMiddlewares(req, res)
使用场景:根据客户端请求的实际上下文(比如:请求参数)动态指定Handler中间件列表。
调用时机:在Handler初始化完成后将进入中间件阶段,在开始分发Handler中间件前将调用此方法获取中间件列表。
注意事项:重写时无需执行
super
操作,使用return
返回中间件列表即可;当执行过程中产生异常时将触发统一错误处理。提示
我们在指定Handler的中间件列表时,通常不直接操作客户端返回实例
res
。另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为
Function
或AsyncFunction
。默认行为:返回
[]
。getMiddlewares(req, res) { return []; }
1
2
3
onInterceptMiddleware(middleware, req, res, next)
使用场景:动态中间件的核心HOOK方法,提供了在分发中间件时控制其执行行为的能力。
调用时机:中间件阶段分发每个中间件时,都将调用此方法完成中间件的实际执行行为。
注意事项:重写时无需执行
super
操作,当执行过程中产生异常时将触发统一错误处理。提示
我们在实现动态中间件时,通常不直接操作客户端返回实例
res
,而是通过流程控制函数控制中间件的执行链路。在中间件拦截阶段,我们可以根据当前分发中间件
middleware.type
的类型,决定是否调用中间件的执行函数middleware.exec
以实现对中间件执行行为的动态控制。默认行为:执行中间件并将其结果作为执行流程控制函数的参数。
onInterceptMiddleware(middleware, req, res, next) { const { exec } = middleware; exec((result) => next(result)); }
1
2
3
4
# 请求处理
preHandler(req, res, next)
[METHOD]Handler(req, res, next)
defaultHandler(req, res, next)
# 统一处理
onFinish(data, req, res)
使用场景:对客户端请求处理完成后的逻辑进行统一处理。
注意事项:重写时无需执行
super
操作,当执行过程中产生异常时将触发统一错误处理。提示
我们在指定统一完成处理逻辑时,通常应直接操作客户端返回实例
res
,向客户端返回处理结果。另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为
Function
或AsyncFunction
。默认行为:操作客户端返回实例
res
,向客户端返回处理结果。onFinish(data, req, res) { // 当请求已返回时不再执行实际逻辑 if (this.isEnded) { return; } // 当data为null或undefined时 - 返回204 else if (isNullOrUndefined(data)) { res.status(204).end(); } // 当data为Number类型时 - data作为状态码 else if (getType(data) === VALUE_TYPE_NUMBER) { res.status(data).end(); } // 其他情况 - data作为应答内容 else { res.status(200).send(data); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onError(error, req, res)
使用场景:对客户端请求处理过程中产生的异常进行统一处理。
调用时机:在任意请求处理阶段的跟函数中产生了未被捕获的异常,或使用流程控制函数执行
next(error)
时,将调用此方法触发统一错误处理。注意事项:重写时无需执行
super
操作,当执行过程中产生异常时将触发ServiceCore中的错误拦截器。提示
我们在指定统一错误处理逻辑时,通常应直接操作客户端返回实例
res
,向客户端返回处理结果。另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为
Function
或AsyncFunction
。默认行为:操作客户端返回实例
res
,向客户端返回500状态码。onError(error, req, res) { !this.isEnded && res.status(500).end(); }
1
2
3
# 处理流程
# 流程控制函数
ServiceCore自动为每个客户端请求创建与请求路径对应的Handler实例,并调用其私有实例方法_onStart()
以启动处理流程。
注意
Handler的实例方法_onStart()
中实现了处理流程控制逻辑。因此,我们在自定义Handler时,一定不要使用_onStart
作为实例方法名。
通常,我们仅在指定统一完成处理和统一错误处理阶段直接操作客户端返回实例res
,其余阶段中使用next
进行流程控制:
执行
next(data)
时:触发统一完成处理,data
将作为实例方法onFinish()
参数列表的第一个入参。提示
默认的统一完成处理逻辑使流程控制函数支持多种调用方式向客户端返回请求处理结果:
next(message)
:当流程控制函数指定的
data
不为Number
类型、null
和undefined
时,认为期望向客户端返回处理结果报文。此时,将向客户端返回200状态码,并将
data
作为返回报文。next(httpCode)
:当流程控制函数指定的
data
为Number
类型时,认为期望向客户端返回状态码。此时,将向客户端返回
data
中指定的状态码和空报文。next()
、next(null)
、next(undefined)
:仅在请求后处理阶段执行
next()
时命中此逻辑,其余处理阶段中执行next()
将分发至下一处理阶段。当流程控制函数指定的
data
为null
或undefined
时,认为期望向客户端返回空报文。此时,将向客户端返回204状态码和空报文。
我们可以通过指定统一完成处理阶段的逻辑以定制流程控制函数向客户端返回请求处理结果的方式。
执行
next(error)
时:触发统一错误处理,error
将作为实例方法onError()
参数列表的第一个入参。执行
next()
、next(null)
、next(undefined)
时:进入下一处理阶段。
提示
Handler的流程控制函数与Express的中间件分发函数拥有一致的使用体验。因此,我们可以在Handler的中间件系统直接使用Express的中间件生态。
不得不提的是,在自定义Handler时,我们应按照Handler处理阶段对业务逻辑进行拆分以实现各个HOOK方法,并在期望阶段处理结束时使用next()
分发处理流程。
虽然在实现上,Handler使用洋葱圈模型串联了每个请求处理阶段,理论上可以通过await next()
进行二次穿透。
# 设置请求路径
在Web服务-设置请求路径一章中,我们已经了解了如何通过重写Handler的静态方法getRoutePath()
以指定Handler的请求路径规则。
接下来,我们将重点讨论指定请求路径规则时的注意事项:
请求路径规则必须为非空字符串。
ServiceCore在执行
bind()
时,将对Handler中指定的请求路径规则进行校验,如果为非字符串型值或空字符串将跳过对此Handler的挂载。请求路径规则应尽量使用
'/'
开头。ServiceCore在执行
bind()
时,还将对Handler中指定的请求路径规则进行校正,如果不以'/'
开头时,将自动附加'/'
作为前缀。使用前缀型请求路径规则场景下,在业务层执行
bind()
时,应将指定了更长请求路径规则的Handler放置于绑定列表中较前的位置。ServiceCore匹配与客户端请求对应的Handler时,将按照执行
bind()
时的指定的Handler顺序依次进行。注意
比如,业务层在执行
bind()
时依次绑定了两个Handler,其请求路径规则分别为'/api'
和'/api/Test.do'
。此时当客户端请求路径为
/api/Test.do
时,ServiceCore将优先匹配到请求路径规则为/api
的Handler用于此次请求处理。因此,我们在使用前缀型请求路径规则场景下,应在执行
bind()
时关注绑定列表中的Handler顺序。
# 初始化和析构
Handler的初始化和析构标志着Handler生命周期的开始和结束:
- 当ServiceCore分发客户端请求进入与请求路径匹配的Handler时,首先将触发Handler初始化。
- 在Handler初始化触发后(非初始化执行完成后),客户端请求返回时将触发Handler析构。
提示
通常,我们仅在统一完成处理和统一错误处理中操作客户端返回实例res
,向客户端返回请求处理结果以触发Handler析构。
当然也有例外,比如在Handler中使用静态资源中间件express.static
,当客户端请求命中静态资源时将直接返回该资源,此时也将触发Handler析构。
我们应在Handler初始化与Handler析构阶段执行相反的资源操作,以使内存得到有效释放。
# Handler初始化
我们通过重写Handler的实例方法initHandler()
以指定Handler初始化阶段执行的逻辑。
出于复用性考虑,推荐在Handler初始化阶段执行一些与业务无关的通用初始化逻辑,比如:创建日志输出器、读取全局配置等;与业务相关的初始化逻辑推荐放入请求预处理阶段中。
提示
通常,我们将Handler初始化阶段中创建的资源提升为实例属性,以在各个请求处理阶段中共享:
initHandler(req, res, next) {
// 创建基础输出器并提升为实例属性
this.logger = new Core.BaseLogger();
// 分发至下一处理阶段
next();
}
2
3
4
5
6
Handler内置的异常捕获套件将自动作用在实例方法initHandler
维度。即:当initHandler
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据Handler初始化阶段中实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的initHandler
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当Handler初始化逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
当然,不要忘记在Handler初始化阶段结束时使用流程控制函数分发处理流程至下一阶段。
接下来,我们将实现一个在处理请求时打印请求处理开始时间的Handler。
首先,创建一个用于模拟耗时同步和异步任务的BaseHandler
作为基类:
class BaseHandler extends Core.Handler {
// 异步耗时任务
asyncTask(duration = 0) {
return new Promise((resolve) => {
setTimeout(() => resolve(), duration);
});
}
// 同步耗时任务
syncTask(duration = 0) {
const startDate = new Date();
while (true) {
if (((new Date()) - startDate) > duration) {
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接下来,基于BaseHandler
实现在请求处理时打印开始时间的自定义Handler:
对于只包含同步行为的Handler初始化逻辑,我们使用Function
类型的initHandler
:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
initHandler(req, res, next) {
// 记录请求时间
this.startDate = new Date();
// 创建日志输出器并打印日志
this.logger = new Core.BaseLogger();
this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
// 执行1000ms的同步任务
this.syncTask(1000);
next();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
同样,当Handler初始化逻辑中包含异步行为时,我们使用AsyncFunction
类型的initHandler
,并通过await
关键字执行异步任务:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
async initHandler(req, res, next) {
// 记录请求时间
this.startDate = new Date();
// 创建日志输出器并打印日志
this.logger = new Core.BaseLogger();
this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
// 执行1000ms的异步任务
await this.asyncTask(1000);
next();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最后,我们将自定义Handler挂载至ServiceCore并启动服务:
const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
2
3
最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do -w 'res -> %{http_code}\n'
检查实现效果:
服务端控制台将于收到请求时打印请求开始处理时间的相关日志。
客户端控制台将于请求发起后约1000ms收到ServiceCore返回的404状态码。
# Handler析构
我们通过重写Handler的实例方法destroyHandler()
以指定Handler析构阶段执行的逻辑。
注意
进入Handler析构阶段时,客户端请求已返回处理结果。
因此,我们在Handler析构阶段仅允许对客户端请求实例req
和客户端返回实例res
发起读取动作。
通常,我们在Handler析构阶段释放对Handler初始化阶段创建资源的引用。
当然,不要忘记在Handler初始化阶段结束时使用流程控制函数分发处理流程至下一阶段。
同样,Handler内置的异常捕获套件也将自动作用在实例方法destroyHandler
维度。即:当destroyHandler
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据Handler析构阶段中实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的destroyHandler
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当Handler析构逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
Handler析构阶段标志着Handler生命周期的终结,其中无法使用流程控制函数。
接下来,我们将完善Handler初始化阶段中的🌰,使其在请求处理结束时执行任务并打印处理耗时。
对于只包含同步行为的Handler析构逻辑,我们使用Function
类型的destroyHandler
:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
initHandler(req, res, next) {
// 记录请求时间
this.startDate = new Date();
// 创建日志输出器并打印日志
this.logger = new Core.BaseLogger();
this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
// 执行1000ms的同步任务
this.syncTask(1000);
next();
}
destroyHandler(req, res) {
// 执行1000ms的同步任务
this.syncTask(1000);
// 计算并打印日志
const duration = (new Date()) - this.startDate;
this.logger.log(`请求处理耗时[${duration}]ms`);
// 关闭日志输出器
this.logger.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
同样,当Handler析构逻辑中包含异步行为时,我们使用AsyncFunction
类型的destroyHandler
,并通过await
关键字执行异步任务:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
async initHandler(req, res, next) {
// 记录请求时间
this.startDate = new Date();
// 创建日志输出器并打印日志
this.logger = new Core.BaseLogger();
this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
// 执行1000ms的异步任务
await this.asyncTask(1000);
next();
}
async destroyHandler(req, res) {
// 执行1000ms的同步任务
await this.asyncTask(1000);
// 计算并打印日志
const duration = (new Date()) - this.startDate;
this.logger.log(`请求处理耗时[${duration}]ms`);
// 关闭日志输出器
this.logger.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
最后,我们打开控制台,使用curl http://localhost:3000/Test.do -w 'res -> %{http_code}\n'
检查实现效果:
服务端控制台将于收到请求时打印请求开始处理时间的相关日志。
服务端控制台将于收到请求后约2000ms打印请求处理时长的相关日志。
客户端控制台将于请求发起后约1000ms收到ServiceCore返回的404状态码。
# 中间件系统
Handler使用的中间件系统兼容Express生态,且支持动态中间件。
我们通过重写Handler的实例方法getMiddlewares
以指定其应用于客户端请求的中间件列表。
同样,Handler内置的异常捕获套件也将自动作用在实例方法getMiddlewares
维度。即:当getMiddlewares
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据构建中间件列表时实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的getMiddlewares
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当构建中间件列表逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
构建中间件列表时无需使用流程控制函数控制处理流程,使用return
关键字返回应用于客户端请求处理的中间件列表即可。
接下来,我们将实现一个在请求处理过程中动态指定中间件列表的Handler。
首先,创建一个用于模拟耗时同步、异步任务和创建中间件的BaseHandler
作为基类:
class BaseHandler extends Core.Handler {
// 异步任务
asyncTask(duration = 0) {
return new Promise((resolve) => {
setTimeout(() => resolve(), duration);
});
}
// 同步任务
syncTask(duration = 0) {
const startDate = new Date();
while (true) {
if (((new Date()) - startDate) > duration) {
break;
}
}
}
// 创建中间件
createMiddleware(value) {
// 将在res.header中自动记录应用于请求处理的中间件
return (req, res, next) => {
const middlewareFieldKey = 'x-middlewares';
const middlewareFieldValue = res.get(middlewareFieldKey);
const middlewareList = middlewareFieldValue ? middlewareFieldValue.split(',') : [];
middlewareList.push(`middleware_${value}`);
res.set(middlewareFieldKey, middlewareList.join(','));
next();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
接下来,基于BaseHandler
实现根据客户端请求入参动态指定中间件列表的自定义Handler:
对于只包含同步行为的构建中间件列表逻辑,我们使用Function
类型的getMiddlewares
:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
getMiddlewares(req, res) {
// 执行1000ms的同步任务
this.syncTask(1000);
// 构建中间件列表
const middlewares = [];
const { count = 0 } = req.query;
for (let i = 1; i < parseInt(count) + 1; i++) {
middlewares.push(this.createMiddleware(i));
}
return middlewares;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
同样,当构建中间件列表逻辑中包含异步行为时,我们使用AsyncFunction
类型的getMiddlewares
,并通过await
关键字执行异步任务:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
async getMiddlewares(req, res) {
// 执行1000ms的异步任务
await this.asyncTask(1000);
// 构建中间件列表
const middlewares = [];
const { count = 0 } = req.query;
for (let i = 1; i < parseInt(count) + 1; i++) {
middlewares.push(this.createMiddleware(i));
}
return middlewares;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最后,我们将自定义Handler挂载至ServiceCore并启动服务:
const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
2
3
最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?count\=5 -D -
检查实现效果:
客户端控制台将于请求发起后约1000ms打印请求处理结果。
客户端收到的请求处理结果中
res.header['x-middlewares']
内记录了5个中间件的信息。
# 中间件拦截
Handler构建中间件列表完成后,将逐个分发其中的中间件。
我们通过重写实例方法onInterceptMiddleware
以拦截每个中间件的分发以动态控制其执行行为。
同样,Handler内置的异常捕获套件也将自动作用在实例方法onInterceptMiddleware
维度。即:当onInterceptMiddleware
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据中间件拦截时实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的onInterceptMiddleware
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当中间件拦截逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
Handler将自动包装当前分发的中间件和中间件执行函数作为onInterceptMiddleware
参数列表的第一个参数middleware
:
middleware.type
:中间件本体函数,用于进行中间件标识校验。middleware.exec
:中间件执行函数,用于实际执行中间件,其参数列表为(callBack)
。提示
middleware.exec(callBack)
实际上是middleware.type(req, res, callBack)
的语法糖,中间件执行过程中产生的异常将通过callBack
回抛。我们可以通过
require('util').promisify
包装middleware.exec
,并使用await
关键字调用,以保证在AsyncFuntion
类型的onInterceptMiddleware
中的逻辑一致性。
当然,在对中间件进行拦截处理后,不要忘记使用流程控制函数分发处理流程至下一阶段。
接下来,我们将完善构建中间件列表时的🌰,以50%的概率随机执行中间件列表中的中间件。
对于只包含同步行为的中间件拦截逻辑,我们使用Function
类型的onInterceptMiddleware
:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
getMiddlewares(req, res) {
// 执行1000ms的同步任务
this.syncTask(1000);
// 构建中间件列表
const middlewares = [];
const { count = 0 } = req.query;
for (let i = 1; i < parseInt(count) + 1; i++) {
middlewares.push(this.createMiddleware(i));
}
return middlewares;
}
onInterceptMiddleware(middleware, req, res, next) {
// 每个中间件都执行500ms的同步任务
this.syncTask(500);
// 计算概率并应用至执行链路
const { exec } = middleware;
const canExec = Math.random() >= 0.5;
canExec ? exec((result) => next(result)) : next();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
同样,当构中间件拦截逻辑中包含异步行为时,我们使用AsyncFunction
类型的onInterceptMiddleware
,并通过await
关键字执行异步任务:
const { promisify } = require('util');
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
async getMiddlewares(req, res) {
// 执行1000ms的异步任务
await this.asyncTask(1000);
// 构建中间件列表
const middlewares = [];
const { count = 0 } = req.query;
for (let i = 1; i < parseInt(count) + 1; i++) {
middlewares.push(this.createMiddleware(i));
}
return middlewares;
}
async onInterceptMiddleware(middleware, req, res, next) {
// 每个中间件都执行500ms的异步任务
await this.asyncTask(500);
// 计算概率并应用至执行链路
const { exec } = middleware;
const canExec = Math.random() >= 0.5;
canExec
? next(await promisify(exec)())
: next();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?count\=5 -D -
检查实现效果:
客户端控制台将于请求发起后约3500ms打印请求处理结果。
客户端收到的请求处理结果中
res.header['x-middlewares']
内随机记录了0-5个中间件的信息。
# 请求预处理
Handler维度的中间件列表内最后一个中间件的拦截逻辑执行完成后,将进入请求预处理阶段。
我们通过重写Handler的实例方法preHandler()
以指定请求预处理阶段执行的逻辑。
提示
通常,我们在请求预处理阶段执行实际业务处理前的准备动作。
比如,在实现支持客户端使用多种请求方式的自定义Handler时,可以在请求预处理阶段对不同请求方式带入的参数进行归并统一。
或是提前创建请求处理所需的基础资源。
我们通常在请求后处理阶段对请求参数进行有效性校验并驳回参数无效的客户端请求。因此,出于性能考虑,我们应在请求预处理阶段创建与客户端请求类型无关的基础资源。
同样,Handler内置的异常捕获套件也将自动作用在实例方法preHandler
维度。即:当preHandler
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据请求预处理中实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的preHandler
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当请求预处理逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
当然,不要忘记在请求预处理阶段结束时使用流程控制函数分发处理流程至下一阶段。
接下来,我们将实现一个在预处理阶段归并请求参数并向客户端返回的Handler。
首先,创建一个用于模拟耗时同步和异步任务的BaseHandler
作为基类:
class BaseHandler extends Core.Handler {
// 异步耗时任务
asyncTask(duration = 0) {
return new Promise((resolve) => {
setTimeout(() => resolve(), duration);
});
}
// 同步耗时任务
syncTask(duration = 0) {
const startDate = new Date();
while (true) {
if (((new Date()) - startDate) > duration) {
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接下来,基于BaseHandler
实现自动归并GET方式和POST方式中客户端附加至query
和body
中参数的自定义Handler:
对于只包含同步行为的请求预处理逻辑,我们使用Function
类型的preHandler
:
const bodyParser = require('body-parser');
const jsonParserMiddleware = bodyParser.json({ limit: 2 * 1024 * 1024 });
const qsParserMiddleware = bodyParser.urlencoded({ limit: 2 * 1024 * 1024, extended: true });
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
getMiddlewares() {
// 挂载解析HTTP BODY的中间件
return [jsonParserMiddleware, qsParserMiddleware];
}
preHandler(req, res, next) {
// 执行1000ms的同步任务
this.syncTask(1000);
// 合并query和body
const body = req.body;
const query = req.query;
req.requestParams = Object.assign({}, body, query);
// 向客户端返回
next(req.requestParams);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
同样,当请求预处理逻辑中包含异步行为时,我们使用AsyncFunction
类型的preHandler
,并通过await
关键字执行异步任务:
const bodyParser = require('body-parser');
const jsonParserMiddleware = bodyParser.json({ limit: 2 * 1024 * 1024 });
const qsParserMiddleware = bodyParser.urlencoded({ limit: 2 * 1024 * 1024, extended: true });
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
getMiddlewares() {
// 挂载解析HTTP BODY的中间件
return [jsonParserMiddleware, qsParserMiddleware];
}
async preHandler(req, res, next) {
// 执行1000ms的异步任务
await this.asyncTask(1000);
// 合并query和body
const body = req.body;
const query = req.query;
req.requestParams = Object.assign({}, body, query);
// 向客户端返回
next(req.requestParams);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
最后,我们将自定义Handler挂载至ServiceCore并启动服务:
const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
2
3
最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?queryKey1\=queryValue1\&queryKey2\=queryValue2 -d 'bodyKey1=bodyValue1&bodyKey2=bodyValue2'
检查实现效果:
客户端控制台将于请求发起后约1000ms打印请求处理结果。
客户端收到的请求处理结果中将包含
query
和body
中附带的参数。
# 请求后处理
在请求预处理阶段使用流程控制函数分发至下一处理阶段时,Handler将尝试调用与客户端请求方式匹配的实例方法进入请求后处理阶段。
我们通过重写Handler中与请求方式对应的实例方法以指定其请求后处理逻辑。比如,实现实例方法postHandler
将指定客户端POST请求对应的后处理逻辑。
提示
在进入请求后处理阶段时,如果Handler中没有实现与请求方式对应的实例方法,默认使用defaultHandler
作为后处理逻辑。
默认的defaultHandler
逻辑中,将直接使用流程控制函数执行next(404)
向客户端返回404状态码。
同样,Handler内置的异常捕获套件也将自动作用在[METHOD]Handler
维度。即:全部请求方式对应的实例方法和defaultHandler
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据请求后处理中实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的[METHOD]Handler
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当请求后处理逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
通常,在请求后处理阶段首先应对客户端附带的参数进行有效性判断,驳回参数无效的客户端请求;在请求参数有效时,触发实际的业务处理。
需要注意的是,请求后处理阶段为请求处理链路的末端节点,通常应使用流程控制函数向客户端发起应答动作。另外,在请求后处理阶段执行next()
时不会继续分发处理流程至下一阶段,而是进入统一完成处理。
接下来,我们将实现一个支持客户端通过POST请求读取本地指定目录的Handler。
对于只包含同步行为的请求后处理逻辑,我们使用Function
类型的postHandler
:
const fs = require('fs');
class Handler extends Core.Handler {
static getRoutePath() {
return '/Test.do';
}
postHandler(req, res, next) {
const { path } = req.query;
try {
// 同步读取目录内容并向客户端返回结果
const result = fs.readdirSync(path);
next(result);
} catch (error) {
// 执行过程中产生异常时向客户端返回异常信息
next(error.message);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
同样,当请求后处理逻辑中包含异步行为时,我们使用AsyncFunction
类型的postHandler
,并通过await
关键字执行异步任务:
const fs = require('fs');
const { promisify } = require('util');
class Handler extends Core.Handler {
static getRoutePath() {
return '/Test.do';
}
async postHandler(req, res, next) {
const { path } = req.query;
// 异步读取目录内容并向客户端返回结果
const result = await promisify(fs.readdir)(path).catch((error) => {
// 执行过程中产生异常时向客户端返回异常信息
next(error.message);
});
result && next(result);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
最后,我们将自定义Handler挂载至ServiceCore并启动服务:
const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
2
3
最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?path\=[本地目录绝对路径]
检查实现效果:
当指定的本地目录绝对路径存在时,客户端收到的处理结果为本地目录中的内容。
当未指定本地目录绝对路径或本地目录绝对路径不存在时,客户端收到的处理结果为异常原因。
注意
样例代码中使用的异常处理方式不推荐在实际业务代码中使用。对于如何优雅的处理异常,我们将在统一错误处理中进行详细讨论。
# 统一完成处理
在任意请求处理阶段中使用流程控制函数执行next(data)
时将触发统一完成处理。
我们通过重写Handler的实例方法onFinish()
以指定统一完成处理阶段执行的逻辑。
说明
我们通常在统一完成处理阶段完成对客户端返回内容的统一结构组装,并操作客户端返回实例res
向客户端返回处理结果。
需要注意的是,直接操作客户端返回实例res
发起返回动作将跳过统一完成处理阶段直接触发Handler析构。
因此,我们在请求处理过程中期望向客户端返回处理结果时应使用流程控制函数,而不是直接操作客户端返回实例res
。
同样,Handler内置的异常捕获套件也将自动作用在实例方法onFinish
维度。即:当onFinish
执行过程中产生了未被捕获的异常时将自动进入统一错误处理。
因此,我们应根据请求预处理中实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的onFinish
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当统一完成处理逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
接下来,我们将完善请求后处理中的🌰,为其向客户端的返回内容添加统一结构。
首先,创建一个用于模拟耗时同步和异步任务的BaseHandler
作为基类:
class BaseHandler extends Core.Handler {
// 异步耗时任务
asyncTask(duration = 0) {
return new Promise((resolve) => {
setTimeout(() => resolve(), duration);
});
}
// 同步耗时任务
syncTask(duration = 0) {
const startDate = new Date();
while (true) {
if (((new Date()) - startDate) > duration) {
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接下来,我们将自定义Handler继承的基类变更为BaseHandler
,并在统一完成处理中完成报文结构包装:
对于只包含同步行为的请求预处理逻辑,我们使用Function
类型的onFinish
:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
postHandler(req, res, next) {
const { path } = req.query;
try {
// 同步读取目录内容并向客户端返回结果
const result = fs.readdirSync(path);
next(result);
} catch (error) {
// 执行过程中产生异常时向客户端返回异常信息
next(error.message);
}
}
onFinish(data, req, res) {
// 执行1000ms的同步任务
this.syncTask(1000);
// 构造报文结构并执行原始的onFinish()方法向客户端返回
const backMessage = { code: 0, data };
super.onFinish(backMessage, req, res);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
同样,当请求后处理逻辑中包含异步行为时,我们使用AsyncFunction
类型的onFinish
,并通过await
关键字执行异步任务:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
async postHandler(req, res, next) {
const { path } = req.query;
const result = await promisify(fs.readdir)(path).catch((error) => {
next(error.message);
});
result && next(result);
}
async onFinish(data, req, res) {
// 执行1000ms的异步任务
await this.asyncTask(1000);
// 构造报文结构并执行原始的onFinish()方法向客户端返回
const backMessage = { code: 0, data };
super.onFinish(backMessage, req, res);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?path\=[本地目录绝对路径]
检查实现效果:
客户端控制台将于请求发起后约1000ms打印请求处理结果,且处理结果拥有统一的结构
{ code: 0, data: [处理结果] }
。当指定的本地目录绝对路径存在时,客户端收到的处理结果为本地目录中的内容。
当未指定本地目录绝对路径或本地目录绝对路径不存在时,客户端收到的处理结果为异常原因。
注意
样例代码中使用的异常处理方式不推荐在实际业务代码中使用。对于如何优雅的处理异常,我们将在统一错误处理中进行详细讨论。
# 统一错误处理
任意请求处理阶段的根函数中产生未被捕获的异常时,Handler将自动引导处理流程进入统一完成处理阶段。
另外,我们可以在请求处理任意阶段中使用流程控制函数执行next(error)
手动触发统一完成处理。
我们通过重写Handler的实例方法onError
以指定统一完成处理阶段执行的逻辑。
说明
我们通常在统一错误处理阶段处理异常后,直接操作客户端返回实例res
向客户端返回处理结果。
同样,Handler内置的异常捕获套件也将自动作用在实例方法onError
维度。与其他处理阶段不同的是,当onError
执行过程中产生了未被捕获的异常时将进入ServiceCore的错误拦截器。
因此,我们应根据请求预处理中实际逻辑的同步或异步,选择使用Function
或AsyncFunction
类型的onFinish
。
注意
Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。
因此,当统一错误处理逻辑中包含异步任务时,我们可以使用async/await
和Promise
处理异步任务,以保证异常可以正常抛出。
接下来,我们将使用更优雅的异常处理方案来完善请求后处理中的🌰。
首先,创建一个用于模拟耗时同步和异步任务的BaseHandler
作为基类:
class BaseHandler extends Core.Handler {
// 异步耗时任务
asyncTask(duration = 0) {
return new Promise((resolve) => {
setTimeout(() => resolve(), duration);
});
}
// 同步耗时任务
syncTask(duration = 0) {
const startDate = new Date();
while (true) {
if (((new Date()) - startDate) > duration) {
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接下来,我们将自定义Handler继承的基类变更为BaseHandler
,删除Handler后处理中异常处理相关的代码,并在统一错误处理中收口异常处理:
对于只包含同步行为的请求预处理逻辑,我们使用Function
类型的onError
:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
postHandler(req, res, next) {
const { path } = req.query;
// 同步读取目录内容并向客户端返回结果
const result = fs.readdirSync(path);
next(result);
}
onError(error, req, res) {
// 执行1000ms的同步任务
this.syncTask(1000);
// 向客户端返回异常信息
res.status(500).send(error.message);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
同样,当请求后处理逻辑中包含异步行为时,我们使用AsyncFunction
类型的onError
,并通过await
关键字执行异步任务:
class Handler extends BaseHandler {
static getRoutePath() {
return '/Test.do';
}
async postHandler(req, res, next) {
const { path } = req.query;
const result = await promisify(fs.readdir)(path);
next(result);
}
async onError(error, req, res) {
// 执行1000ms的异步任务
await this.asyncTask(1000);
// 向客户端返回异常信息
res.status(500).send(error.message);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?path\=[不存在的本地目录绝对路径]
检查实现效果:
- 客户端控制台将于请求发起后约1000ms打印请求处理结果,处理结果为异常原因。