阅读 99

手把手带你实现Koa框架核心模块

1.Koa是什么

基于Node.js平台的下一代Web开发框架

2.Koa用法

Koa的基本用法比较简单,而且没有捆绑任何中间件

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
复制代码

2.1 Koa基本核心骨架

由Koa的用法可以看出Koa本身是一个类,有use和listen方法,废话不多说,上代码。 Koa基本代码结构,核心代码文件 application.js

测试文件test.js

let Koa = require('./koa/application')
let app = new Koa()

app.use((req,res)=>{
    res.end('jeffywin')  
})
app.listen(3000)

复制代码

application.js

/**
 * 核心方法 use listen handleRequest
 */
let http = require('http')
class Koa {
    constructor(){
        this.callbackFn;
    }
    //test.js中的use回调传给cb,然后存到this.callbackFn中
    use(cb) {
        this.callbackFn = cb;
    }
    //1.test.js中use方法中要如何能拿到req,res,首先handleRequest接收listen中的req,res
    //2.接着req,res传给this.callbackFn,也就是上面use方法中赋值的this.callbackFn
    handleRequest(req,res){
        console.log(req,res)
        this.callbackFn(req,res)
    }
    //listen方法中接收test.js listen方法传递过来的参数,接着将req,res传给方法handleRequest
    listen(...args) {
        let server = http.createServer(this.handleRequest.bind(this))
        server.listen(...args)
    }
}

module.exports = Koa;

复制代码

2.2 Koa中的ctx 上下文

更新test.js中use方法,实现下面4个ctx中的属性

let Koa = require('./koa/application')
let app = new Koa()
//ctx是对原生req,res的封装
app.use((ctx,next)=>{
    console.log(ctx.req.path) //ctx.req = req
    console.log(ctx.request.req.path) //ctx.request.req = req
    console.log(ctx.request.path) //ctx.request 是Koa自己封装的
    console.log(ctx.path) //ctx代理ctx.request  ctx.path === ctx.request.path
    ctx.body = 'jeffywin'
})
app.listen(3000)

复制代码

application.js

/**
 * 核心方法 use listen handleRequest
 */
let http = require('http')
let Stream = require('stream');
let context = require('./context')
let response = require('./response')
let request = require('./request')

class Koa {
    constructor(){
        this.callbackFn;
        this.context = context
        this.response = response
        this.request = request
    }
    //test.js中的use回调传给cb,然后存到this.callbackFn中
    use(cb) {
        this.callbackFn = cb;
    }
    createContext(req,res) {
        //ctx可以拿到context的属性,但又不修改context,通过Object.create()相当于创建匿名函数 
        let ctx = Object.create(this.context)
        ctx.request = Object.create(this.request)
        ctx.response = Object.create(this.response)
        ctx.req = ctx.request.req = req//ctx自己添加属性request
        ctx.res = ctx.response.res = res//ctx自己添加属性response
        return ctx
    }
    //1.use方法中要能拿到req,res,首先handleRequest接收listen中的req,req
    //2.接着req,res传给this.createContext得到ctx,也就是上面use方法中赋值的this.callbackFn
    handleRequest(req,res){
        res.statusCode = 404
        //创建上下文
        let ctx = this.createContext(req,res)
        this.callbackFn(ctx)
        
        let body = ctx.body
        if (body instanceof Stream){
            body.pipe(res);
          }else if(Buffer.isBuffer(body) || typeof body === 'string'){
            res.end(body);
          }else if(typeof body === 'object' ){
            res.end(JSON.stringify(body));
          }else{
            res.end(body);
          }
    }
    //listen方法中接收test.js listen方法传递过来的参数,接着将req,res传给方法handleRequest
    listen(...args) {
        let server = http.createServer(this.handleRequest.bind(this))
        server.listen(...args)
    }
}

module.exports = Koa;

复制代码

request.js

//ctx.request.url = ctx.url 
let url = require('url')
let request = {
    //get方法相当于Object.defineProperty()的get方法的简写,只要调ctx.request的url方法
    //就会返回this.req.url,比如ctx.request.url,此时this == ctx.request
    //当调用ctx.request.url 就相当于调用 ctx.request.req.url,而后者在application.js中可以拿到
    get url(){
        return this.req.url
    },
    get path() {
        return url.parse(this.req.url).pathname
    }
}
module.exports = request;
复制代码

response.js

let response = {
    set body(val) {
        this.res.statusCode = 200
        this._body = val
    },
    get body(){
        return this._body
    }
}
module.exports = response; 

复制代码

context.js

//怎么直接ctx.url;方法,用ctx代理ctx.request,用原生方法__defineGetter__进行封装
let proto = {}

function defineGetter(property,name) {
    proto.__defineGetter__(name,function(){
        return this[property][name]
    })
}
function defineSetter(property,name) {
    proto.__defineSetter__(name,function(val){
        this[property][name] = val
    })
}
//调用ctx.url 相当于调用this[property][name],也就是this.request.url
defineGetter('request','url')
defineGetter('request','path')//ctx.path = ctx.request.path
defineGetter('response','body')//ctx.body = ctx.response.body
defineSetter('response','body')
module.exports = proto;

复制代码

2.3 Koa中间件原理 next

test.js

let Koa = require('./koa/application')
let app = new Koa()

app.use((ctx,next)=>{
    console.log(1)
    next()
    console.log(2)
})

app.use((ctx,next)=>{
    console.log(3)
    next()
    console.log(4)
})
app.use((ctx,next)=>{
    console.log(5)
    next()
    console.log(6)
})
app.listen(3000)

复制代码

next()方法相当于下一个执行函数

按照Koa源码 应该是 1 => 3 => 5 => 6 => 4 => 2,类似洋葱模型,如下图所示
但是上述代码中use中如果有异步,就有问题

// 模拟Koa中间件实现原理
let fs = require('fs');
function app() = {}
app.middlewares = []//声明一个数组
app.use = function(cb){
    app.middlewares.push(cb)
}

app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});
app.use(async (ctx, next) => {
  console.log(5);
  await next();
  console.log(6);
});

function dispatch(index){
    if(index === add.middlewares.length) return
    let middleware = app.middlewares[index]
    middle({},()=>dispatch(index+1))//递归调用下一个中间件,next()
}
dispatch(0)//让第一个中间件执行
复制代码

回到test.js中

let Koa = require('./koa/application')
let app = new Koa()
//Koa可以使用 async await

//第一个use中使用了await next(),此时next是第二个use中的 async函数,并且由于是await,所以第二个use中返回的是Promise,需要等待执行完才可以执行下面代码,同理,第二个next要等待第三个函数执行完,这些中间件会形成一个Promise链
let log = () => {
    return new Promise((resolve,reject) =>{
        setTimeout(()=>{
            console.log('Ok')
            resolve()
        },1000)
    })
}
app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(2);
  });
app.use(async (ctx, next) => {
  console.log(3);
  await log();
  await next();
  console.log(4);
});
app.use(async (ctx, next) => {
  console.log(5);
  await next();
  console.log(6);
  ctx.body = 'jeffywin'
});
app.on('error', err => {
  console.log(err);
})
app.listen(3000);

// 1 3 Ok 5 6 4 2
复制代码

最终版application.js

let http = require('http')
let Stream = require('stream');
let EventEmitter = require('events');
let context = require('./context')
let response = require('./response')
let request = require('./request')

class Koa extends EventEmitter{
    constructor(){
        super();
        this.middlewares = []
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    use(cb) {
        this.middlewares.push(cb)
    }
    createContext(req,res) {
        //ctx可以拿到context的属性,但又不修改context,通过Object.create()相当于创建匿名函数 
        let ctx = Object.create(this.context)
        ctx.request = Object.create(this.request)
        ctx.response = Object.create(this.response)
        ctx.req = ctx.request.req = req//ctx自己添加属性request
        ctx.res = ctx.response.res = res//ctx自己添加属性response
        return ctx
    }
    //核心方法 compose
    compose(ctx,middlewares){
        function dispatch(index) {
          if(index === middlewares.length) return Promise.resolve();
          let fn = middlewares[index];
          return Promise.resolve(fn(ctx,()=>dispatch(index+1)));
          }
        return dispatch(0);
      }
    //1.use方法中要能拿到req,res,首先handleRequest接收listen中的req,res
    //2.接着req,res传给this.createContext得到ctx
    handleRequest(req,res){
        res.statusCode = 404
        //创建上下文
        let ctx = this.createContext(req,res)
        let composeMiddleware = this.compose(ctx, this.middlewares)//返回的肯定是Promise
        //当Promise都执行完后,执行成功回调
        composeMiddleware.then(()=>{
            let body = ctx.body
            if (body instanceof Stream){
                body.pipe(res);
              }else if(Buffer.isBuffer(body) || typeof body === 'string'){
                res.end(body);
              }else if(typeof body === 'object' ){
                res.end(JSON.stringify(body));
              }else{
                res.end(body);
              }
        }).catch(err => {
            this.emit('error', err);
          });  
    }
    //listen方法中接收test.js listen方法传递过来的参数,接着将req,res传给方法handleRequest
    listen(...args) {
        let server = http.createServer(this.handleRequest.bind(this))
        server.listen(...args)
    }
}

module.exports = Koa;

复制代码
关注下面的标签,发现更多相似文章
评论