koa2 源码解析

141 阅读5分钟

koa

概述

Koa是一个新的web框架,致力于成为web应用和API开发领域中的一个更小、更富有表现力、更健壮的基石。 koa是express的下一代基于node.js的web框架; koa完全使用Promise并配合async来实现异步

特点

  • 轻量,无捆绑
  • 中间件架构
  • 优雅的API
  • 增强的错误处理

安装

npm i koa -s

koa原理

koa源码地址

一个基于nodejs的入门级http服务,类似于下面代码:

const http = require('http');

const server = http.createServer((req,res) => {
   res.writeHead(200)
   res.end('hi yuan')
})

server.listen(3000,() => {
   console.log('监听端口3000')
})

下图蓝框框架部分是固定的,要想封装需要对红框业务逻辑进行封装 我们用koa框架,类似于下面代码:

const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
   ctx.body = 'hi balabala'
   next()
})
app.listen(3000,() => {
   console.log(监听3000端口);
})

koa的目标是用更简单化、流程化、模块化的方式实现回调部分。 我们不难看出首先koa是一个class,类里面有两个方法,use和listen。接下来我们自己实现一个koa,取名ykoa

//ykoa.js
const http = require('http');

class ykoa{
   listen(...args){
       const server = http.createServer( (req,res) => {
           this.callback(req,res);
       });
       server.listen(...args);
   };
   // use方法调用中间件,就是执行一个回调函数
   use(callback){
       this.callback = callback;
   }
}

module.exports = ykoa
//-----------------------------------------------------------------------------------------------------------------
//index.js
const YKOA = require('./ykoa');

const app = new YKOA();

app.use((req,res) => {
   res.writeHead(200)
   res.end('hi ykoa');
})

app.listen(3000,() => {
   console.log('监听3000端口')
})

nodemon index启动查看,成功 到目前为止,ykoa还只是个马甲,要真正实现koa还需要引入上下文context和中间件机制middleware

context

koa为了能够简化api,引入上下文context概念,将原始请求对象req和响应对象res封装并挂载到context上,并且在context上设置getter和setter,从而简化操作

//创建request.js
module.exports = {
   get url() {
       return this.req.url
   },
   get method() {
       return this.req.method
   }
}
//创建response.js
module.exports = {
   get body() {
       return this._body;
   },
   set body(val) {
       this._body = val;
   }
}
//创建context.js
module.exports = {
   get url() {
       return this.request.url;
   },
   get method() {
       return this.request.method;
   },
   get body() {
       return this.response.body;
   },
   set body(val) {
       this.response.body = val
   }
}
class ykoa{
   listen(...args){
       const server = http.createServer( (req,res) => {
           // 创建上下文
           const ctx = this.createContext(req,res);
           this.callback(ctx);
           // 响应
           res.end(ctx.body)
       });
       server.listen(...args);
   };
   // use方法调用中间件,就是执行一个回调函数
   use(callback){
       this.callback = callback;
   };
   // 构建上下文,把res和req都挂载到ctx之上,并且ctx.req和ctx.request.req同时保存
   createContext(req,res){
       // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象
       const ctx = Object.create(context);
       ctx.request = Object.create(request);
       ctx.response = Object.create(response);
       ctx.req = ctx.request.req = req;
       ctx.res = ctx.response.res = res;
       return ctx;
   }
}
//index.js  可以使用ctx对象优雅的处理req,res
app.use((ctx) => {
   ctx.body = 'hi ykoa';
})

中间件机制

koa中间件机制就是函数式组合概念compose的概念,将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值。洋葱圈模型可以形象表示这种机制。 compose函数,返回函数集 functions 组合后的复合函数;大致特点:

  • 参数均为函数, 返回值也是函数
  • 第一函数接受参数, 其他函数接受的上一个函数的返回值
  • 第一个函数的参数是多元的, 其他函数的一元的
  • 自右向左执行
// 定义两个函数
const add = (x,y) => x + y
const square = z => z * z

// 两个函数组合调用
const fn = (x,y) => square(add(x,y))
console.log(fn(1,2))

// 将上面两个函数合并成一个函数
 const compose = (fn1,fn2) => (...args) => {
     return fn2(fn1(...args))
 }
 const fn3 = compose(add,square)
 console.log(fn3(2,2))

// 多个函数组合:中间件的数目是不固定的,我们可以用数组来模拟
const composes = (...[first,...other]) => (...args) => {
    let ret = first(...args);
    other.forEach(fn => {
        ret = fn(ret)
    })
    return ret
}
const fn4 = composes(add,square)
console.log(fn4(5,5))

异步中间件,上面的函数都是同步的,顺序遍历执行,如果是异步的函数就是一个pormise,我们要支持async + await的中间件,所以要等异步结束后,在执行下一个中间件。

//compose.js
function compose(middlewares){
    return function(){
        // 执行第0个函数
        return dispatch(0);
        function dispatch(i){
            let fn = middlewares[i];
            if(!fn){
                return Promise.resolve();
            }
            return Promise.resolve(
                fn(function next(){
                    // promise完成后再执行下一个
                    return dispatch(i + 1);
                })
            )
        }
    }
}
//测试
async function fn1(next){
    console.log("fn1")
    await next();
    console.log("end fn1")
}

async function fn2(next){
    console.log("fn2")
    await delay();
    await next();
    console.log("end fn2")
}

function fn3(next){
    console.log('fn3')
}

function delay(){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            resolve();
        },2000);
    });
}

const middlewares = [fn1,fn2,fn3];
const finalFn = compose(middlewares);
finalFn();

打印结果

修改ykoa.js,完整版

const context = require("./context");
const request = require("./request");
const response = require("./response");
// 研究koa源码,koa是一个class  里面有两个方法  一个use  一个listen
const http = require('http');

class ykoa{
    constructor(){
        this.middlewares = [];
    }
    
    listen(...args){
        const server = http.createServer(async (req,res) => {
            // 创建上下文
            const ctx = this.createContext(req,res);
            // 中间件合成
            const fn = this.compose(this.middlewares);
            // 执行合成函数并传入上下文
            await fn(ctx);
            // 响应
            res.end(ctx.body)
        });
        server.listen(...args);
    };
    // use方法调用中间件,就是执行一个回调函数
    use(middleware){
        // 将中间件添加到数组里
        this.middlewares.push(middleware);
    };
    // 构建上下文,把res和req都挂载到ctx之上,并且ctx.req和ctx.request.req同时保存
    createContext(req,res){
        // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象
        const ctx = Object.create(context);
        ctx.request = Object.create(request);
        ctx.response = Object.create(response);

        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;

        return ctx;
    }

    // 合成函数
    compose(middlewares){
        return function(ctx){ //传入上下文
            // 执行第0个函数
            return dispatch(0);
            function dispatch(i){
                let fn = middlewares[i];
                if(!fn){
                    return Promise.resolve();
                }
                return Promise.resolve(
                    fn(ctx,function next(){ //将上文传入中间件 fn(ctx,next)
                        // promise完成后再执行下一个
                        return dispatch(i + 1);
                    })
                )
            }
        }
    }
}

module.exports = ykoa
//index.js测试
const YKOA = require('./ykoa');

const app = new YKOA();

const delay = () =>{
    new Promise(resolve => setTimeout(() => resolve(),2000));
}
app.use(async (ctx,next) => {
    ctx.body = '1';
    await next();
    ctx.body += '5';
})
app.use(async (ctx,next) => {
    ctx.body += '2';
    await delay();
    await next();
    ctx.body += '4';
})
app.use((ctx,next) => {
    ctx.body += '3'
})

app.listen(3000,() => {
    console.log('监听3000端口')
})

查看输出结果

中间件

koa中间件规范:

  • 一个async函数
  • 接受ctx和next两个参数
  • 任务结束需要执行next

中间件常见任务

  • 请求拦截
  • 路由
  • 日志
  • 静态文件服务

路由router中间件的实现

//router.js
class Router{
    constructor(){
        // 初始化一个数组用来存放路由对象
        this.stack = [];
    }

    register(path,methods,middleware){
        // 创建route对象
        let route = {path,methods,middleware}
        // 存放到数组中
        this.stack.push(route);
    }
    // get请求
    get(path,middleware){
        this.register(path,'get',middleware)
    }
    // post请求
    post(path,middleware){
       this.register(path,'post',middleware)
    }

    routes(){
        let stock = this.stack;
        return async function(ctx,next){
            let currentPath = ctx.url;
            let route;
            stock.forEach(item => {
                 // 判断path和method
                if(currentPath === item.path && item.methods.indexOf(ctx.method) >= 0){
                   route = item.middleware;
                }
            })
            if(typeof route === 'function' ){
                route(ctx,next)
                return;
            }
            await next();
        }
    }
}
module.exports = Router
//使用 index.js
const YKOA = require('./ykoa');
const Router = require('./router');

const app = new YKOA();
const router = new Router();

router.get('/index', async ctx => {
 
    console.log('index,xx')
     
    ctx.body = 'index page';
    });
    
router.get('/post', async ctx => { ctx.body = 'post page'; });
router.get('/list', async ctx => { ctx.body = 'list page'; });
router.post('/index', async ctx => { ctx.body = 'post page'; });

app.use(router.routes());
app.listen(3000,() => {
    console.log('监听3000端口')
})

查看结果