koa源码浅析

443 阅读4分钟

koa是现在我们最常用的node框架,它是一个轻量的web框架,只提供了http的协议的解析和中间件功能。我们要实现路由、静态页面托管和文件上传等功能均需要插件来实现。

koa源码结构

上图是koa的源码结构,lib放着koa的核心文件::application.js、context.js、request.js、response.js。

application.js

application.js是koa的入口文件,它向外到处了Koa类,即函数。Koa继承了node的事件模块event,因此,我们new Koa()的实例app,可以基于事件来实现观察订阅的功能。Koa还有内置了常用的几个函数:listen、use、createContext、toJSON。
listen方法是通过http.createServer开启并监听了http服务,并且它里面还进行了中间件的合并、上下文context的初始化,并且每次请求来的中件合并、context都会重新初始化。

context.js

这部分是对中间件上下对象ctx封装和暴露,里面的重点在delegate,这个就是代理,比如我们要访问ctx.repsponse.status但是我们通过delegate,可以直接访问ctx.status访问到它。

// 暴露出来的对象
const proto = module.exports = {
  toJSON() {
    return {
        // this.request 是通过application.js 中的createContext 方法将 reques和response对象挂载
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },
  get cookies() {
  // ....
  },

  set cookies(_cookies) {
    // ....
  }
};
// 代理 ctx.reponse 和ctx.request
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  // ...

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
 // ...

request.js、response.js

这两个文件是对原生对象req和res的解析,主要是使用了Gettersetter的方式来对http协议完成解析,便于我们使用.

// request
module.exports = {
  get header() {
    return this.req.headers;
  },
  set header(val) {
    this.req.headers = val;
  }
  // ........
    get url() {
    return this.req.url;
  },
  set url(val) {
    this.req.url = val;
  },
}

上面的this.req 也是application.js 里面的application 方法挂载的

  createContext(req, res) {
    const context = Object.create(this.context);
    // 初始化上下文ctx对象 的 request和repoonse 属性
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    // 保留app 实例
    context.app = request.app = response.app = this;
    // 保留原生的 req 和 res 对象 也是 上面 request.js文件里面 写法的原因
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    //保留上下文
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }

Koa整体流程梳理

const http = require("http");
class Application {
    constructor() {
        // 用来存储 use进来的中间件函数
        this.middleware = [];
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    
     use(middleware) {    // 将中间件加到数组⾥里里    
        this.middleware.push(middleware);  
     }
    
    listen(..args){
        const server = http.createServer(async(req,res)=>{
            // compose 会组合所有中间件
            const fn = compose(this.middleware);
            // 初始化上下文
            const ctx = this.createContext(req, res); 
             // 执⾏行行合成函数并传⼊入上下⽂文     
             await fn(ctx);   
             // 简单处理ctx.body 
            res.end(ctx.body);
             //源码中是通过handlRequest来处理请求 并在函数 reponse中对 ctx.body的各种取值情况做了判断
        })
        server.listen(...args);
    }
    compose() {
        //.....
    }
}
module.exports = Application;

中间件的原理

koa的中间件机制是一个剥洋葱式的模型,多个中间件通过use放进一个数组队列然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分,这就是剥洋葱模型。koa的中间件机制,是基于async/await + Promise实现的.

compose函数实现

function compose(middlewares) {
    return function (ctx) {
        // 初始执行第一个中间件函数
        return disPatch(0)
        function disPatch(i) {
            let fn = middlewares[0];
            if(!fn) {
                return  Promise.resolve()
            }
            // 返回promise
            return Promise.resolve(fn(ctx,function next(){
                return disPatch(++i)
            }))
        }
    }
}

koa-compose源码

function compose (middleware) {
    // 错误处理
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  return function (context, next) {
    let index = -1
    // 从第一个中间件开始执行
    return dispatch(0)
    function dispatch (i) {
        // 中间件重复执行
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      // 边界处理 这里的next 为undefined 
      if (i === middleware.length) fn = next
      // 当fn为undefined 不执行 直接resolved
      if (!fn) return Promise.resolve()
      try {
        // 实际app.use 中的 next 就是 disptch函数,它对应这当前的中间件函数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
module.exports = compose

来看下洋葱模型的执行结果:

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((reslove, reject) => {    
        setTimeout(() => {      reslove();    }, 2000);  
    }); 
}
const middlewares = [fn1, fn2, fn3]; const finalFn = compose(middlewares); finalFn();

总结

koa的核心对象:Koa类构造函数、request、response、context都有梳理,koa的源码做了许多细节处理,这样处理有什么好处,还需和大家共同探讨