13.手写koa核心原理
Zijue opened this issue · comments
Koa介绍
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
Koa的使用
const Koa = require('koa');
const app = new Koa(); // const server = http.createServer(function (req, res) { })
app.use(ctx => {
ctx.body = 'zijue'
});
app.listen(3000, function () {
console.log('Koa server start at 3000');
});
运行上述代码,便可开启koa
服务。在浏览器中访问http://127.0.0.1:3000/
,页面便会显示zijue
字样。
很明显,app
就是一个http.createServer
创建的一个实例;同时还可以绑定error
事件,说明本身继承了EventEmitter
:
const Koa = require('koa');
const app = new Koa();
app.use(ctx => {
throw Error('报错了');
ctx.body = 'zijue'
});
app.listen(3000, function () {
console.log('Koa server start at 3000');
});
app.on('error', function(err){
console.log('err: ', err); // 打印程序执行中的错误
});
接下来看看app.use(ctx =>{ })
中的ctx
,首先看看下面这段代码:
app.use(ctx => {
console.log(ctx.req.url); // http原生的
console.log(ctx.request.req.url); // koa上封装的request上有req属性。这是为了在request对象中可以通过this获取到原生的req
console.log(ctx.request.query); // koa封装的
console.log(ctx.query); // koa中封装的request对象的属性被代理到了ctx对象上
ctx.body = 'zijue'; // 最终会执行 res.end(ctx.body)
console.log(ctx.response.res); // koa上封装的response上有res属性(http原生的)
console.log(ctx.response.body); // 同样的,koa中封装的response对象的属性被代理到了ctx对象上
});
koa核心原理的实现
为了和koa
源码保持一致,首先创建如下的目录结构:
koa
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
- 首先构建
Koa
类,让代码不具备其它功能先跑起来
// application.js
const EventEmitter = require('events');
const http = require('http');
class Koa extends EventEmitter {
constructor() {
super();
}
handleRequest(req, res) {
console.log(req.url);
res.end('zijue ~'); // 在浏览器中访问127.0.0.1:3000页面正常返回显示该内容
}
listen(...args) {
const server = http.createServer(this.handleRequest.bind(this));
server.listen(...args);
}
}
module.exports = Koa;
- 完成
app.use
功能
app.use
传递的函数我们称之为中间件函数,它会接受Koa
传递的上下文参数ctx
对象,该对象上拥有request
、response
封装对象作为属性;
// application.js
const EventEmitter = require('events');
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Koa extends EventEmitter {
constructor() {
super();
// 通过原型链的方式,保证应用之间的隔离;否则多个应用共享一个上下文,会造成混乱
this.context = Object.create(context); // this.context.__proto__ = context
this.request = Object.create(request);
this.response = Object.create(response);
}
use(middleware) {
this.middleware = middleware;
}
createContext(req, res) {
// 处理应用间上下文需要隔离,一个应用下的多个请求之间也是需要隔离上下文的。保证每次请求对象和响应对象的独立
const ctx = Object.create(this.context); // ctx.__proto__.__proto__ = context
const request = Object.create(this.request);
const response = Object.create(this.response);
ctx.request = request; // request.xxx 都是封装的
ctx.req = ctx.request.req = req; // req.xxx 就是原生的
ctx.response = response;
ctx.res = ctx.response.res = res;
return ctx
}
handleRequest(req, res) {
const ctx = this.createContext(req, res);
this.middleware(ctx);
if (ctx.body) {
res.end(ctx.body);
} else {
res.end('Not Found');
}
}
listen(...args) {
const server = http.createServer(this.handleRequest.bind(this));
server.listen(...args);
}
}
module.exports = Koa;
引入我们写的Koa
代码,测试一下:
const Koa = require('./koa');
const app = new Koa();
app.use(ctx => {
ctx.body = 'hi, zijue'
console.log(ctx.req.url);
})
app.listen(3000, function () {
console.log('Koa server start at 3000');
});
在浏览器中访问,页面正常显示hi, zijue
,控制台也打印了req.url
;
- 扩展
request
和response
对象
// request.js
const url = require('url');
request = {
get url() { // Object.defineProperty 属性访问器
return this.req.url
},
get path() {
return url.parse(this.url).pathname;
},
get query() {
return url.parse(this.url, true).query;
}
}
module.exports = request;
// response.js
response = {
_body: undefined,
get body() {
return this._body;
},
set body(value) {
this._body = value;
}
}
module.exports = response;
- 将
request
和response
对象上的方法和属性代理到context
对象上
// context.js
const context = {}
function defineGetter(target, key) {
context.__defineGetter__(key, function () {
return this[target][key]
})
}
function defineSetter(target, key) {
context.__defineSetter__(key, function (val) {
this[target][key] = val;
})
}
// 此处按照koa源码使用的api编写,也可以使用defineProperty、proxy等方式
defineGetter('request', 'query');
defineGetter('request', 'path');
defineGetter('response', 'body');
defineSetter('response', 'body');
module.exports = context;
- 多个中间件的组合处理
先看两段代码的执行过程:
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('sleep');
resolve();
}, time);
})
}
app.use(async (ctx, next) => {
console.log('1');
next();
console.log('2');
ctx.body = 'hi, zijue ~'
});
app.use(async (ctx, next) => {
console.log('3');
await sleep(1000);
next();
console.log('4');
});
app.use(async (ctx, next) => {
console.log('5');
next();
console.log('6');
});
执行顺序为:1 -> 3 -> 2 -> 页面显示hi, zijue ~
-> (等待1s) sleep -> 5 -> 6 -> 4;
app.use(async (ctx, next) => {
console.log('1');
await next();
console.log('2');
ctx.body = 'hi, zijue ~'
});
app.use(async (ctx, next) => {
console.log('3');
await sleep(1000);
await next();
console.log('4');
});
app.use(async (ctx, next) => {
console.log('5');
await next();
console.log('6');
});
执行顺序为:1 -> 3 -> (等待1s) sleep -> 5 -> 6 -> 4 -> 2 -> 页面显示hi, zijue ~
;
通过结果可以看出当第一个中间件(从上至下的顺序)执行完毕后,请求就被响应了。所以在Koa
中的中间件函数中,调用next()
时,前面必须加await
或return
,这样才能保证后面的中间件执行完成。
对于使用await
的多个koa
中间件,koa
会将传入的多个中间件进行组合处理,内部会将这三个函数全部包装成promise
,并且将这三个promise
串联起来,内部会使用promise
连接起来。当第一个use
传入的中间件执行完,整个请求就完成了。
新增compose
组合函数:
// application.js
const EventEmitter = require('events');
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Koa extends EventEmitter {
constructor() {
super();
// 通过原型链的方式,保证应用之间的隔离;否则多个应用共享一个上下文,会造成混乱
this.context = Object.create(context); // this.context.__proto__ = context
this.request = Object.create(request);
this.response = Object.create(response);
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
compose(ctx) {
let dispatch = (i) => {
if (this.middlewares.length == i) return Promise.resolve(); // 当 执行下标 == 中间件长度,表示所有中间件执行完毕
return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1))); // 否则,执行当前下标的中间件,并将下标后移的next函数传入中间件
}
return dispatch(0);
}
createContext(req, res) {
// 处理应用间上下文需要隔离,一个应用下的多个请求之间也是需要隔离上下文的。保证每次请求对象和响应对象的独立
const ctx = Object.create(this.context); // ctx.__proto__.__proto__ = context
const request = Object.create(this.request);
const response = Object.create(this.response);
ctx.request = request; // request.xxx 都是封装的
ctx.req = ctx.request.req = req; // req.xxx 就是原生的
ctx.response = response;
ctx.res = ctx.response.res = res;
return ctx
}
handleRequest(req, res) {
const ctx = this.createContext(req, res);
res.statusCode = 404;
this.compose(ctx).then(() => {
if (ctx.body) {
res.end(ctx.body);
} else {
res.end('Not Found');
}
}).catch(err => {
this.emit('error', err);
})
}
listen(...args) {
const server = http.createServer(this.handleRequest.bind(this));
server.listen(...args);
}
}
module.exports = Koa;
至此,Koa
最核心的功能就写完了。但是还有个问题需要解决,就是当用户在中间件中调用两次next()
时就会出问题,为此我们需要添加执行标识并限制,代码如下:
compose(ctx) {
// 将middlewares中的所有方法拿出来,先调用第一个,第一个完毕后,会调用next,再去调用执行第二个
let index = -1; // 执行标识
let dispatch = (i) => {
if (i <= index) return Promise.reject('next() called multiple times.');
index = i;
if (this.middlewares.length == i) return Promise.resolve(); // 当 执行下标 == 中间件长度,表示所有中间件执行完毕
return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1))); // 否则,执行当前下标的中间件,并将下标后移的next函数传入中间件
}
return dispatch(0);
}