手把手帶你實現Koa框架核心模塊

1.Koa是什麼

基於Node.js平臺的下一代Web開發框架數組

2.Koa用法

Koa的基本用法比較簡單,並且沒有捆綁任何中間件bash

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 app

測試文件test.js框架

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

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

複製代碼

application.jskoa

/**
 * 核心方法 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.jsasync

/**
 * 核心方法 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.jsui

//怎麼直接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;

複製代碼
相關文章
相關標籤/搜索