koa源码中只有4个文件
- application.js:入口文件。
- context.js:ctx对象相关。
- request.js:请求对象相关。
- response.js:响应对象相关。
先来看application.js
Koa有一套错误处理机制,需要监听实例的error事件,所以要引入events模块继承EventEmitter。再引入另外三个自定义模块。app.listen()是http的语法糖,实际上还是用了http.createServer(),然后监听了一个端口,所以要引入http模块。
初始代码
let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
class Koa extends EventEmitter {
constructor () {
super()
}
use () {
}
listen () {
}
}
module.exports = Koa
先实现listen方法
其实中间件就是一个函数,使用use方法将中间件存入,然后创建http服务调用中间件。
class Koa extends EventEmitter {
constructor () {
super()
this.fn
}
use (fn) {
this.fn = fn // 先只有一个中间件
}
listen (...args) {
let server = http.createServer(this.fn) //创建http服务,执行中间件
server.listen(...args)
}
}
这样就可以简单启动一个服务了
let Koa = require('./application')
let app = new Koa()
app.use((req, res) => { // 还没写中间件,所以这里还不是ctx和next
res.end('hello world')
})
app.listen(3000)
下面先解决ctx
ctx是一个上下文对象,里面绑定了很多请求和相应相关的数据和方法,例如ctx.path、ctx.query、ctx.body()等等等等,极大的为开发提供了便利。
思路是这样的:用户调用use方法时,把这个回调fn存起来,创建一个createContext函数用来创建上下文,创建一个handleRequest函数用来处理请求,用户listen时将handleRequest放进createServer回调中,在函数内调用fn并将上下文对象传入,用户就得到了ctx。
class Koa extends Emitter{
constructor(){
super();
this.fn = null;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn){
this.fn = fn;
}
// 生成ctx
createContext(req, res){
let ctx = this.context; //ctx对象原型是context.js中暴露对象
ctx.request = this.request; //ctx.request对象原型是request.js中暴露对象
ctx.response = this.response; //ctx.response对象原型是response.js中暴露对象
ctx.req = ctx.request.req = req; //request对象中可用原生req
ctx.res = ctx.response.res = res; //response对象中可用原生res
return ctx;
}
// http.createServer里的那个入口函数
handleRequest(req, res){
let ctx = this.createContext(req, res); //创建ctx
this.fn(ctx); //将ctx传入中间件
res.end(ctx.body); //将ctx.body响应给客户端
}
listen(...arg){
//为避免this指向出错,这里手动bind一下
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...arg);
}
}
此时ctx对象身上还没有多少属性,我们希望能用ctx.url、ctx.path、ctx.query等来直接获取一些数据。同时ctx.request和ctx.response对象也有一些属性跟ctx直接相关联,其实实现思路很简单,就是对象属性访问器getter和修改器setter实现数据绑定(代理)。
request.js
上面代码中已经将request.req指向原生的req对象,所以request.js模块里的request中可以直接this.req取得原生req对象。
const url = require('url');
let request = {
//用get访问器形式定义数据代理,url: this.req.url这种直接定义会报错,对象初始没有req.url
get url(){ return this.req.url },
get path(){ return url.parse(this.req.url).pathname },
get query(){ return url.parse(this.req.url, true).query }
};
module.exports = request;
到这里就可以在中间件中使用ctx.request.url、ctx.request.path、ctx.request.query了。
下面在context.js模块中与request.js模块进行数据绑定。
context.js
let context = {
get url(){ return this.request.url }, //与request.js中url数据关联,只读
get path(){ return this.request.path }, //与request.js中path数据关联,只读
get query(){ return this.request.query } //与request.js中query数据关联,只读
};
module.exports = context;
到这里就可以在中间件中使用ctx.url、ctx.path、ctx.query了。
下面在response.js模块与context.js模块中处理ctx.response.body与ctx.body的数据绑定。
response.js
let response = {
get body(){ return this._body },
set body(value){
this.res.statusCode = 200; //只要写了body,响应状态码就是200
this._body = value;
}
};
module.exports = response;
修改context.js
let context = {
get url(){ return this.request.url }, //与request.js中url数据关联,只读
get path(){ return this.request.path }, //与request.js中path数据关联,只读
get query(){ return this.request.query }, //与request.js中query数据关联,只读
get body(){ return this.response.body }, //与response.js中body数据关联,可读可写
set body(value){ this.response.body = value }
};
module.exports = context;
到这里就可以在中间件中使用ctx.body了。
因为原生的res.end()只支持响应String和Buffer两种数据类型,但用户可能将ctx.body设置成其他数据类型,所以需要在handleRequest函数中做些处理。
let Stream = require('stream') // 引入stream
handleRequest(req,res){
res.statusCode = 404 // 默认404
let ctx = this.createContext(req, res)
this.fn(ctx)
// 响应
if(typeof ctx.body === 'object'){ //如果ctx.body是个对象怎么响应输出
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(ctx.body));
}else if(ctx.body instanceof Stream){ //如果ctx.body是流怎么响应输出
ctx.body.pipe(res);
}else if(typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)){//如果ctx.body是字符串或buffer
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(ctx.body);
}else{
res.end('NOT FOUND');
}
}
以上就基本完成了上下文对象相关的处理。下面该是koa中的重要概念了。
中间件
koa2中中间件执行机制有一个非常形象的概念叫“洋葱模型”。
当我们多次使用use时
app.use((crx, next) => {
console.log(1)
next()
console.log(2)
})
app.use((crx, next) => {
console.log(3)
next()
console.log(4)
})
app.use((crx, next) => {
console.log(5)
next()
console.log(6)
})
它的执行顺序是这样的:
next方法会调用下一个use,next下面的代码会在下一个use执行完再执行。
我们可以把上面的代码想象成这样:
app.use((ctx, next) => {
console.log(1)
// next() 被替换成下一个use里的代码
console.log(3)
// next() 又被替换成下一个use里的代码
console.log(5)
// next() 没有下一个use了,所以这个无效
console.log(6)
console.log(4)
console.log(2)
})
除此之外,koa的中间件还支持异步,可以使用async/await
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
let p = new Promise((resolve, roject) => {
setTimeout(() => {
console.log('3.5')
resolve()
}, 1000)
})
await p.then()
await next()
console.log(4)
ctx.body = 'hello world'
})
输出顺序:1=>3 一秒后=>3.5=>4=>2
koa源码里中间件执行的核心逻辑是一个compose方法,该方法是一个名为koa-compose的第三方模块,下面简单实现一下
第一步,先不考虑async异步函数
可以将next想象成下一个中间件的占位符,当next()执行是实际是执行下一个中间件。
class Koa extends Emitter{
constructor(){
super();
this.middlewares = []; //中间件集合
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn){
this.middlewares.push(fn); //将中间件存进中间件集合中
}
// 中间件执行机制
compose(middlewares, ctx){
function dispatch(index){
if(index === middlewares.length) return;
let fn = middlewares[index];
fn(ctx, () => dispatch(index + 1)); //next实际是调用下一个中间件的函数
}
dispatch(0); //第一个中间件开始执行
}
// 生成ctx
createContext(req, res){
let ctx = this.context; //ctx对象原型是context.js中暴露对象
ctx.request = this.request; //ctx.request对象原型是request.js中暴露对象
ctx.response = this.response; //ctx.response对象原型是response.js中暴露对象
ctx.req = ctx.request.req = req; //request对象中可用原生req
ctx.res = ctx.response.res = res; //response对象中可用原生res
return ctx;
}
// http.createServer里的那个入口函数
handleRequest(req, res){
res.statusCode = 404; //响应状态码默认404,如果ctx.body有值则会改变
let ctx = this.createContext(req, res);
this.compose(this.middlewares, ctx); //调用compose,传入中间件集合和ctx
// 响应
if(typeof ctx.body === 'object'){ //如果ctx.body是个对象怎么响应输出
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(ctx.body));
}else if(ctx.body instanceof Stream){ //如果ctx.body是流怎么响应输出
ctx.body.pipe(res);
}else if(typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)){//如果ctx.body是字符串或buffer
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(ctx.body);
}else{
res.end('NOT FOUND');
}
}
listen(...arg){
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...arg);
}
}
第二步,use方法的回调函数(也就是中间件)是个async函数
async函数返回的是一个promise,当上一个use的next前加上await关键字,会等待下一个use的回调resolve了再继续执行代码。
将每一个回调函数都包装成promise,无论用户写的回调函数是否为async函数,在中间件执行时都是返回一个promise。
class Koa extends Emitter{
constructor(){
super();
this.middlewares = []; //中间件集合
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn){
this.middlewares.push(fn); //将中间件存进中间件集合中
}
// 中间件执行机制
compose(middlewares, ctx){
function dispatch(index){
if(index === middlewares.length) return Promise.resolve();
let fn = middlewares[index];
// 无论用户写的中间件是否为async函数,最后返回的都是一个promise
return Promise.resolve(fn(ctx, () => dispatch(index + 1))); //next实际是调用下一个中间件的函数
}
return dispatch(0); //第一个中间件开始执行
}
// 生成ctx
createContext(req, res){
let ctx = this.context; //ctx对象原型是context.js中暴露对象
ctx.request = this.request; //ctx.request对象原型是request.js中暴露对象
ctx.response = this.response; //ctx.response对象原型是response.js中暴露对象
ctx.req = ctx.request.req = req; //request对象中可用原生req
ctx.res = ctx.response.res = res; //response对象中可用原生res
return ctx;
}
// http.createServer里的那个入口函数
handleRequest(req, res){
res.statusCode = 404; //响应状态码默认404,如果ctx.body有值则会改变
let ctx = this.createContext(req, res);
let fn = this.compose(this.middlewares, ctx);
// 响应
fn.then(() => { //等中间件执行完再响应
if(typeof ctx.body === 'object'){ //如果ctx.body是个对象怎么响应输出
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(ctx.body));
}else if(ctx.body instanceof Stream){ //如果ctx.body是流怎么响应输出
ctx.body.pipe(res);
}else if(typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)){//如果ctx.body是字符串或buffer
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(ctx.body);
}else{
res.end('NOT FOUND');
}
}).catch(err => { //错误处理,用于app.js中app.on('error', err => {})
this.emit('error', err);
res.statusCode = 500;
res.end('server error');
})
}
listen(...arg){
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...arg);
}
}
到此,基本实现了一个简单的koa2框架。
application.js完整代码
const http = require('http');
const Emitter = require('events');
const Stream = require('stream');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Koa extends Emitter{
constructor(){
super();
this.middlewares = []; //中间件集合
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn){
this.middlewares.push(fn); //将中间件存进中间件集合中
}
// 中间件执行机制
compose(middlewares, ctx){
function dispatch(index){
if(index === middlewares.length) return Promise.resolve();
let fn = middlewares[index];
// 无论用户写的中间件是否为async函数,最后返回的都是一个promise
return Promise.resolve(fn(ctx, () => dispatch(index + 1))); //next实际是调用下一个中间件的函数
}
return dispatch(0); //第一个中间件开始执行
}
// 生成ctx
createContext(req, res){
let ctx = this.context; //ctx对象原型是context.js中暴露对象
ctx.request = this.request; //ctx.request对象原型是request.js中暴露对象
ctx.response = this.response; //ctx.response对象原型是response.js中暴露对象
ctx.req = ctx.request.req = req; //request对象中可用原生req
ctx.res = ctx.response.res = res; //response对象中可用原生res
return ctx;
}
// http.createServer里的那个入口函数
handleRequest(req, res){
res.statusCode = 404; //响应状态码默认404,如果ctx.body有值则会改变
let ctx = this.createContext(req, res);
let fn = this.compose(this.middlewares, ctx);
// 响应
fn.then(() => { //等中间件执行完再响应
if(typeof ctx.body === 'object'){ //如果ctx.body是个对象怎么响应输出
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(ctx.body));
}else if(ctx.body instanceof Stream){ //如果ctx.body是流怎么响应输出
ctx.body.pipe(res);
}else if(typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)){//如果ctx.body是字符串或buffer
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(ctx.body);
}else{
res.end('NOT FOUND');
}
}).catch(err => { //错误处理,用于app.js中app.on('error', err => {})
this.emit('error', err);
res.statusCode = 500;
res.end('server error');
})
}
listen(...arg){
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...arg);
}
}
module.exports = Koa;