学习笔记-koa源码分析与简易实现

本文详细探讨了Koa框架的源码,包括`application.js`中的核心方法如`listen()`和`callback()`,特别是`handleRequest()`中的`compose()`中间件组合功能。文章还介绍了`context.js`、`request.js`和`response.js`中如何封装Node.js的原生对象,并展示了简易实现Koa的核心思想,以及洋葱圈模型解释了中间件的执行顺序。
摘要由CSDN通过智能技术生成

Hello Koa

const Koa = require('koa'); //引入koa
const app = new Koa();// 实例化一个对象
 
// 中间件一
app.use(ctx => {
  ctx.body = 'Hello Koa'; // 设置响应体
});
 
app.listen(3000); //在3000端口上启动koa服务

两个核心方法:

  • use():引用中间件
  • listen():启动服务

源码分析

概要

koa的源码清晰明了,有四个见名知义的文件:

  • application.js :定义Koa对象,use()、listen()
  • contex.js:koa中的上下文与Node中的req,res是怎样代理(映射)的
  • request.js:定义node中的req对象属性的getter、setter,方便取用
  • response.js:封装了node中的res

application.js

1 listen()

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

http.createServe()创建了一个server对象,每当有请求时都会执行callback()这个回调方法

2 callback()

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    // 这就是原生Node起服务时On('request',(req,res)=>{.....})
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

callback 返回了一个闭包 handleRequest

高潮渐渐出现了:
  • const fn = compose(this.middleware);暂时理解为把所有的中间件copmose(压缩合成)了一个fn,对,就是把所有use()里注册对方法压缩成了一个fn,牛逼吧,这也是koa中间件的巧妙之处,想想怎么实现呢,闭包+递归,看下文详解
  • const ctx = this.createContext(req, res); 创建了ctx,怎么创建的?看contex.js
  • return this.handleRequest(ctx, fn);this.handleReques才是真正处理请求然后响应的方法
ctx上下文
  • ctx.resquest:
  • ctx.response:官方推荐所有的响应处理都在response 对象上进行,应避免使用node的对象和属性
  • ctx.req: Node的request对象
  • ctx.res: Node的reponse对象

3 this.handleRequest(ctx,fnMiddleware)

 handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

接收上下文ctx和合成的中间件fn,完成了最后的响应。此处有两个问题:

  • respond():看源码后发现了方法内部其实是设置了响应体,发给客户端了
    • res.end()
    • res.pipe()
  • fnMiddleware(ctx).then(handleResponse).catch(onerror);
    • 最骚的一顿操作
    • fnMiddleware这个合成的中间件居然是一个promise,这样所有中间件执行完后才会去处理reponse响应,同时也会捕获未处理的中间件的错误
    • 又绕回到最神秘的compose()了

compose(middlewares)

回过头来慢慢体会compose,渐入佳境 (最高潮),compose()方法是koa、koa-router的核心方法

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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    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]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

去繁从简,去掉参数校验,容错处理等看主流程


function compose (middleware) {

  return function (context, next) {
  
    return dispatch(0) //直接执行第一个中间件
    
    function dispatch (i) {
      let fn = middleware[i]

      if (!fn) return Promise.resolve() //走完最后一个中间件
      
        //执行一个中间件
        fn(context, function next () {
          return dispatch(i + 1) //递归调用 闭包
        })
      
    }
  }
}

简易实现

调用demo
const chalk = require('chalk')
const Koa = require('./src/tiny-koa')
const util = require('util')
const port = 9001
const app = new Koa()

app.use(async (ctx, next) => {
  console.log('1.1')
  await next()
  console.log('1.2')
})

app.use(async (ctx, next) => {
  console.log('2.1')
  await next()
  console.log('2.2')
})

app.use(async (ctx, next) => {
  console.log('3.1')
  await next()
  console.log('3.2')
})

app.listen(port, _ => {
  console.log(chalk.red(`koa server started at ${port}`))
})


app.on('error',error=>{
 console.log( util.inspect(error))
})

// process.on('uncaughtException', function (err) {
//   console.error('Caught exception: ' +chalk.red(err));
// });

TinyKoa:简易实现koa

版本一:简易实现next调用
const http = require('http')
const EventEmiter = require('events')

class TinyKoa extends EventEmiter {

  constructor() {
    super();
    this.middlewares = []
    this.ctx = {}
  }

  use(fn) {
    if (typeof fn !== 'function') {
      throw new Error('the middleware must be a function')
    }
    this.middlewares.push(fn)
  }

  listen(...argvs) {
    try {
      const server = http.createServer(this.onRequest.bind(this))
      server.listen(...argvs)
    } catch (error) {
      this.emit('error', error)
    }
  }

  onRequest(req, res) {
    let middlewares = this.middlewares
    let ctx = this.ctx
    ctx.request = req
    ctx.response = res
    dispatch(0)
    function dispatch(idx) {
      try {
        let fn = middlewares[idx]
        fn && fn(ctx, () => dispatch(idx + 1))
      } catch (error) {
        this.emit('error', error)
      }

    }
  }

}

module.exports = TinyKoa

核心方法
  onRequest(req, res) {
    let middlewares = this.middlewares
    let ctx = this.ctx
    ctx.request = req
    ctx.response = res
    dispatch(0)
    function dispatch(idx) {
      try {
        let fn = middlewares[idx]
        fn && fn(ctx, () => dispatch(idx + 1))
      } catch (error) {
        this.emit('error', error)
      }
    }
  }

context.js

context核心方法

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .access('body')
  .access('lastModified')
  .access('etag')


/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsEncodings')
  .method('accepts')
  .method('get')
  .access('querystring')
  .access('search')
  .access('method')
  .access('query')
  .access('path')

context.js 就是将res,req上的常用属性、方法通过delegate代理到ctx上

delegate类核心方法

Delegator原型上定义有method、access方法,都返回了this,实现了链式调用,jquery也是这样

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

/**
 * Delegate method `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};

/**
 * Delegator accessor `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */

Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};

  • 方法代理:bind
  • 属性代理:类似vue实现响应式的原理
    • defineSetter
    • defineGetter

request.js

  • 对node原生对象req上一些属性和方法的封装
  • get
  • set

response.js

  • 对node原生对象res上一些属性和方法的封装
  • get
  • set

洋葱圈模型

ci

  • 中间件洋葱圈图

image

  • 中间件执行顺序图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值