KOA2 内部原理及模拟源码

前言

Koa 2.x 版本是当下最流行的 NodeJS 框架,同时社区涌现出一大批围绕 Koa 2.x 的中间件以及基于 Koa 2.x 封装的企业级框架,如 egg.js,然而 Koa 自己的代码却很是精简,精简到全部文件的代码去掉注释后还不足 2000 行,本篇就围绕着这 2000 行不到的代码抽出核心逻辑进行分析,并压缩成一版只有 200 行不到的简易版 Koanode

Koa 分析过程

在下面的内容中,咱们将对 Koa 所使用的功能由简入深的分析,首先会给出使用案例,而后根据使用方式,分析实现原理,最后对分析的功能进行封装,封装过程会从零开始并一步一步完善,代码也是从少到多,会完整的看到一个简版 Koa 诞生的过程,再此以前咱们打开 Koa 源码地址git


Koa 文件目录

经过上面对 Koa 源码目录的截图,发现只有 4 个核心文件,为了方便理解,封装简版 Koa 的文件目录结构也将严格与源码同步。github

搭建基本服务

在引入 Koa 时咱们须要建立一个 Koa 的实例,而启动服务是经过 listen 监听一个端口号实现的,代码以下。编程

const Koa = require("koa");

const app = new Koa();

app.listen(3000, () => {
    console.log("server start 3000");
});复制代码

经过使用咱们能够分析出 Koa 导出的应该是一个类,或者构造函数,鉴于 Koa 诞生的时间以及基于 node v7.6.0 以上版本的状况来分析,正是 ES6 开始 “横行霸道” 的时候,因此推测 Koa 导出的应该是一个类,打开源码一看,果真如此,因此咱们也经过 class 的方式来实现。json

而从启动服务的方式上看,app.listen 的调用方式与原生 http 模块提供的 server.listen 几乎相同,咱们分析,listen 方法应该是对原生 http 模块的一个封装,启动服务的本质仍是靠 http 模块来实现的。redux

文件路径:~koa/application.js
const http = require("http");

class Koa {
    handleRequest(req, res) {
        // 请求回调
    }
    listen(...args) {
        // 建立服务
        let server = http.createServer(this.handleRequest.bind(this));

        // 启动服务
        server.listen(...args);
    }
}

module.exports = Koa;复制代码

上面的代码初步实现了咱们上面分析出的需求,为了防止代码冗余,咱们将建立服务的回调抽取成一个 handleRequest 的实例方法,内部的逻辑在后面完善,如今能够建立这个 Koa 类的实例,经过调用实例的 listen 方法启动一个服务器。数组

上下文对象 ctx 的封装

一、基本使用

Koa 还有一个很重要的特性,就是它的 ctx 上下文对象,咱们能够调用 ctxrequestresponse 属性获取原 reqres 的属性和方法,也在 ctx 上增长了一些原生没有的属性和方法,总之 ctx 给咱们要操做的属性和方法提供了多种调用方式,使用案例以下。promise

const Koa = require("koa");

const app = new Koa();

app.use((ctx, next) => {
    // 原生的 req 对象的 url 属性
    console.log(ctx.req.url);
    console.log(ctx.request.req.url);
    console.log(ctx.response.req.url);

    // Koa 扩展的 url
    console.log(ctx.url);
    console.log(ctx.request.req.url);

    // 设置状态码和响应内容
    ctx.response.status = 200;
    ctx.body = "Hello World";
});

app.listen(3000, () => {
    console.log("server start 3000");
});复制代码

二、建立 ctx 的引用关系

从上面咱们能够看出,ctxuse 方法的第一个参数,requestresponsectx 新增的,而经过这两个属性又均可以获取原生的 reqres 属性,ctx 自己也能够获取到原生的 reqres,咱们能够分析出,ctx 是对这些属性作了一个集成,或者说特殊处理。浏览器

源码的文件目录中正好有与 requestresponse 名字相对应的文件,而且还有 context 名字的文件,咱们其实能够分析出这三个文件就是用于封装 ctx 上下文对象使用的,而封装 ctx 中也会用到 reqres,因此核心逻辑应该在 handleRequest 中实现。bash

在使用案例中 ctx 是做为 use 方法中回调函数的参数,因此咱们分析应该有一个数组统一管理调用 use 后传入的函数,Koa 应该有一个属性,值为数组,用来存储这些函数,下面是实现代码。

文件路径:~koa/application.js
const http = require("http");

// ***************************** 如下为新增代码 *****************************
const context = require("./context");
const request = require("./request");
const response = require("./response");
// ***************************** 以上为新增代码 *****************************

class Koa {
// ***************************** 如下为新增代码 *****************************
    contructor() {
        // 存储中间件
        this.middlewares = [];

        // 为了防止经过 this 修改属性而致使影响原引入文件的导出对象,作一个继承
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    use(fn) {
        // 将传给 use 的函数存入数组中
        this.middlewares.push(fn);
    }
    createContext(req, res) {
        // 或取定义的上下文
        let ctx = this.context;

        // 增长 request 和 response
        ctx.request = this.request;
        ctx.response = this.response;

        // 让 ctx、request、response 都具备原生的 req 和 res
        ctx.req = ctx.request.req = ctx.response.req = req;
        ctx.res = ctx.response.res = ctx.request.res = res;

        // 返回上下文对象
        return ctx;
    }
// ***************************** 以上为新增代码 *****************************
    handleRequest(req, res) {
        // 建立 ctx 上下文对象
        let ctx = this.createContext(req, res);
    }
    listen(...args) {
        // 建立服务
        let server = http.createServer(this.handleRequest.bind(this));

        // 启动服务
        server.listen(...args);
    }
}

module.exports = Koa;复制代码

首先,给实例建立了三个属性 contextrequestresponse 分别继承了 context.jsrequest.jsresponse.js 导出的对象,之因此这么作而不是直接赋值是防止操做实例属性时 “污染” 原对象,而获取原模块导出对象的属性能够经过原型链进行查找,并不影响取值。

其次,给实例挂载了 middlewares 属性,值为数组,为了存储 use 方法调用时传入的函数,在 handleRequest 把建立 ctx 属性及引用的过程单独抽取成了 createContext 方法,并在 handleRequest中调用,返回值为建立好的 ctx 对象,而在 createContext 中咱们根据案例中的规则构建了 ctx 的属性相关的各类引用关系。

三、实现 request 取值

上面构建的属性中,全部经过访问原生 reqres 的属性都能获取到,反之则是 undefined,这就须要咱们去构建 request.js

文件路径:~koa/request.js
const url = require("url");

// 给 url 和 path 添加 getter
const request = {
    get url() {
        return this.req.url;
    },
    get path() {
        return url.parse(this.req.url).pathname;
    }
};

module.exports = request;复制代码

上面咱们只构造了两个属性 urlpath,咱们知道 url 是原生所自带的属性,咱们在使用 ctx.request.url 获取是经过 request 对象设置的 getter,将 ctx.request.req.url 的值返回了。

path 是原生 req 所没有的属性,但倒是经过原生 requrl 属性和 url 模块共同构建出来的,因此咱们一样用了给 request 对象设置 getter 的方式获取 requrl 属性,并使用 url 模块将转换对象中的 pathname 返回,此时就能够经过 ctx.request.path 来获取访问路径,至于源码中咱们没有处理的 req 属性都是经过这样的方式创建的引用关系。

四、实现 response 的取值和赋值

Koaresponse 对象的真正做用是给客户端进行响应,使用时是经过访问属性获取,并经过从新赋值实现响应,可是如今 response 获取的属性都是 undefined,咱们这里先无论响应给浏览器的问题,首先要让 response 下的某个属性有值才行,下面咱们来实现 response.js

文件路径:~koa/response.js
// 给 body 和 status 添加 getter 和 setter
const response = {
    get body() {
        return this._body;
    },
    set body(val) {
        // 只要给 body 赋值就表明响应成功
        this.status = 200;
        this._body = val;
    },
    get status() {
        return this.res.statusCode;
    },
    set status(val) {
        this.res.statusCode = val;
    }
};

module.exports = response;复制代码

这里选择了 Koa 在使用时,response 对象上比较重要的两个属性进行处理,由于这两个属性是服务器响应客户端所必须的,并模仿了 request.js 的方式给 bodystatus 设置了 getter,不一样的是响应浏览器所作的实际上是赋值操做,因此又给这两个属性添加了 setter,对于 status 来讲,直接操做原生 res 对象的 statusCode 属性便可,由于同为赋值操做。

还有一点,响应是经过给 body 赋值实现,咱们认为只要触发了 bodysetter 就成功响应,因此在 bodygetter 中将响应状态码设置为 200,至于 body 赋值是如何实现响应的,放在后面再说。

五、ctx 代理 request、response 的属性

上面实现了经过 requestresponse 对属性的操做,Koa 虽然给咱们提供了多样的属性操做方式,但因为咱们程序猿(媛)们都很 “懒”,几乎没有人会在开发的时候愿意多写代码,大部分状况都是经过 ctx 直接操做 requestresponse 上的属性,这就是咱们如今的问题所在,这些属性经过 ctx 访问不到。

咱们须要给 ctx 对象作一个代理,让 ctx 能够访问到 requestresponse 上的属性,这个场景何曾相识,不正是 Vue 建立实例时,将传入参数对象 optionsdata 属性代理给实例自己的场景吗,既然如此,咱们也经过类似的方式实现,还记得上面引入的 context 模块做为实例的 context 属性所继承的对象,而剩下的最后一个核心文件 context.js 正是用来作这件事的,代码以下。

文件路径:~koa/context.js
const proto = {};

// 将传入对象属性代理给 ctx
function defineGetter(property, key) {
    proto.__defineGetter__(key, function () {
        return this[property][key];
    });
}

// 设置 ctx 值时直接操做传入对象的属性
function defineSetter(property, key) {
    proto.__defineSetter__(key, function (val) {
        this[property][key] = val;
    });
}

// 将 request 的 url 和 path 代理给 ctx
defineGetter("request", "url");
defineGetter("request", "path");

// 将 response 的 body 和 status 代理给 ctx
defineGetter("response", "body");
defineSetter("response", "body");
defineGetter("response", "status");
defineSetter("response", "status");

module.exports = proto;复制代码

Vue 中是使用 Object.defineProperty 来时实现的代理,而在 Koa 源码中借助了 delegate 第三方模块来实现的,并在添加代理时链式调用了 delegate 封装的方法,咱们并无直接使用 delegate 模块,而是将 delegate 内部的核心逻辑抽取出来在 context.js 中直接编写,这样方便你们理解原理,也能够清楚的知道是如何实现代理的。

咱们封装了两个方法 defineGetterdefineSetter 分别来实现取值和设置值时,将传入的属性(第二个参数)代理给传入的对象(第一个参数),函数内是经过 Object.prototype.__defineGetter__Object.prototype.__defineSetter__ 实现的,点击方法名可查看官方 API。

洋葱模型 —— 实现中间件的串行

如今已经实现了 ctx 上下文对象的建立,可是会发现咱们封装 ctx 以前所写的案例 use 回调中的代码并不能执行,也不会报错,根本缘由是 use 方法内传入的函数没有调用,在使用 Koa 的过程当中会发现,咱们每每使用多个 use,而且传入 use 的回调函数除了 ctx 还有第二个参数 next,而这个 next也是一个函数,调用 next 则执行下一个 use 中的回调函数,不然就会 “卡住”,这种执行机制被取名为 “洋葱模型”,而这些被执行的函数被称为 “中间件”,下面咱们就来分析这个 “洋葱模型” 并实现中间件的串行。


洋葱模型

一、洋葱模型分析

下面来看看表述洋葱模型的一个经典案例,结果彷佛让人匪夷所思,一时很难想到缘由,不着急先看了再说。

const Koa = require("koa");

const app = new Koa();

app.use((ctx, next) => {
    console.log(1);
    next();
    console.log(2);
});

app.use((ctx, next) => {
    console.log(3);
    next();
    console.log(4);
});

app.use((ctx, next) => {
    console.log(5);
    next();
    console.log(6);
});

app.listen(3000, () => {
    console.log("server start 3000");
});

// 1
// 3
// 5
// 6
// 4 复制代码

// 2根据上面的执行特性咱们不妨来分析如下,咱们知道 use 方法执行时实际上是把传入的回调函数放入了实例的 middlewares 数组中,而执行结果打印了 1 说明第一个回调函数被执行了,接着又打印了 2 说明第二个回调函数被执行了,根据上面的代码咱们能够大胆的猜测,第一个回调函数调用的 next 确定是一个函数,可能就是下一个回调函数,或者是 next 函数中执行了下一个回调函数,这样根据函数调用栈先进后出的原则,会在 next 执行完毕,即出栈后,继续执行上一个回调函数的代码。

二、支持异步的中间件串行

在实现中间件串行以前须要补充一点,中间件函数内调用 next 时,前面的代码出现异步,则会继续向下执行,等到异步执行结束后要执行的代码插入到同步代码中,这会致使执行顺序错乱,因此在官方推荐中告诉咱们任何遇到异步的操做前都须要使用 await 进行等待(包括 next,由于下一个中间件中可能包含异步操做),这也间接的说明了传入 use 的回调函数只要有异步代码须要 await,因此应该是 async 函数,而了解 ES7 特性 async/await 的咱们来讲,必定能分析出 next 返回的应该是一个 Promise 实例,下面是咱们在以前 application.js 基础上的实现。

文件路径:~koa/application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");

class Koa {
    contructor() {
        // 存储中间件
        this.middlewares = [];

        // 为了防止经过 this 修改属性而致使影响原引入文件的导出对象,作一个继承
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    use(fn) {
        // 将传给 use 的函数存入数组中
        this.middlewares.push(fn);
    }
    createContext(req, res) {
        // 或取定义的上下文
        let ctx = this.context;

        // 增长 request 和 response
        ctx.request = this.request;
        ctx.response = this.response;

        // 让 ctx、request、response 都具备原生的 req 和 res
        ctx.req = ctx.request.req = ctx.response.req = req;
        ctx.res = ctx.response.res = ctx.request.res = res;

        // 返回上下文对象
        return ctx;
    }
// ***************************** 如下为新增代码 *****************************
    compose(ctx, middles) {
        // 建立一个递归函数,参数为存储中间件的索引,从 0 开始
        function dispatch(index) {
            // 在全部中间件执行以后给 compose 返回一个 Promise(兼容一个中间件都没写的状况)
            if (index === middles.length) return Promise.resolve();

            // 取出第 index 个中间件函数
            const route = middles[index];

            // 为了兼容中间件传入的函数不是 async,必定要包装成一个 Promise
            return Promise.resolve(route(ctx, () => dispatch(index + 1)));
        }
        return dispatch(0); // 默认执行一次
    }
// ***************************** 以上为新增代码 *****************************
    handleRequest(req, res) {
        // 建立 ctx 上下文对象
        let ctx = this.createContext(req, res);

// ***************************** 如下为新增代码 *****************************
        // 执行 compose 将中间件组合在一块儿
        this.compose(ctx, this.middlewares);
// ***************************** 以上为新增代码 *****************************
    }
    listen(...args) {
        // 建立服务
        let server = http.createServer(this.handleRequest.bind(this));

        // 启动服务
        server.listen(...args);
    }
}

module.exports = Koa;复制代码

仔细想一想咱们其实在利用循环执行每个 middlewares 中的函数,并且须要把下一个中间件函数的执行做为函数体的代码包装一层成为新的函数,并做为参数 next 传入,那么在上一个中间件函数内部调用 next 就至关于先执行了下一个中间件函数,而下一个中间件函数内部调用 next,又先执行了下一个的下一个中间件函数,依次类推。

直到执行到最后一个中间件函数,调用了 next,可是 middlewares 中已经没有下一个中间件函数了,这也是为何咱们要给下一个中间件函数外包了一层函数而不是直接将中间件函数传入的缘由之一(另外一个缘由是解决传参问题,由于在执行时还要传入下一个中间件函数),可是防止递归 “死循环”,要配合一个终止条件,即指向 middlewares 索引的变量等于了 middlewares 的长度,最后只是至关于执行了一个只有一条判断语句的函数就 return 的函数,而并无报错。

在这整个过程当中若是有任意一个 next 没有被调用,就不会向下执行其余的中间件函数,这样就 “卡住了”,彻底符合 Koa 中间件的执行规则,而 await 事后也就是下一个中间件优先执行完成,则会继续执行当前中间件 next 调用下面的代码,这也就是 一、三、五、六、四、2 的由来。

为了实现所描述的执行过程,将全部中间件串行的逻辑抽出了一个 compose 方法,可是咱们没有使用普通的循环,而是使用递归实现的,首先在 compose 建立 dispatch 递归函数,参数为当前数组函数的索引,初始值为 0,函数逻辑是先取出第一个函数执行,并传入一个回调函数参数,回调函数参数中递归 dispatch,参数 +1,这样就会将整个中间件串行起来了。

可是上面的串行也只是同步串行,若是某个中间件内部须要等待异步,则调用得 next 函数必须返回一个 Promise,有些中间件没有执行异步,则不须要 async 函数,也不会返回 Promise,而 Koa 规定只要遇到 next 就须要等待,则将取出每个中间件函数执行后的结果使用 Promise.resolve 强行包装成一个成功态的 Promise,就对异步进行了兼容。

咱们最后也但愿 compose 返回一个 Promise 方便执行一些只有在中间件都执行后才会执行的逻辑,每次串行最后执行的都是一个只有一条判断逻辑就 return 了的函数(包含一个中间件也没有的状况),此时 compose 返回了 undefined,没法调用 then 方法,为了兼容这种状况也强行的使用相同的 “招数”,在判断条件的 return 关键字后面加上了 Promise.resolve(),直接返回了一个成功态的 Promise。

注意:官方只是推荐咱们在调用 next 的时候使用 await 等待,即便执行的 next 真的存在异步,也不是非 await 不可,咱们彻底可使用 return 来代替 await,惟一的区别就是 next 调用后,下面的代码不会再执行了,类比 “洋葱模型”,形象地说就是 “下去了就上不来了”,这个彻底能够根据咱们的使用须要而定,若是 next 后面再也不有任何逻辑,彻底可使用 return 替代。

实现真正的响应

在对 ctx 实现属性代理后,咱们经过 ctx.body 从新赋值其实只是改变了 response.js 导出对象的 _body属性,而并无实现真正的响应,看下面这个 Koa 的例子。

const Koa = require("koa");
const fs = require("fs");

const app = new Koa();

app.use(async (ctx, next) => {
    ctx.body = "hello";
    await next();
});

app.use(async (ctx, next) => {
    ctx.body = fs.createReadStream("1.txt");

    ctx.body = await new Promise((resolve, reject) => {
        setTimeout(() => resolve("panda"), 3000);
    });
});

app.listen(3000, () => {
    console.log("server start 3000"); 复制代码

});其实最后响应给客户端的值是 panda,正常在最后一个中间件执行后,因为异步定时器的代码没有执行完,ctx.body 最后的值应该是 1.txt 的可读流,这与客户端接收到的值相违背,经过这个猜测上的差别咱们应该知道,compose 在串行执行中间件后为何要返回一个 Promise 了,由于最后执行的只有判断语句的函数会等待咱们例子中最后一个 use 传入的中间件函数执行完毕调用,也就是说在执行 compose 返回值的 then 时,ctx.body 的值已是 panda 了。

文件路径:~koa/application.js
const http = require("http");

// ***************************** 如下为新增代码 *****************************
const Stream = require("stream");
// ***************************** 以上为新增代码 *****************************

const context = require("./context");
const request = require("./request");
const response = require("./response");

class Koa {
    contructor() {
        // 存储中间件
        this.middlewares = [];

        // 为了防止经过 this 修改属性而致使影响原引入文件的导出对象,作一个继承
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    use(fn) {
        // 将传给 use 的函数存入数组中
        this.middlewares.push(fn);
    }
    createContext(req, res) {
        // 或取定义的上下文
        let ctx = this.context;

        // 增长 request 和 response
        ctx.request = this.request;
        ctx.response = this.response;

        // 让 ctx、request、response 都具备原生的 req 和 res
        ctx.req = ctx.request.req = ctx.response.req = req;
        ctx.res = ctx.response.res = ctx.request.res = res;

        // 返回上下文对象
        return ctx;
    }
    compose(ctx, middles) {
        // 建立一个递归函数,参数为存储中间件的索引,从 0 开始
        function dispatch(index) {
            // 在全部中间件执行以后给 compose 返回一个 Promise(兼容一个中间件都没写的状况)
            if (index === middles.length) return Promise.resolve();

            // 取出第 index 个中间件函数
            const route = middles[index];

            // 为了兼容中间件传入的函数不是 async,必定要包装成一个 Promise
            return Promise.resolve(route(ctx, () => dispatch(index + 1)));
        }
        return dispatch(0); // 默认执行一次
    }
    handleRequest(req, res) {
        // 建立 ctx 上下文对象
        let ctx = this.createContext(req, res);

// ***************************** 如下为修改代码 *****************************
        // 设置默认状态码(Koa 规定),必须在调用中间件以前
        ctx.status = 404;

        // 执行 compose 将中间件组合在一块儿
        this.compose(ctx, this.middlewares).then(() => {
            // 获取最后 body 的值
            let body = ctx.body;

            // 检测 ctx.body 的类型,并使用对应的方式将值响应给浏览器
            if (Buffer.isBuffer(body) || typeof body === "string") {
                // 处理 Buffer 类型的数据
                res.setHeader("Content-Type", "text/plain;charset=utf8");
                res.end(body);
            } else if (typeof body === "object") {
                // 处理对象类型
                res.setHeader("Content-Type", "application/json;charset=utf8");
                res.end(JSON.stringify(body));
            } else if (body instanceof Stream) {
                // 处理流类型的数据
                body.pipe(res);
            } else {
                res.end("Not Found");
            }
        });
// ***************************** 以上为修改代码 *****************************
    }
    listen(...args) {
        // 建立服务
        let server = http.createServer(this.handleRequest.bind(this));

        // 启动服务
        server.listen(...args);
    }
}

module.exports = Koa;复制代码

处理 response 时,在 bodysetter 中将状态码设置为了 200,就是说须要设置 ctx.body 去触发 setter 让响应成功,若是没有给 ctx.body 设置任何值,默认应该是无响应的,在官方文档也有默认状态码为 404 的明确说明,因此在 handleRequest 把状态码设置为了 404,但必须在 compose 执行以前才叫默认状态码,由于中间件中可能会操做 ctx.body,从新设置状态码。

comosethen 中,也就是在全部中间件执行后,咱们取出 ctx.body 的值,即为最后生效的响应值,对该值进行了数据类型验证,如 Buffer、字符串、对象和流,并分别用不一样的方式处理了响应,但本质都是调用的原生 res 对象的 end 方法。

中间件错误处理

在上面的逻辑当中咱们实现了不少 Koa 的核心逻辑,可是只考虑了顺利执行的状况,并无考虑若是中间件中代码执行出现错误的问题,以下面案例。

const Koa = require("koa");

const app = new Koa();

app.use((ctx, next) => {
    // 抛出异常
    throw new Error("Error");
});

// 添加 error 监听
app.on("error", err => {
    console.log(err);
});

app.listen(3000, () => {
    console.log("server start 3000");
});复制代码

咱们之因此让 compose 方法在执行全部中间件后返回一个 Promise 还有一个更重要的意义,由于在 Promise 链式调用中,只要其中任何一个环节出现代码执行错误或抛出异常,都会直接执行出现错误的 then 方法中错误的回调或者最后的 catch 方法,对于 Koa 中间件的串行而言,最后一个 then 调用 catch 方法就是 compose 的返回值调用 then 后继续调用的 catchcatch 内能够捕获到任意一个中间件执行时出现的错误。

文件路径:~koa/application.js
const http = require("http");
const Stream = require("stream");

// ***************************** 如下为新增代码 *****************************
const EventEmitter = require("events");
const httpServer = require("_http_server");
// ***************************** 以上为新增代码 *****************************

const context = require("./context");
const request = require("./request");
const response = require("./response");

// ***************************** 如下为修改代码 *****************************
// 继承 EventEmitter 后能够用建立的实例 app 添加 error 监听,能够经过 emit 触发监听
class Koa extends EventEmitter {
    contructor() {
        supper();
// ***************************** 以上为修改代码 *****************************

        // 存储中间件
        this.middlewares = [];

        // 为了防止经过 this 修改属性而致使影响原引入文件的导出对象,作一个继承
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    use(fn) {
        // 将传给 use 的函数存入数组中
        this.middlewares.push(fn);
    }
    createContext(req, res) {
        // 或取定义的上下文
        let ctx = this.context;

        // 增长 request 和 response
        ctx.request = this.request;
        ctx.response = this.response;

        // 让 ctx、request、response 都具备原生的 req 和 res
        ctx.req = ctx.request.req = ctx.response.req = req;
        ctx.res = ctx.response.res = ctx.request.res = res;

        // 返回上下文对象
        return ctx;
    }
    compose(ctx, middles) {
        // 建立一个递归函数,参数为存储中间件的索引,从 0 开始
        function dispatch(index) {
            // 在全部中间件执行以后给 compose 返回一个 Promise(兼容一个中间件都没写的状况)
            if (index === middles.length) return Promise.resolve();

            // 取出第 index 个中间件函数
            const route = middles[index];

            // 为了兼容中间件传入的函数不是 async,必定要包装成一个 Promise
            return Promise.resolve(route(ctx, () => dispatch(index + 1)));
        }
        return dispatch(0); // 默认执行一次
    }
    handleRequest(req, res) {
        // 建立 ctx 上下文对象
        let ctx = this.createContext(req, res);

        // 设置默认状态码(Koa 规定),必须在调用中间件以前
        ctx.status = 404;

        // 执行 compose 将中间件组合在一块儿
        this.compose(ctx, this.middlewares).then(() => {
            // 获取最后 body 的值
            let body = ctx.body;

            // 检测 ctx.body 的类型,并使用对应的方式将值响应给浏览器
            if (Buffer.isBuffer(body) || typeof body === "string") {
                // 处理 Buffer 类型的数据
                res.setHeader("Content-Type", "text/plain;charset=utf8");
                res.end(body);
            } else if (typeof body === "object") {
                // 处理对象类型
                res.setHeader("Content-Type", "application/json;charset=utf8");
                res.end(JSON.stringify(body));
            } else if (body instanceof Stream) {
                // 处理流类型的数据
                body.pipe(res);
            } else {
                res.end("Not Found");
            }
// ***************************** 如下为修改代码 *****************************
        }).catch(err => {
            // 执行 error 事件
            this.emit("error", err);

            // 设置 500 状态码
            ctx.status = 500;

            // 返回状态码对应的信息响应浏览器
            res.end(httpServer.STATUS_CODES[ctx.status]);
        });
// ***************************** 以上为修改代码 *****************************
    }
    listen(...args) {
        // 建立服务
        let server = http.createServer(this.handleRequest.bind(this));

        // 启动服务
        server.listen(...args);
    }
}

module.exports = Koa;复制代码

在使用的案例当中,使用 app(即 Koa 建立的实例)监听了一个 error 事件,当中间件执行错误时会触发该监听的回调,这让咱们想起了 NodeJS 中一个重要的核心模块 events,这个模块帮咱们提供了一个事件机制,经过 on 方法添加监听,经过 emit 触发监听,因此咱们引入了 events,并让 Koa 类继承了 events 导入的 EventEmitter 类,此时 Koa 的实例就可使用 EventEmitter 原型对象上的 onemit 方法。

compose 执行后调用的 catch 中,经过实例调用了 emit,并传入了事件类型 error 和错误对象,这样就是实现了中间件的错误监听,只要中间件执行出错,就会执行案例中错误监听的回调。

让引入的 Koa 直接指向 application.js

在上面咱们实现了 Koa 大部分经常使用功能的核心逻辑,但还有一点美中不足,就是咱们引入本身的简易版 Koa时,默认会查找 koa 路径下的 index.js,想要执行咱们的 Koa 必需要使用路径找到 application.js,代码以下。

如今的引入方式
const Koa = require("./koa/application");复制代码
但愿的引入方式
const Koa = require("./koa");复制代码

咱们更但愿像直接引入指定 koa 文件夹,就能够找到 application.js 文件并执行,这就须要咱们在 koa 文件夹建立 package.json 文件,并在动一点小小的 “手脚” 以下。

文件路径:~koa/package.js
{
    .
    .
    .
    "main": "./application.js",
    .
    .
    .
}复制代码

Koa 原理图

在文章最后一节送给你们一张 Koa 执行的原理图,这张图片是准备写这篇文章时在 Google 上发现的,以为把 Koa 的整个流程表达的很是清楚,因此这里拿来帮助你们理解 Koa 框架的原理和执行过程。


Koa 原理图

之因此没有在文章开篇放上这张图是由于以为在彻底没有了解过 Koa 的原理以前,可能有一部分小伙伴看这张图会懵,会打消学习的积极性,由于本篇的目的就是带着你们从零到有的,一步一步实现简易版 Koa,梳理 Koa的核心逻辑,若是你已经看到了这里,是否是以为这张图出现的不早不晚,刚恰好。

总结

最后仍是在这里作一个总结,在 Koa 中主要的部分有 listen 建立服务器、封装上下文对象 ctx 并代理属性、use 方法添加中间件、compose 串行执行中间、让 Koa 继承 EventEmitter 实现错误监听,而我我的以为最重要的就是 compose,它是一个事件串行机制,也是实现 “洋葱模型” 的核心,现在 compose 已经再也不只是一个方法名,而是一种编程思想,用于将多个程序串行在一块儿,或同步,或异步,在 Koa 中自没必要多说,由于你们已经见识过了,composeReact 中也起着串联中间件的做用,如串联 promiseredux-thunklogger 等,在 Webpack 源码依赖的核心模块 tapable 中也有所应用,在咱们的学习过程当中,这样优秀的编程思想是应该重点吸取的。

原文出自:https://www.pandashen.com

相关文章
相关标签/搜索