Koa 2.x
版本是当下最流行的 NodeJS 框架,同时社区涌现出一大批围绕 Koa 2.x
的中间件以及基于 Koa 2.x
封装的企业级框架,如 egg.js
,然而 Koa
自己的代码却很是精简,精简到全部文件的代码去掉注释后还不足 2000
行,本篇就围绕着这 2000
行不到的代码抽出核心逻辑进行分析,并压缩成一版只有 200
行不到的简易版 Koa
。node
在下面的内容中,咱们将对 Koa
所使用的功能由简入深的分析,首先会给出使用案例,而后根据使用方式,分析实现原理,最后对分析的功能进行封装,封装过程会从零开始并一步一步完善,代码也是从少到多,会完整的看到一个简版 Koa
诞生的过程,再此以前咱们打开 Koa
源码地址。git
经过上面对 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
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
方法启动一个服务器。数组
Koa
还有一个很重要的特性,就是它的 ctx
上下文对象,咱们能够调用 ctx
的 request
和 response
属性获取原 req
和 res
的属性和方法,也在 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
为 use
方法的第一个参数,request
和 response
是 ctx
新增的,而经过这两个属性又均可以获取原生的 req
和 res
属性,ctx
自己也能够获取到原生的 req
和 res
,咱们能够分析出,ctx
是对这些属性作了一个集成,或者说特殊处理。浏览器
源码的文件目录中正好有与 request
、response
名字相对应的文件,而且还有 context
名字的文件,咱们其实能够分析出这三个文件就是用于封装 ctx
上下文对象使用的,而封装 ctx
中也会用到 req
和 res
,因此核心逻辑应该在 handleRequest
中实现。bash
在使用案例中 ctx
是做为 use
方法中回调函数的参数,因此咱们分析应该有一个数组统一管理调用 use
后传入的函数,Koa
应该有一个属性,值为数组,用来存储这些函数,下面是实现代码。
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;复制代码
首先,给实例建立了三个属性 context
、request
和 response
分别继承了 context.js
、request.js
和 response.js
导出的对象,之因此这么作而不是直接赋值是防止操做实例属性时 “污染” 原对象,而获取原模块导出对象的属性能够经过原型链进行查找,并不影响取值。
其次,给实例挂载了 middlewares
属性,值为数组,为了存储 use
方法调用时传入的函数,在 handleRequest
把建立 ctx
属性及引用的过程单独抽取成了 createContext
方法,并在 handleRequest
中调用,返回值为建立好的 ctx
对象,而在 createContext
中咱们根据案例中的规则构建了 ctx
的属性相关的各类引用关系。
上面构建的属性中,全部经过访问原生 req
或 res
的属性都能获取到,反之则是 undefined
,这就须要咱们去构建 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;复制代码
上面咱们只构造了两个属性 url
和 path
,咱们知道 url
是原生所自带的属性,咱们在使用 ctx.request.url
获取是经过 request
对象设置的 getter
,将 ctx.request.req.url
的值返回了。
path
是原生 req
所没有的属性,但倒是经过原生 req
的 url
属性和 url
模块共同构建出来的,因此咱们一样用了给 request
对象设置 getter
的方式获取 req
的 url
属性,并使用 url
模块将转换对象中的 pathname
返回,此时就能够经过 ctx.request.path
来获取访问路径,至于源码中咱们没有处理的 req
属性都是经过这样的方式创建的引用关系。
Koa
中 response
对象的真正做用是给客户端进行响应,使用时是经过访问属性获取,并经过从新赋值实现响应,可是如今 response
获取的属性都是 undefined
,咱们这里先无论响应给浏览器的问题,首先要让 response
下的某个属性有值才行,下面咱们来实现 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
的方式给 body
和 status
设置了 getter
,不一样的是响应浏览器所作的实际上是赋值操做,因此又给这两个属性添加了 setter
,对于 status
来讲,直接操做原生 res
对象的 statusCode
属性便可,由于同为赋值操做。
还有一点,响应是经过给 body
赋值实现,咱们认为只要触发了 body
的 setter
就成功响应,因此在 body
的 getter
中将响应状态码设置为 200
,至于 body
赋值是如何实现响应的,放在后面再说。
上面实现了经过 request
和 response
对属性的操做,Koa
虽然给咱们提供了多样的属性操做方式,但因为咱们程序猿(媛)们都很 “懒”,几乎没有人会在开发的时候愿意多写代码,大部分状况都是经过 ctx
直接操做 request
和 response
上的属性,这就是咱们如今的问题所在,这些属性经过 ctx
访问不到。
咱们须要给 ctx
对象作一个代理,让 ctx
能够访问到 request
和 response
上的属性,这个场景何曾相识,不正是 Vue
建立实例时,将传入参数对象 options
的 data
属性代理给实例自己的场景吗,既然如此,咱们也经过类似的方式实现,还记得上面引入的 context
模块做为实例的 context
属性所继承的对象,而剩下的最后一个核心文件 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
中直接编写,这样方便你们理解原理,也能够清楚的知道是如何实现代理的。
咱们封装了两个方法 defineGetter
和 defineSetter
分别来实现取值和设置值时,将传入的属性(第二个参数)代理给传入的对象(第一个参数),函数内是经过 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
基础上的实现。
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
了。
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
时,在 body
的 setter
中将状态码设置为了 200
,就是说须要设置 ctx.body
去触发 setter
让响应成功,若是没有给 ctx.body
设置任何值,默认应该是无响应的,在官方文档也有默认状态码为 404
的明确说明,因此在 handleRequest
把状态码设置为了 404
,但必须在 compose
执行以前才叫默认状态码,由于中间件中可能会操做 ctx.body
,从新设置状态码。
在 comose
的 then
中,也就是在全部中间件执行后,咱们取出 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
后继续调用的 catch
,catch
内能够捕获到任意一个中间件执行时出现的错误。
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
原型对象上的 on
和 emit
方法。
在 compose
执行后调用的 catch
中,经过实例调用了 emit
,并传入了事件类型 error
和错误对象,这样就是实现了中间件的错误监听,只要中间件执行出错,就会执行案例中错误监听的回调。
在上面咱们实现了 Koa
大部分经常使用功能的核心逻辑,但还有一点美中不足,就是咱们引入本身的简易版 Koa
时,默认会查找 koa
路径下的 index.js
,想要执行咱们的 Koa
必需要使用路径找到 application.js
,代码以下。
const Koa = require("./koa/application");复制代码
const Koa = require("./koa");复制代码
咱们更但愿像直接引入指定 koa
文件夹,就能够找到 application.js
文件并执行,这就须要咱们在 koa
文件夹建立 package.json
文件,并在动一点小小的 “手脚” 以下。
{
.
.
.
"main": "./application.js",
.
.
.
}复制代码
在文章最后一节送给你们一张 Koa
执行的原理图,这张图片是准备写这篇文章时在 Google 上发现的,以为把 Koa
的整个流程表达的很是清楚,因此这里拿来帮助你们理解 Koa
框架的原理和执行过程。
之因此没有在文章开篇放上这张图是由于以为在彻底没有了解过 Koa
的原理以前,可能有一部分小伙伴看这张图会懵,会打消学习的积极性,由于本篇的目的就是带着你们从零到有的,一步一步实现简易版 Koa
,梳理 Koa
的核心逻辑,若是你已经看到了这里,是否是以为这张图出现的不早不晚,刚恰好。
最后仍是在这里作一个总结,在 Koa
中主要的部分有 listen
建立服务器、封装上下文对象 ctx
并代理属性、use
方法添加中间件、compose
串行执行中间、让 Koa
继承 EventEmitter
实现错误监听,而我我的以为最重要的就是 compose
,它是一个事件串行机制,也是实现 “洋葱模型” 的核心,现在 compose
已经再也不只是一个方法名,而是一种编程思想,用于将多个程序串行在一块儿,或同步,或异步,在 Koa
中自没必要多说,由于你们已经见识过了,compose
在 React
中也起着串联中间件的做用,如串联 promise
、redux-thunk
、logger
等,在 Webpack
源码依赖的核心模块 tapable
中也有所应用,在咱们的学习过程当中,这样优秀的编程思想是应该重点吸取的。