Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

koa源码分析-从入门到看不懂 #22

Open
YIngChenIt opened this issue Jun 30, 2020 · 0 comments
Open

koa源码分析-从入门到看不懂 #22

YIngChenIt opened this issue Jun 30, 2020 · 0 comments

Comments

@YIngChenIt
Copy link
Owner

koa源码分析-从入门到看不懂

前言

本文用来记录一下自己学习koa的时候阅读源码和相关学习资料的心得和总结,但是写的很乱,大致的意思没有体现出来,不建议阅读下去,如果想更加清楚的理解koa的核心源码,可以移步下一章节,自己实现一个mini版的koa,这会让你对源码的认知提升到一个很好的地步。

koa是什么?

引用官网的话,koa是基于Node.js平台的下一代web开发框架,致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

用简单的话来总结就是:

1、基于node原生req和res为request和response对象赋能,并基于它们封装成一个context对象。

2、基于async/await(generator)的中间件洋葱模型机制。

基本用法

const Koa = require('koa')

const app = new Koa()

app.use(async(ctx, next) => {
    ctx.body = 'hello world'
    next()
})

app.listen(5050, () => {
    console.log('sever start')
})

以上是最简单的 hello world, 通过代码我们大致可以发现koa的使用并不难,而且koa的源码也并不多,但是设计的很抽象简洁,刚开始阅读的时候还是有点难接受的。

以下是网上借鉴来的一张koa结构示意图,可以更加直观的了解到koa的结构。

koa

源码结构

如果你看了koa的源码,你会发现koa源码其实很简单,就4个文件

── lib
   ├── application.js
   ├── context.js
   ├── request.js
   └── response.js

结合示意图,发现这个结构很简单,其中 context 、request 和 response 就是 3 个字面量形式创建的简单对象,上面封装了一些列方法(其实绝大部分是属性的赋值器(setter)和取值器(getter))。

这4个文件也就对应着koa的4个对象

── lib
   ├── new Koa()  || ctx.app
   ├── ctx
   ├── ctx.req  || ctx.request
   └── ctx.res  || ctx.response

那我们先对这4个文件进行初步的认识。

application.js

从koa的package.json中我们发现,application.js是koa的入口,也是koa的核心所在。

{
  "name": "koa",
  "version": "2.7.0",
  "description": "Koa web app framework",
  "main": "lib/application.js",
  ---
}

下面对核心代码进行注释

/**
 * 依赖模块,包括但不止于下面的,只列出核心需要关注的内容
 */
const response = require('./response');
const compose = require('koa-compose');
const context = require('./context');
const request = require('./request');
const Emitter = require('events');

/**
 * `Application`继承于`Emitter`,说明具有异步处理的能力
 */
module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.middleware = [];//存放中间件函数
    // context 、request 和 response 就是 3 个字面量形式创建的简单对象,他们将作为app的相应属性的原型
    // Object.create() 让两者不会指向同一片内存,不会相互影响。
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

/**
 * 创建服务器
 */
  listen(...args) {
    // 调用原生的node创建服务器的方法起一个服务
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

/**
 * use函数用于将中间件函数收集起来,存放在middleware中
 */
  use(fn) {
    // 兼容koa1 的写法,因为koa1主要使用generator,而koa2主要使用 async await
    if (isGeneratorFunction(fn)) {
      fn = convert(fn);
    }
    // 将中间件函数收集起来,存放在middleware中
    this.middleware.push(fn);
    // 返回当前实例,支持链式调用
    return this;
  }

/**
 * http.createServer的参数,返回一个类似于 (req, res) => {} 的函数,作为服务请求的处理函数
 */
  callback() {
    // 将所有use函数收集的中间件函数集合形成超集,实现洋葱模型中间件机制
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      // 基于req、res封装出更强大的ctx超集
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

/**
 * 请求处理函数
 */
  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);
  }

/**
 * 将req res 进行组合,形成强大的ctx
 */ 
  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;
  }

/**
 * 错误处理函数
 */
  onerror(err) {
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 == err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
};

通过application.js的初步解读,我们可以总结一下application.js的作用
1、框架入口

2、实现洋葱模型的中间件机制

3、将原生req、res处理成一个强大的超集ctx

4、错误的统一处理

context.js

const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');

const proto = module.exports = {
  // 省略了一些不甚重要的函数
  onerror(err) {
    // 触发application实例的error事件
    this.app.emit('error', err, this);
  },
};

/*
 在application.createContext函数中,
 被创建的context对象会挂载基于request.js实现的request对象和基于response.js实现的response对象。
 下面2个delegate的作用是让context对象代理request和response的部分属性和方法
*/
delegate(proto, 'response')
  .method('attachment')
  ...
  .access('status')
  ...
  .getter('writable')
  ...;

delegate(proto, 'request')
  .method('acceptsLanguages')
  ...
  .access('querystring')
  ...
  .getter('origin')
  ...;

从代码中我们可以总结context的作用:
1、错误的处理
2、代理request、response的属性和方法

request.js 和 response.js

module.exports = {
  
  // 在application.js的createContext函数中,会把node原生的req作为request对象(即request.js封装的对象)的属性
  // request对象会基于req封装很多便利的属性和方法
  get header() {
    return this.req.headers;
  },

  set header(val) {
    this.req.headers = val;
  },

  // 省略了大量类似的工具属性和方法
};

request对象基于node原生req封装了一系列便利属性和方法,也提供了一些原生上没有的属性和方法,如 path

所以当你访问ctx.request.xxx的时候,实际上是在访问request对象上的赋值器(setter)和取值器(getter)。

response对象和request对象类似,但是需要注意的是返回的body支持Buffer、Stream、String以及最常见的json

深入了解源码机制

上文中我们对koa的源码结构和对应文件的功能做了一些小的总结,下面我们从初始化、启动应用、中间件处理、错误处理来更加深入了解一下koa。

初始化

const Koa = require('koa');
const app = new Koa();

首先我们创建了 Koa 的实例 app,其构造函数十分简单,如下:

constructor() { 
  super();
  this.proxy = false;
  this.middleware = [];
  this.subdomainOffset = 2;
  this.env = process.env.NODE_ENV || 'development';
  this.context = Object.create(context);
  this.request = Object.create(request); 
  this.response = Object.create(response);
}

结合上文中的示意图,在创建实例的时候context request response分别被初始化,也在实例上挂载了一些常用的如 subdomainOffset env 等属性

我们可以总结一下,koa在初始化的时候,koa获得处理异步事件的能力,并且挂载一些属性和方法。

启动应用

app.listen(5050, () => {
    console.log('sever start')
})

app.listen()做的事件也很简单, 我们已经知道,通过向 http.createServer 创建一个服务,部分源码如下

listen() {
  const server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
}

唯一需要我们关注的就是这个 this.callback ,也是理解 koa 应用的核心所在。

this.callback() 执行返回一个类似于 (req, res) => {} 的函数

那么这个函数具体是怎么做的呢?首先,它基于 req 和 res 封装出我们中间件所使用的 ctx 对象,再将 ctx 传递给中间件所组合成的一个嵌套函数。中间件组合的嵌套函数返回的是一个 Promise 的实例,等到这个组合函数执行完( resolve ),通过 ctx 中的信息(例如 ctx.body )想 res 中写入数据,执行过程中出错 (reject),这调用默认的错误处理函数。

原理还是很简单,看一下代码:

callback() {
  const fn = compose(this.middleware);
  return (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    onFinished(res, onerror);
    fn(ctx).then(() => respond(ctx)).catch(onerror);
  };
}

就像我们阅读源码发现时候一样,通过 compose方法对中间件函数组合成一个大的嵌套函数供后序执行的时候调用

createContext根据 req 和 res 封装中间件所需要的 ctx

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;
  // 省略一点无关紧要的代码
  return context;
}

简单的说就是创建了3个简单的对象,并且将他们的原型指定为我们 app 中对应的对象。然后将原生的 req 和 res 赋值给相应的属性,这也是为何以下结构得到的结果是一样的原因。

    console.log(ctx.req.url); 
    console.log(ctx.request.req.url);
    console.log(ctx.request.url);
    console.log(ctx.url); // ctx.request.url

如我们示意图,整个 Koa 的结构就完整了。

但是,ctx 上不是暴露出来很多属性吗?它们在哪?他们就在我们示意图的最右边,一开始我们略过的 3 个简单对象。通过原型链的形式,我们 ctx.request 所能访问属性和方法绝大部分都在其对应的 request 这个简单的对象上面。request 又是怎么封装的呢?我只需要简单的贴一点源码,大家就秒懂了。

module.exports = {
  //...
  get method() {
    return this.req.method;
  },

  set method(val) {
    this.req.method = val;
  }
  //...
}

中间件处理

---略

错误处理

---略

参考文章

可能是目前最全的koa源码解析指南

十分钟带你看完 KOA 源码

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant