Koa2源码学习

5 篇文章 0 订阅
2 篇文章 0 订阅

Koa2源码学习

Koa – 基于 Node.js 平台的下一代 web 开发框架

简单使用

const koa = require('koa');

const app = new koa();
app.use((context, next) => {
  // do some thing
});
app.listen(8080);

以上代码构建了一个简单的服务器,你可以在浏览器输入 localhost:8080 来访问
下面我们通过创建koa服务器,且发送一次HTTP请求来了解源码

实例化koa前

在koa实例化前,先介绍四个对象

// lib/application.js
...
const context = require('./context');
const request = require('./request');
const response = require('./response');
...
// 实例化前,主要是引入koa三个对象 
  • Application对象:koa 应用对象,封装了端口监听,请求回调,中间件使用…
  • Context对象:上下文对象,封装了Response、Request及HTTP原生response、request对象的方法和属性
  • Request对象: koa Request对象,是对http.IncomingMessage的抽象
  • Response对象:koa Response对象,是对http.ServerResponse的抽象
Context

koa上下文对象,大部分操作都是通过ctx(context简写)完成的

Context对象包含两部分:

  • 自身对象的属性和方法
  • 通过delegate方法委托的Response对象及Request对象的属性和方法
// lib/context.js
// context对象自身的方法及属性
const proto = module.exports = {
  ...
  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },
   ...
}

// 委托的Request和Response对象的方法
delegate(proto, 'response')
  .method('attachment')
...

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

delegate方法实现

Request

对http.IncomingMessage的抽象,提供了很多方法和属性,使操作更加方便

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

对http.ServerResponse的抽象,提供了很多方法和属性,使操作更加方便

// lib/response.js
module.exports = {
  ...
 get socket() {
    return this.res.socket;
  },
  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {};  // Node < 7.7
  },
  ...
}

实例化

...
// 应用代码
const koa = require('koa');
const app = new koa();
...

实例化koa(即Application对象)时,执行constructor里面代码

// lib/application.js
...
 constructor() {
    super();
    ...
    this.middleware = []; 
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
	...
}
...
// 主要是赋值,这里的middleware就是存储中间件的数组

值得注意的是这里仅仅是赋值,并没有任何调用HTTP服务的代码
Object.create语法参考

调用listen方法

// 应用代码
app.listen(8080);

使用listen方法时,调用Application的listen方法

// lib/application.js
...
  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
 ...
 // listen方法就是是HTTP的语法糖

这里值得注意的是

  • this.callback返回值会自动添加到 "request"事件中去,每当有HTTP请求时,都会去执行
  • server.listen(…args)返回一个net.Server实例,可以控制该服务器,你可以通过返回值来控制当前服务器,例如关闭服务器
    node api:http_http_createservernet_server_listen
callback方法

在listen方法中,主要是this.callback方法,其他都是http基本用法

...
// lib/application.js
  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
...

callback重要的两个部分:

  • compose初始化中间件
  • handleRequest加载中间件且处理响应

以下分别来介绍这两个部分

compose方法

在koa中,中间件非常重要,绝大部分多功能都是由中间件来实现的,中间件重要的几个特点:

  • 保证中间件的调用顺序,使用next方法调用下一个中间件
  • 洋葱圈模型
  • 支持async/await

让我们看一下koa-compose是如何实现的

...
// koa-compose/index.js
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 {
        // 最重要的代码 实现next方法和洋葱圈
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
...

以上代码中,最重要的dispatch函数,通过递归dispatch,来实现中间件的连续且按顺序调用

以两个中间件为例子

app.use((context, next) => {
  // do some thing1
  next()
  // do some thing3
});
app.use((context, next) => {
  // do some thing2
});
  • 调用第一个中间件

  • 调用dispatch(0)

  • 调用fn(context, dispatch.bind(null, i + 1))
    next的实现:

    • 中间件需要两个传参,第一个参数为context,第二个参数为next,此时next为dispatch.bind(null, i + 1),即next此时是第二个中间件。当你在第一个中间件内部执行next方法时,其实调用的是第二个中间件,依次类推;
  • 执行第一个中间件代码:do some thing1

  • 调用了next方法

  • 执行第二个中间件代码:do some thing2,后续无中间件,即返回

  • next返回后,再次执行第一个中间件 do some thing3

  • 执行完毕后返回
    这就是koa中间件的特性之一,洋葱圈的原理

值得注意的是:

  • fn(context, dispatch.bind(null, i + 1)) ,这里使用了bind方法,如果这里不使用bind的方法话,第二个中间件会立即执行,无论你是否调用next方法;
  • 这里的返回值都是Promise类型,为了支持async/await语法
  • 在使用async函数后,可以阻塞异步操作,保证中间件执行顺序
handleRequest方法

这里调用了两个方法createContext和handleRequest

// lib/application.js
...
    const handleRequest = (req, res) => {
      // 这里接受的req,res参数为http.IncomingMessage和http.ServerResponse的实例
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
  ...

createContext
createContext方法主要是创建context对象及赋值

// lib/application.js
...
 createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    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;
  }
...

这里值得注意的:

  • context和this.context不相等,即在使用中间件的时候,context和context.app.context是不相等的
  • 因为this.callback是在每次"request"请求时调用的,每个request请求都会调用createContext方法,即每个请求的context都是不一样的,可以放心操作

handleRequest

该方法主要是调用中间件及处理响应
// lib/application.js
...
  handleRequest(ctx, fnMiddleware) {
    // fnMiddleware为compose处理后的返回函数,当调用该函数时,即调用整个中间件
    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); //此步骤执行中间件且处理响应,在中间件全部未抛出错误的下,对响应值进行处理,否则捕获错误
  }
  ...

use方法

// 应用代码
app.use((context, next) => {
  // do some thing1
  next()
  // do some thing3
});

使用use方法时,调用Application的use方法

// lib/application.js
...
use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
        'See the documentation for examples of how to convert old middleware ' +
        'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn); // 将中间件存入数组中
    return this;
  }
 ...

创建一个简单服务器及响应一次请求

下面按照"创建一个简单服务器及响应一次请求"来阐述代码执行顺序

  • 实例化Application,声明对象属性、方法及赋值

  • 执行listen函数

  • 执行callback函数

    • 执行koa-compose的compose函数,返回一个匿名函数
    • 返回handleRequet函数,但并不执行,且此函数为"request"事件的回调函数
  • 服务器收到请求,触发"request"事件

  • 执行handleRequest函数

    • 执行createContext函数,生成上下文
    • 执行handleRequest函数(与上文的handleRequest不是一个函数,具体可以看代码)
      • 生成处理响应函数和错误捕获函数
      • 执行中间件
      • 中间件执行文完毕,成功则进入响应函数,错误则进入错误捕获函数
        • 响应函数根据内容和状态码返回不同的值
        • 错误捕获函数捕获错误,抛出
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值