手写koa2

原文地址

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;

代码已上传到github

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值