Koa源码浅读与实现【这一次带你剖析Koa内部实现】

前言
Koa是继Express后新的Node框架,由Express原班人马开发,相比Express更加简洁,源码只有2000多行,结合最新的ECMA语法,这使得Koa更小 更具有表现力 更健壮,因为每个中间件的执行结果都是Promise,结合Async Await抛弃复杂的传统回调形式。并且错误结果处理起来也更加方便
创建一个简单的Koa程序

// yarn add koa
const Koa = require('koa');
const app = new Koa();
app.use((ctx)=>{
    ctx.body = 'Hello Koa';
})
app.listen(9001,()=>{
    console.log('🎉服务开启成功,端口号为:9001')
})

分析源码并实现自己的Koa

一、目录分析

image.png

  1. 创建一个新的文件夹,使用npm init初始项目,package.json中添加启动命令
{
  "name": "koa-server",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "serve": "nodemon server.js"
  }
}

  1. 文件夹内新建koa文件夹并使用npm init初始项目,package.json中指定入口文件
{
  "name": "koa",
  "version": "1.0.0",
  "main": "./lib/application.js"
}
  1. 在koa文件中新建lib文件,在lib文件中新建application.js context.js request.js response.js

image.png

二、分析并实现request.js文件
  • Koa源码中request.js文件做了很多请求相关的参数处理,通过get/set的访问方式对属性进行了包装,使用户获取属性更加方便

image.png

  • 实现自己的request.js
    • 内部的this指向ctx.request,所以ctx.request上面必须有req对象,该对象指向原生的request对象
const url = require('url');
module.exports = {
  get query() {
    const { query } = url.parse(this.req.url);
    return query;
  },
  get path() {
    const { pathname } = url.parse(this.req.url);
    return pathname;
  },
};
  • 实现context.js
    • context除了提供自身方法和属性外,还对其他属性进行了委托 (将请求相关的属性委托到ctx.requset上,将响应相关的属性和方法代理到ctx.response)
    • 用户访问ctx.body其实访问的是ctx.request.body(后续创建上下文对象ctx时,会将request挂载到ctx身上)
    • delegate的原理就是 __defineGetter__,__defineSetter__属性,可以访问对象属性时,将属性委托到其他对象身上
const delegate = require('delegates');
const proto = (module.exports = {
  // 给context自身添加属性和方法
  toJSON() {
    return {};
  },
});

// 当直接访问ctx.xx时 委托到ctx.response.xx身上
delegate(proto, 'response')
  .access('body')
  .access('status');

// 当直接访问ctx.xx时 委托到ctx.request.xx身上
delegate(proto, 'request')
  .access('query')
  .access('path')
  .access('url');
  1. delegatene内部也是通过__defineGetter__,__defineSetter__两种方法实现的属性委托\
  2. 上面的context实现方式,也可以通过下面__defineGetter__,__defineSetter__直接实现
const proto = (module.exports = {
  // 给context自身添加属性和方法
  toJSON() {
    return {};
  },
});
function defineGetters(taregt, key) {
  proto.__defineGetter__(key, function() {
    return this[taregt][key];
  });
}
defineGetters('request', 'query');
defineGetters('request', 'path');
defineGetters('request', 'url');
defineGetters('response', 'body');
defineGetters('response', 'status');

function defineSetters(target, key) {
  proto.__defineSetter__(key, function(value) {
    this[target][key] = value;
  });
}
defineSetters('response', 'body');
defineSetters('response', 'status');
  • 分析并实现response.js
    • response内部通过get set 提供了很多响应相关的属性和方法
    • 简单实现自己的response.js
      image.png
      简单实现:
  • 因为上面实现context是对response添加了属性代理,当context直接访问body或status属性时,其实访问的是context.response.body(后续只需实现将response对象挂载到body上面即可)
const response = {
  _body: undefined,
  get body() {
    return this._body;
  },
  set body(value) {
    this._body = value;
  },
  get status() {
    return this.res.statusCode;
  },
  set status(code) {
    this.res.statusCode = code;
  },
};
module.exports = response;
  • 剖析application源码并实现它
    (1)构造函数
  1. 继承Events函数,可以直接订阅或发布事件
  2. 通过Object.create()分别创建context,request,response对象,目的是为了基于原型链创建一个新对象,避免全局中多个程序造成对象引用污染
  3. 创建中报错间件的集合middleware
module.exports = class Application extends EventEmitter {
  constructor() {
    super();
    // 创建全新的context request response对象
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 保存中间件的数组
    this.middleware = [];
  }
}

(2)use()

  1. 验证并添加中间件
use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 将注册的中间件添加到数组中管理
    this.middleware.push(fn);
 }

(3)listen()

  1. 通过http创建server,通过this.callback完成回调
listen(...args) {
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

(4)callback()

  1. 通过调用compose包装中间件,返回一个可执行的函数,调用该函数则开始执行中间件
  2. 创建请求相关的处理函数,内部创建全局上下文对象ctx,将ctx和中间件的调用函数交给this.handleRequest函数处理
callback() {
  // fn函数内部将执行注册的中间件
  const fn = this.compose();
  // 处理request请求
  const handleRequest = (req, res) => {
    // 创建上下文对象ctx
    const ctx = this.createContext(req, res);
    this.handleRequest(ctx, fn);
  };
  return handleRequest;
}

(5)compose()

  1. 默认直接执行第一个中间件
  2. 没有中间件或中间件执行完毕直接返回成功的结果
  3. 记录上一个中间件的索引index,防止一个中间件内多次调用next()
  4. 递归调用dispatch(),中间件的第一个参数是ctx对象,第二个参数next为 dispatch(i+1)

image.png

compose() {
  // 每个中间价必须是个方法
  for (const fn of this.middleware) {
    if (typeof fn !== 'function')
      throw new TypeError('Middleware must be composed of functions!');
  }
  // 开始执行中间件
  return ctx => {
    // 上一个中间件的索引
    let index = -1;
    const dispatch = i => {
      // 防止中间内多次调用next函数
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      index = i;
      // 没有中间件 或执行完最后一个中间件 直接返回成功
      if (this.middleware.length === i) return Promise.resolve();
      let fn = this.middleware[i];
      try {
        // next 函数内部调用了dispatch,并且直接执行下一个中间件
        let next = () => dispatch.bind(null, i + 1);
        return Promise.resolve(fn(ctx, next()));
      } catch (err) {
        return Promise.reject(err);
      }
    };
    // 默认直接执行第一个中间件
    return dispatch(0);
  };
}

(6)createContext()

  1. 每一个请求都需要有一个全新的上下文对象,通过Object.create创建
  2. 将request,response对象挂载到上下对象ctx身上,方便通过__defineGetter__和__defineSetter__进行属性委托
createContext(req, res) {
  // 基于原型链创建新的ctx request response(避免不同的请求污染)
  const ctx = Object.create(this.context);
  const request = Object.create(this.request);
  const response = Object.create(this.response);
  ctx.request = request; // 上下文对象中保存包装后的request对象
  ctx.request.req = ctx.req = req; // 将原生的request对象分别挂载到 ctx.request 和 ctx上
  ctx.response = response; // 上下文对象中保存包装后的response对象
  ctx.response.res = ctx.res = res; // 将原生的response对象分别挂载到 ctx.response 和 ctx上
  return ctx;
}

(7)handleRequest()

  1. 创建默认的状态码
  2. 执行全部中间件
handleRequest(ctx, fn) {
  // 默认的状态码
  ctx.res.statusCode = 404;
  // 不同情况的响应处理
  const handleResponse = () => this.respond(ctx)
  // 执行中间件 全部中间件成功执行完毕 执行respond响应结果
  fn(ctx)
    .then(handleResponse)
    .catch(err => {
      this.emit('error', err);
    });
}

(8)respond()

respond(ctx) {
  // [1] 这里上下文对象的body其实是代理的response对象中的body
  // [2] ctx.body ==== ctx.response.body
  // [3] Koa源码中使用delegate函数完成代理 (__defineGetter__ , __defineSetter__)
  const body = ctx.body || 'Not Define';
  return ctx.res.end(body);
}

(9)自己实现的application.js完整流程

const request = require('./request');
const response = require('./response');
const context = require('./context');
const http = require('http');
const EventEmitter = require('events');

module.exports = class Application extends EventEmitter {
  constructor() {
    super();
    // 创建全新的context request response对象
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 保存中间件的数组
    this.middleware = [];
  }
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 将注册的中间件添加到数组中管理
    this.middleware.push(fn);
  }
  createContext(req, res) {
    // 基于原型链创建新的ctx request response(避免不同的请求污染)
    const ctx = Object.create(this.context);
    const request = Object.create(this.request);
    const response = Object.create(this.response);
    ctx.request = request; // 上下文对象中保存包装后的request对象
    ctx.request.req = ctx.req = req; // 将原生的request对象分别挂载到 ctx.request 和 ctx上
    ctx.response = response; // 上下文对象中保存包装后的response对象
    ctx.response.res = ctx.res = res; // 将原生的response对象分别挂载到 ctx.response 和 ctx上
    return ctx;
  }
  respond(ctx) {
    // [1] 这里上下文对象的body其实是代理的response对象中的body
    // [2] ctx.body ==== ctx.response.body
    // [3] Koa源码中使用delegate函数完成代理 (__defineGetter__ , __defineSetter__)
    const body = ctx.body || 'Not Define';
    return ctx.res.end(body);
  }
  compose() {
    // 每个中间价必须是个方法
    for (const fn of this.middleware) {
      if (typeof fn !== 'function')
        throw new TypeError('Middleware must be composed of functions!');
    }
    // 开始执行中间件
    return ctx => {
      let index = -1;
      const dispatch = i => {
        // 防止中间内多次调用next函数
        if (i <= index) return Promise.reject(new Error('next() called multiple times'));
        index = i;
        // 没有中间件 或执行完最后一个中间件 直接返回成功
        if (this.middleware.length === i) return Promise.resolve();
        let fn = this.middleware[i];
        try {
          // next 函数内部调用了dispatch,并且直接执行下一个中间件
          let next = () => dispatch.bind(null, i + 1);
          return Promise.resolve(fn(ctx, next()));
        } catch (err) {
          return Promise.reject(err);
        }
      };
      // 默认直接执行第一个中间件
      return dispatch(0);
    };
  }
  handleRequest(ctx, fn) {
    // 默认的状态码
    ctx.res.statusCode = 404;
    // 不同情况的响应处理
    const handleResponse = () => this.respond(ctx);
    // 执行中间件 全部中间件成功执行完毕 执行respond响应结果
    fn(ctx)
      .then(handleResponse)
      .catch(err => {
        this.emit('error', err);
      });
  }
  callback() {
    // fn函数内部将执行注册的中间件
    const fn = this.compose();
    // 处理request请求
    const handleRequest = (req, res) => {
      // 创建上下文对象ctx
      const ctx = this.createContext(req, res);
      this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
};

(9)调用并测试自己的koa程序

const Koa = require('./koa');
const app = new Koa();
 
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(8);
});
app.use(async (ctx, next) => {
  console.log(2);
  await next();
  console.log(7);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(6);
});
app.use(async (ctx, next) => {
  console.log(4);
  ctx.status = 201;
  ctx.body = 'Hello Koa';
  await next();
  console.log(5);
});
app.listen(9001, () => {
  console.log('🎉服务开启成功,端口号为:9001');
});

总结
本文通过浅析Koa源码,并手动实现一个Koa程序来帮助学习Koa或加深对Koa的理解,希望本文章对您学习Koa有所帮助

  • 实现request.js文件,
    • 通过get/set访问或设置属性,封装更多请求参数和方法,并导出包装后的该对象。
  • 实现response.js文件
    • 通过get/set访问或设置属性,封装更多响应时的参数和方法,并导出包装后的该对象。
  • 实现context.js文件
    • 内部除了实现自身方法外,通过_defineGetter_,_defineSetter_,将属性的访问和设置,委托到request对象或response对象身上。
  • 实现application.js文件
    • use() 添加中间件
    • listen() 创建server,通过this.callback完成回调
    • callback() 调用compose包装中间件,创建上下文对象,使用handleRequest处理请求响应
    • compose() 执行中间件 (洋葱模型)
    • createContext() 创建ctx上下文对象,并将response,request对象添加到ctx上,方便通过委托访问(ctx.body === ctx.response.body)
    • handleRequest() 处理服务响应,创建状态码,执行中间件
    • respond() 不同情况的响应处理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值