前置---node服务理解
node服务MVP:本质上就是利用http模块,通过response.end响应请求
从node服务的角度上看,GET、POST等所有请求方法以及接口,只是request.method和request.url的区别,但本质都是走的http.createServer的回调函数,说白了接口定义的方式,无论是url亦或是请求方法的隔离,都是框架层面(koa,express...)基于node的http模块做的上层封装
js
const http = require('http')
const serverHandler = (request, response) => {
response.end('Hello World' + '@' + request.method + '@' + request.url)
}
http
.createServer(serverHandler)
.listen(8888, _ => console.log('Server run at http://127.0.0.1:8888'))
express中间件模式实现
支持:以请求方法+url的形式定义接口 & 多中间件逻辑串联
以
app.get('/', callback)
的方式注册接口,本质上就是通过this.route方法在this.handlers上挂载方法
tstype handlers = { GET: Array<() => void>; POST: Array<() => void>; };
callback的返回值即为node服务处理请求的回调,对于每个中间件,都用next方法进行包装增强,目的只有一个,为中间件回调函数增加除了request, response之外的第三个参数next方法,以此控制下一个中间件的调用(是否调用/调用时机)
js
class App {
constructor() {
this.handlers = {}
this.get = this.route.bind(this, 'GET')
this.post = this.route.bind(this, 'POST')
}
route(method, path, ...handler) {
let pathInfo = (this.handlers[path] = this.handlers[path] || {})
pathInfo[method] = handler
}
callback() {
return (request, response) => {
let { url: path, method } = request
let handlers = this.handlers[path] && this.handlers[path][method]
if (handlers) {
let context = {}
function next(handlers, index = 0) {
handlers[index] &&
handlers[index].call(context, request, response, () =>
next(handlers, index + 1)
)
}
next(handlers)
} else {
response.end('404')
}
}
}
}
使用方式
js
const http = require("http");
const app = new App();
function generatorId(request, response, next) {
this.id = 123
next()
}
app.get('/', generatorId, function(request, response) {
response.end(`Hello World ${this.id}`)
})
http.createServer(app.callback()).listen(3001, () => {
console.log("listening on 3001");
});
Koa中间件模式实现
🧅洋葱模型
同上封装,用next方法来包装所有中间件函数,做两件事情:对中间件回调的入参进行改造;对中间件的返回值进行改造:
- 中间件入参改造:第一个参数ctx为所有中间件共享的上下文对象;第二个参数next为调用下一个中间件的控制函数
- 中间件返回值改造:改造为promise,以实现中间件执行的🧅洋葱模型
next方法需要让中间件的调用顺序满足洋葱模型,算法角度如何理解洋葱模型?只考虑其中一个中间件,以最外层的,也就是第一个中间件为例:
- 中间件逻辑被
await next()
分割为上下两部分,上面部分属于"从外到内进入洋葱时经过当前层"执行的逻辑,下面部分属于"从内到外出去洋葱时经过当前层"执行的逻辑,逻辑界限就是是否执行完了下一层的逻辑- 站在某个内层中间件的角度思考:自身逻辑是否执行完成这个信息需要通过promise让外层拿到,也就是next返回值为promise,并在中间件回调执行完成后进行resolve即可
js
const http = require("http");
class App {
constructor() {
this.handlers = {};
this.get = this.route.bind(this, "GET");
this.post = this.route.bind(this, "POST");
}
route(method, path, ...handler) {
let pathInfo = (this.handlers[path] = this.handlers[path] || {});
// register handler
pathInfo[method] = handler;
}
callback() {
return (request, response) => {
let { url: path, method } = request;
let handlers = this.handlers[path] && this.handlers[path][method];
if (handlers) {
let context = { url: request.url };
function next(handlers, index = 0) {
return new Promise((resolve, reject) => {
if (!handlers[index]) return resolve();
handlers[index](context, () => next(handlers, index + 1)).then(
resolve,
reject
);
});
}
next(handlers).then((_) => {
// 结束请求
response.end(context.body || "404");
});
} else {
response.end("404");
}
};
}
}
使用方式
js
const app = new App();
const one = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
};
const two = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
};
const three = async (ctx, next) => {
console.log(5);
await next();
console.log(6);
};
app.get("/", one, two, three); // 请求后输出 1 3 5 6 4 2
http.createServer(app.callback()).listen(3001, () => {
console.log("listening on 3001");
});
洋葱模型实战场景
类似于spring框架中常见的基于aop的切面逻辑,我们可以利用洋葱模型编写一个错误统一处理的中间件:
这个切面统一捕获后续中间件的异常,并记录日志(addLogger)以及错误响应(ctx.body)
js
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
console.error(error);
if (ctx.body instanceof PassThrough) { // 流式响应
const msg = buildErrorResponseBody(error);
addLogger({
// ...ctx
});
ctx.body.write(`error: ${JSON.stringify(msg)}`);
setTimeout(() => {
ctx.body.end();
}, 1000);
} else { // 普通响应
addLogger({
// ...ctx
});
ctx.body = buildErrorResponseBody(error);
}
}
});