參考koa源碼,實現個myKoa

看了koa的源碼,才發現它竟如此簡潔,真正作到了小而美的框架。一個字,amazing!javascript

koa的小,在於直接使用nodejs原生的http模塊來提供web服務,很簡單;html

koa的美,在於經過中間件來實現複雜的功能(中間件之間的銜接是經過上下文變量ctx)java

原生http服務與koa的對比

原生http服務node

let http = require('http')
let server = http.createServer((req, res)=>{
    res.end('hello world.')
})
server.listen(3000)
複製代碼

koagit

const Koa = require('koa')
const app = new Koa()
app.use((ctx, next)=>{
    ctx.body = 'hello world.'
})
app.listen(3000)
複製代碼

在use函數中,koa收集中間件;github

在listen函數中, koa調用http.createServer來建立web server服務(在其回調中處理上下文變量ctx、處理中間件的調用);web


代碼結構

myKoa
├── app.js //調用lib中的application.js獲得koa服務
├── lib
│   ├── application.js //定義Application類,建立web服務,處理中間件
│   ├── context.js  //上下文對象,代理一些屬性,好比ctx.body就是代理的ctx.response.body
│   ├── request.js  //請求對象
│   └── response.js //響應對象
└── package.json複製代碼

代碼鏈接:mykoa在github上的連接json

app.js
數組

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

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

app.listen(3000, ()=>{
  console.log('listening on 3000');
})

複製代碼

當在瀏覽器上訪問localhost:3000,在server中打印輸出是:1 2 3 4 5 6瀏覽器

關鍵的application.js

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

class Application extends EventEmitter{
  constructor(){
    super()
    this.middlewares = []
    this.context = context
    this.request = request
    this.response = response
  }ge
  use(fn){//收集中間件
    this.middlewares.push(fn)
  }
  compose(){//合成中間件,準確的說是串聯中間件
    return async ctx =>{
      function createNext(middleware, oldNext){
        return async ()=>{
          await middleware(ctx, oldNext)
        }
      }
      let len = this.middlewares.length
      let next = async ()=>{
        return Promise.resolve()
      }
      for (let i=len-1; i>=0; i--) {
        let currentMiddleware = this.middlewares[i]
        next = createNext(currentMiddleware, next)
      }
      await next()
    }
  }
  createContext(req, res){
    const ctx = Object.create(this.context)
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    request.ctx = response.ctx = ctx
    request.response = response
    response.request = request
    return ctx
  }
  handleRequest(ctx, fn){
    let res = ctx.res
    fn(ctx)//調用中間件
    .then(()=>{
      let bodyType = typeof ctx.body
      if (bodyType == 'object') {
        res.setHeader('Content-type', 'application/json;charset=utf8')
        res.end(JSON.stringify(ctx.body))
      } else if (ctx.body instanceof Stream) {
        ctx.body.pipe(res)
      } else if (bodyType == 'string' || Buffer.isBuffer(ctx.body)) {
        res.setHeader('Content-type', 'text/html;charset=utf8')
        res.end(ctx.body)
      } else {
        res.end('not found')
      }
    })
    .catch(err=>{
      this.emit('error', err)
      res.statusCode = 500
      res.end('server error')
    })
  }
  callback(){
    const fn = this.compose()//將中間件合成
    return (req,res)=>{//返回web服務的回調
      console.log('callback....')
      const ctx = this.createContext(req, res)//獲得上下文變量ctx
      return this.handleRequest(ctx, fn)//處理請求
    }
  }
  listen(...args){
    let server = http.createServer(this.callback())//建立web服務
    return server.listen(...args)
  }
}

module.exports = Application

複製代碼

listen和use函數沒啥好說的,關鍵仍是compse函數的理解,這纔是koa的核心

串聯中間件的compose函數

在koa源碼中使用的是koa-compose中間件來實現的,這裏參考的是別人手寫的一個compose函數,很精妙。

compose(){
  return async ctx =>{
    function createNext(middleware, oldNext){
      return async ()=>{
        await middleware(ctx, oldNext)
      }
    }
    let len = this.middlewares.length
    let next = async ()=>{
      return Promise.resolve()
    }
    for (let i=len-1; i>=0; i--) {
      let currentMiddleware = this.middlewares[i]
      next = createNext(currentMiddleware, next)
    }
    await next()
  }
}
複製代碼

compose函數的關鍵是怎麼依次調用中間件,不對,準確的說法應該是串聯中間件;

串聯好中間件後,只要在handleRequest中調用第一個中間件就能夠了。

上述代碼的關鍵是createNext函數,將下一個應該被調用的middleware包裹在async函數中;

倒着遍歷中間件數組,就能夠把下一個應該被調用的middleware放在上個調用的next中。

說的可能有點暈,我把代碼轉化爲一個調用圖,就明白了,以app.js中的中間件調用爲例:


發現async/await功能真的是超級好用啊。

小結

koa的源碼真的很短,也有不少很好的解析,本身看一遍,寫一遍,仍是超有收穫的。


參考

1. koa源碼

2. KOA2框架原理解析和實現

3. node進階——之事無鉅細手寫koa源碼

相關文章
相關標籤/搜索