手把手和你一起学习Koa源码(二)——Appilication

2020-10-17 15:32:34 蜻蜓队长

前言

本文的主要内容是通过描述作者自己学习koa源代码的过程,来和大家一起来学习koa的源码,koa设计的初衷是 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石,说白了就是 小巧扩展性高所以koa的源码阅读起来也相对容易,如果你跟着我的文章学习 还学不会那么一定是我写的不好

在上一篇文章中我们学习了目录结构,知道了入口文件application.js,本篇我们就来一起学习一下。

Application

打开 lib/appication 我们发现这个文件也只有200多行,我们如何阅读这个文件?
先看 Koa项目--hello word 的启动

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

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
复制代码

这个基础的项目就干了4件事

  • require Koa,也就是导入了我们这个application.js
  • 创建一个实例,new 了一个我们application导出的类
  • 调用了这个实例的use方法,传入了一个function
  • 调用了listen 方法,监听了3000端口

我们根据下面的方式来看一下我们的application.js 的内容。 (版本koa@2.13.0)

先看导出的内容

看到第30行导出了application类 , 并且该类继承 Emitter ,然后看一下 Emitter 是通过第16行的 events 模块导入的。

constructor

知道了application的继承,继续看application的初始化。下面代码我加了一点自己的注释,下文会有一些解读。

 constructor(options) {
    super();
    
    // ①配置项信息相关
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    
    // ②重要的属性
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    
    // ③检查相关
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
复制代码

我将上面的constructor的代码大致分为3块

  • ①配置项相关部分
    option通过new 的时候传入的参数来控制自身的一些属性,通过名字可以大致的猜到什么意思但是也不能明确到底是干什么的,也不知道在什么情况下会使用,所以不用纠结,大致知道是有一些属性是在这里定义的并且给了一些默认值就好了,用的时候再来看就是了。
  • ②重要的属性
    如果你使用过koa,那你就可以大致猜到这几个属性是干什么的
    • middleware koa可以实现洋葱模型的中间件工作的基础
    • context 上下文对象,通过const context = require('./context');引入,也就是lib/context.js
    • request koa 的 Request 对象,就是 lib/request.js
    • response koa 的 Response 对象,就是 lib/response.js
  • ③检查相关
    也不用纠结用到再说

use

当我们new 好了一个 application 对象之后,我们开始使用调用application的方法,执行app.use

<!--const Koa = require('koa');-->
<!--const app = new Koa();-->

// 看这一句
app.use(async ctx => {
  ctx.body = 'Hello World';
});

<!--app.listen(3000);-->
复制代码

看一下use这个方法的源代码,在application.js中的第122行。

  use(fn) {
    // ①保证参数必须为一个function
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    
    // ②如果是generator函数要进行一次转化
    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状态下的输出
    debug('use %s', fn._name || fn.name || '-');
    
    // ④将方法push到我们的中间件数组
    this.middleware.push(fn);
    
    // ⑤想要链式调用,必须返回自身
    return this;
  }
复制代码

这个方法做的事情非常简单

  • ①保证参数必须为一个function
  • ②如果是generator函数要进行一次转化
    • 先判断是否为generator函数,看到isGeneratorFunction这个方法名就知道是干什么的,我也不管怎么实现的(想要写出高质量的代码,命名真的非常重要
    • 描述了v3版本不再支持gennorator函数作为参数,同时告诉你怎么去转化
    • convert方法是引入的koa-convert包,简单来说功能就是Generator函数转换成将async函数。更准确的说是将Generator函数转换成使用co包装成的Promise对象。
  • ③debug状态下的输出
    我们看到很多项目都有debug模式会将执行信息暴露出来,用的就是类似的方法,不影响主流程就不深入看了。
  • 将方法push到我们的中间件数组
    整个use方法最核心的就这一句了,将use调用的方法push到middleware数组中,本质上app.use的调用就是为了完成将参数(方法)push到middleware数组中这件事。
  • ⑤想要链式调用,必须返回自身

listen

我们继续执行我们的koa程序

<!--const Koa = require('koa');-->
<!--const app = new Koa();-->

<!--app.use(async ctx => {-->
<!--  ctx.body = 'Hello World';-->
<!--});-->

app.listen(3000); // 看这一句
复制代码

看一下listen函数,在application.js 的第79行

listen(...args) {
    debug('listen');
    
    // ①调用node中的http模块的createServer方法
    const server = http.createServer(this.callback());
    
    // ②http.Server实例调用listen方法
    return server.listen(...args);
}
复制代码

这个listen也很简单,就是对node原生的http模块的创建服务和服务监听进行了一次封装

  • ①调用node中的http模块的createServer方法
    这块先看一下node文档的http模块,了解一下http.createserver方法的使用,以及返回了http.server实例
  • ②http.Server实例调用listen方法
    可以看一下server.listen的文档,通常最简单的使用一般就是传一个端口号。

callback

koa的listen方法中调用了callback方法,我们来看看callback方法干了什么事情。 代码在application.js 的第143行

callback() {
    // ①洋葱模型原理核心
    const fn = compose(this.middleware);
    
    // ②错误监听相关
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    
    // ③koa封装的requestListener
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    
    return handleRequest;
}
复制代码

这个callback方法可以说是 koa 事件处理逻辑的核心

  • ①compose 洋葱模型的核心原理

先来看一下什么是洋葱模型

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

const middleware1 = async (ctx, next) => { 
  console.log(1); 
  await next();  
  console.log(6);   
}

const middleware2 = async (ctx, next) => { 
  console.log(2); 
  await next();  
  console.log(5);   
}

const middleware3 = async (ctx, next) => { 
  console.log(3); 
  await next();  
  console.log(4);   
}

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

app.listen(3001)

// 输出1,2,3,4,5,6
复制代码

通过分析之前的代码我们知道 app.use(fn) 函数最主要的作用就是对 fn 进行 this.middleware.push(fn) 在上面的代码中 我们通过app.use(middleware1); app.use(middleware2); app.use(middleware3);this.middleware 数组变成了 [middleware1, middleware1, middleware1]
callback函数 第一句是执行了const fn = compose(this.middleware); 找到compose的定义 koa-compose的源码如下(将部分不影响主流程内容删减):

 function compose (middleware) {
  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)
      }
    }
  }
}
复制代码

代码非常的短只有几十行
比较关键的就是这个dispatch函数了,它将遍历整个middleware,然后将contextdispatch(i + 1)传给middleware中的方法。 巧妙的实现了

  1. context一路传下去给中间件
  2. middleware中的下一个中间件fn作为未来next的返回值

最终通过compose将[middleware1, middleware1, middleware1] 转变成了类似 middleware1(middleware2(middleware3()));的函数。 运用了 compose 的特性,结合 async await 中 next 的等待执行,形成了洋葱模型,我们可以利用这一特性在 next 之前对 request 进行处理,而在 next 之后对 response 进行处理。

  • ②错误监听相关
    调用this.listenerCount 函数, 我找了application.js代码中没有相关函数,想起Application继承 自events 就翻阅了一下node的api,listenerCount方法找到了主要功能如下

onerror 方法将错误信息进行包装返回。

  • ③koa封装的requestListener ,这里定义了一个handleRequest函数并返回

先来看一下这个req和res是怎么来的,我们上文说过this.callback()是作为http.createServer的参数使用的,那么很明显这里面的 reqres 也就是node中的通过http.createServe返回的req对象和res对象
先是通过this.createContext函数创建了一个上下文对象 ctx,然后返回了this.handleRequest函数(和function handleRequest 不是一个函数)这里将ctx和通过compose转变过的middleware 作为参数。

createContext

我们先来看一下this.createContext函数如何创建上下文对象ctx 代码在 177行。

  createContext(req, res) {
    // ① 创建context 、request、response 对象
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    
    // ② context 、request 、 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;
    
    // ③ 记录原始url 并返回自身
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }
复制代码
  • ① 创建context 、request、response 对象 通过context.js、request.js、response.js 分别创建对象具体内容会在后面讲,这里只要知道在这里创建即可
  • ② context 、request 、 response 互相绑定 在koa使用中我们经常会通过 app.ctx.request 这种方式调用,就是在这块进行的互相挂载比较简单就不多解释了,同时注意一下 ctx.request !== ctx.req
  • ③ 记录原始url 并返回自身 记录一下原始的url信息,以及创建了一个state对象(也不用管用到时候再说)并且返回了context实例,也即是我们说的 上下文对象ctx,同时我们也知道这个上下文对象在这里获得了request实例、response实例以及通过node返回的 req和res。

handleRequest

再来看一下callback 最终返回的handleRequest函数,代码在162行。

handleRequest(ctx, fnMiddleware) {
    // ①将res的statusCode 设置为 404
    const res = ctx.res;
    res.statusCode = 404;
    
    // ②定义catch 时调用的onerror函数,以及正常返回时的 handleResponse函数
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    
    // ③ 顾名思义,判断是否结束以及调用的某方法
    onFinished(res, onerror);
    
    // ④ 调用之前通过compose生产的函数
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
复制代码
  • ①将res的statusCode 设置为 404
  • ②定义catch 时调用的onerror函数,以及正常返回时的 handleResponse函数
  • ③ 顾名思义,判断是否结束以及调用的某方法
  • ④ 调用之前通过compose生产的函数 这个就是我们最终通过listen --> callback --> handleRequest 返回的结果, 这里面的fnMiddleware 就是我们生成的middleware1(middleware2(middleware3()))函数,handleResponse就是this.response函数,通过将ctx、request、response、req、res,一路向下传后一路向上返回最终返回结果。

还有response 函数我就不细说了,也比较简单主要就是根据不同的返回状态返回不同的结果,主要包括对method === "HEAD"的返回,以及对返回body类型时res对象的一些处理

梳理

最后我用一张图来梳理整个Application.js的过程

总结

本篇application.js我们先分析到这里,下一篇会讲述关于context.js的内容,本文完全按照作者自己学习源码的过程进行描述,文笔不好读起来可能会有一点流水账,但是作者会努力描述清楚,并且把阅读源码的一些方法技巧分享,请收藏点赞支持。

相关文章

手把手和你一起学习Koa源码(一)——目录结构

手把手和你一起学习Koa源码(二)——Appilication

本文使用 mdnice 排版

以上内容来自于网络,如有侵权联系即删除
相关文章

上一篇: 基于下一代构建方案落地JOYY业务中台微前端

下一篇: 我们应该如何写好HTML&CSS

客服紫薇:15852074331
在线咨询
客户经理