参考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源码

相关文章
相关标签/搜索