本次的简单实现重点在中间件的原理上(compose函数
),而其他部分的实现就忽略了很多细节,只是让代码能跑起来。
与express
不同,koa2
的路由处理与它本身是分开的(路由处理用koa-router
)。这里的内容不包括路由处理。
本次的实现内容包括koa的use
,listen
和中间件的洋葱圈模型。
koa2中间件使用的是洋葱圈模型
。这里附上一张网络上广为流传的图:
从这张图也能知道个大概意思:请求从最外层传到最里层,响应从最里层逐层往外传递。
下面来看一个经典的例子理解一下(如果之间不了解koa中间件机制的话,请跟着注释一步步地走):
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
// 1. request: 请求处理开始!执行这一行代码,然后往下走
console.log('第一层洋葱 - 开始')
// 2. request: 遇到一个异步函数,进入这个中间件
await next();
// 12. response: 第二个中间件执行结束后返回这里,代码往下执行,获得之前在ctx中存储的变量
const rt = ctx.response.get('X-Response-Time');
// 13. response: 控制台打印 请求方法 请求路径 响应时间
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
// 14. response: 所有中间件执行结束!
console.log('第一层洋葱 - 结束')
});
// x-response-time
app.use(async (ctx, next) => {
// 3. request: 从第一个中间件进入了这里,执行这一行代码,然后往下走
console.log('第二层洋葱 - 开始')
// 4. request: 继续执行代码,存储当前时间戳
const start = Date.now();
// 5. request: 又遇到一个异步函数,进入这第三个中间件
await next();
// 9. response: 第三个中间件执行结束后返回这里,代码往下执行,获得 ms 变量:第三个中间件的执行时间
const ms = Date.now() - start;
// 10. response: 设置 ctx属性,值可以从ctx.response中拿到
ctx.set('X-Response-Time', `${ms}ms`);
// 11. response: 第二个中间件执行结束,返回到第一个中间件的next()下方
console.log('第二层洋葱 - 结束')
});
// response
app.use(async ctx => {
// 6. request: 从第二个中间件进入到了这里,执行这一行代码,然后往下走
console.log('第三层洋葱 - 开始')
// 7. 收到响应数据,将 Hello World 返回给前端
ctx.body = 'Hello World';
// 8. response: 第三个中间件执行结束,响应阶段开始,返回到第二个中间件的 next() 下方
console.log('第三层洋葱 - 结束')
});
// 该函数优先于上面所有函数的执行!
app.listen(8000, () => {
console.log('server is listening at 8000');
});
执行代码后,在浏览器中输入http://localhost:8000
,控制台打印出:
server is listening at 8000
第一层洋葱 - 开始
第二层洋葱 - 开始
第三层洋葱 - 开始
第三层洋葱 - 结束
第二层洋葱 - 结束
GET / - 7ms
第一层洋葱 - 结束
浏览器中显示Hello World
。
这里遵循koa2
的写法直接导出一个类。
class MiniKoa2 {
}
module.exports = MiniKoa2;
用一个数组来存放所有中间件。
constructor() {
this.middlewares = [];
}
koa2
中提供一个use
函数用来注册中间件,函数参数为中间件函数。因此在use
中我们直接将中间件放入数组中,最后返回this
以支持链式调用。
use(fn) {
this.middlewares.push(fn);
return this; // 支持链式调用
}
在这个方法中,我们需要创建一个http服务,并把处理路由的回调函数传进去。这里创建一个私有函数_callback作为这个回调函数:
const http = require('http');
const _callback = Symbol();
listen
函数:
listen(...args) {
const server = http.createServer(this[_callback]());
server.listen(...args);
}
_callback
方法_callback
方法作为创建 http 服务的回调函数,将返回一个拥有req
和res
参数的函数。
[_callback]() {
// 组合中间件
const fn = this[_compose](this.middlewares);
return (req, res) => {
// 创建ctx
const ctx = this[_createContext](req, res);
// 执行fn
return this[_handleRequest](ctx, fn);
}
}
// 核心代码:组合中间件,实现next机制
[_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!')
}
return (ctx, next) => {
// 上一个执行的中间件
let index = -1;
// 首先执行第一个中间件
return dispatch(0);
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next()被多次调用'));
index = i;
// 当前执行的中间件
let fn = middleware[i];
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
// 最终返回一个Promise对象
try {
// 按规范来说fn应该为一个Promise函数
// 但为了兼容fn不是Promise函数的情况,就再用Promise包裹一层
// 使用bind是为了让dispatch不执行,直接作为函数传入fn
return Promise.resolve(
// 执行当前中间件
// 中间件的参数为(ctx, next)
fn(ctx, dispatch.bind(null, i + 1))
)
} catch (err) {
return Promise.reject(err);
}
}
}
}
[_createContext](req, res) {
// 这里只写了很少的一部分
const ctx = {
req,
res
}
ctx.method = req.method;
ctx.url = req.url;
return ctx;
}
[_handleRequest](ctx, fn) {
// 简单实现
return fn(ctx);
}
const http = require('http');
// 声明私有方法
const _compose = Symbol();
const _callback = Symbol();
const _createContext = Symbol();
const _handleRequest = Symbol();
class MiniKoa2 {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
return this; // 支持链式调用
}
listen(...args) {
const server = http.createServer(this[_callback]());
server.listen(...args);
}
[_callback]() {
const fn = this[_compose](this.middlewares);
return (req, res) => {
const ctx = this[_createContext](req, res);
return this[_handleRequest](ctx, fn);
}
}
// 核心代码:组合中间件,实现next机制
[_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!')
}
return (ctx, next) => {
// 上一个执行的中间件
let index = -1;
// 首先执行第一个中间件
return dispatch(0);
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next()被多次调用'));
index = i;
// 当前执行的中间件
let fn = middleware[i];
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
// 最终返回一个Promise对象
try {
// 按规范来说fn应该为一个Promise函数
// 但为了兼容fn不是Promise函数的情况,就再用Promise包裹一层
// 使用bind是为了让dispatch不执行,直接作为函数传入fn
return Promise.resolve(
// 执行当前中间件
// 中间件的参数为(ctx, next)
fn(ctx, dispatch.bind(null, i + 1))
)
} catch (err) {
return Promise.reject(err);
}
}
}
}
[_createContext](req, res) {
const ctx = {
req,
res
}
ctx.method = req.method;
ctx.url = req.url;
return ctx;
}
[_handleRequest](ctx, fn) {
return fn(ctx);
}
}
module.exports = MiniKoa2;
const app = require('./MiniKoa2')();
// logger
app.use(async (ctx, next) => {
console.log('第一层洋葱 - 开始')
await next();
const rt = ctx['X-Response-Time'];
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
console.log('第一层洋葱 - 结束')
});
// x-response-time
app.use(async (ctx, next) => {
console.log('第二层洋葱 - 开始')
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx['X-Response-Time'] = `${ms}ms`;
console.log('第二层洋葱 - 结束')
});
// response
app.use(async ctx => {
console.log('第三层洋葱 - 开始')
ctx.res.end('hello koa');
console.log('第三层洋葱 - 结束')
});
app.listen(8000, () => {
console.log('server is listening at 8000');
});
启动node服务,在浏览器中输入http://localhost:8000/
。
可以看到浏览器中打印出hello koa
。
控制台中打印出:
server is listening at 8000
第一层洋葱 - 开始
第二层洋葱 - 开始
第三层洋葱 - 开始
第三层洋葱 - 结束
第二层洋葱 - 结束
GET / - 7ms
第一层洋葱 - 结束