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

前言

本文的主要内容是经过描述做者本身学习koa源代码的过程,来和你们一块儿来学习koa的源码,koa设计的初衷是 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石,说白了就是 小巧扩展性高因此koa的源码阅读起来也相对容易,若是你跟着个人文章学习 还学不会那么必定是我写的很差html

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

Application

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

const Koa = require('koa');
const app = new Koa();  app.use(async ctx => {  ctx.body = 'Hello World'; });  app.listen(3000); 复制代码

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

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

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

先看导出的内容

看到第30行导出了application类 , 而且该类继承 Emitter ,而后看一下 Emitter 是经过第16行的 events 模块导入的。 node_modules里没有找到events模块,说明events模块是node的原生模块,application类继承了node原生的evetns模块,不了解events模块也不用纠结能够继续往下看,用到events模块的内容再看就是了。api

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块app

  • ①配置项相关部分
    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.usedom

<!--const Koa = require('koa');-->
<!--const app = new Koa();-->  // 看这一句 app.use(async ctx => {  ctx.body = 'Hello World'; });  <!--app.listen(3000);--> 复制代码

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

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方法找到了主要功能以下

这行代码的意思就很明确了,就是判断是否监听过error信息,若是没有监听过error,就将错误信息经过 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 排版

相关文章
相关标签/搜索