用 Node.js
写一个web 服务器
,我后面曾经写过两篇文章了:
- 第一篇是不应用任何框架也能搭建一个
web 服务器
,次要是相熟Node.js
原生 API 的应用:应用 Node.js 原生 API 写一个 web 服务器 - 第二篇文章是看了
Express
的根本用法,更次要的是看了下他的源码:手写 Express.js 源码
Express
的源码还是比较复杂的,自带了路由解决和动态资源反对等等性能,性能比拟全面。与之相比,本文要讲的 Koa
就简洁多了,Koa
尽管是 Express
的原班人马写的,然而设计思路却不一样。Express
更多是偏差 All in one
的思维,各种性能都集成在一起,而 Koa
自身的库只有一个中间件内核,其余像路由解决和动态资源这些性能都没有,全副须要引入第三方中间件库能力实现。上面这张图能够直观的看到 Express
和koa
在性能上的区别,此图来自于官网文档:
基于 Koa
的这种架构,我打算会分几篇文章来写,全部都是源码解析:
Koa
的外围架构会写一篇文章,也就是本文。- 对于一个
web 服务器
来说,路由是必不可少的,所以@koa/router
会写一篇文章。 - 另外可能会写一些罕用中间件,动态文件反对或者
bodyparser
等等,具体还没定,可能会有一篇或多篇文章。
本文可运行迷你版 Koa 代码曾经上传 GitHub,拿下来,一边玩代码一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore
简略示例
我写源码解析,个别都遵循一个简略的套路:先引入库,写一个简略的例子,而后本人手写源码来代替这个库,并让咱们的例子顺利运行。本文也是遵循这个套路,因为 Koa
的外围库只有中间件,所以咱们写出的例子也比较简单,也只有中间件。
Hello World
第一个例子是Hello World
,轻易申请一个门路都返回Hello World
。
const Koa = require("koa");
const app = new Koa();
app.use((ctx) => {ctx.body = "Hello World";});
const port = 3001;
app.listen(port, () => {console.log(`Server is running on http://127.0.0.1:${port}/`);
});
logger
而后再来一个 logger
吧,就是记录下解决以后申请花了多长时间:
app.use(async (ctx, next) => {const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
留神这个中间件应该放到 Hello World
的后面。
从下面两个例子的代码来看,Koa
跟 Express
有几个显著的区别:
ctx
代替了req
和res
- 能够应用 JS 的新 API 了,比方
async
和await
手写源码
手写源码前咱们看看用到了哪些 API,这些就是咱们手写的指标:
- new Koa():首先必定是
Koa
这个类了,因为他应用new
进行实例化,所以咱们认为他是一个类。 - app.use:app是
Koa
的一个实例,app.use
看起来是一个增加中间件的实例办法。 - app.listen:启动服务器的实例办法
- ctx:这个是
Koa
的上下文,看起来代替了以前的req
和res
- async和 await:反对新的语法,而且能应用
await next()
,阐明next()
返回的很可能是一个promise
。
本文的手写源码全副参照官网源码写成,文件名和函数名尽量保持一致,写到具体的办法时我也会贴上官网源码地址。Koa
这个库代码并不多,次要都在这个文件夹外面:https://github.com/koajs/koa/tree/master/lib,上面咱们开始吧。
Koa 类
从 Koa
我的项目的 package.json
外面的 main
这行代码能够看出,整个利用的入口是 lib/application.js
这个文件:
"main": "lib/application.js",
lib/application.js
这个文件就是咱们常常用的 Koa
类,尽管咱们常常叫他 Koa
类,然而在源码外面这个类叫做Application
。咱们先来写一下这个类的壳吧:
// application.js
const Emitter = require("events");
// module.exports 间接导出 Application 类
module.exports = class Application extends Emitter {
// 构造函数先运行下父类的构造函数
// 再进行一些初始化工作
constructor() {super();
// middleware 实例属性初始化为一个空数组,用来存储后续可能的中间件
this.middleware = [];}
};
这段代码咱们能够看出,Koa
间接应用 class
关键字来申明类了,看过我之前 Express
源码解析的敌人可能还有印象,Express
源码外面还是应用的老的 prototype
来实现面向对象的。所以 Koa
我的项目介绍外面的 Expressive middleware for node.js using ES2017 async functions
并不是一句虚言,它不仅反对 ES2017
新的 API,而且在本人的源码外面外面也是用的新 API。我想这也是 Koa
要求运行环境必须是 node v7.6.0 or higher
的起因吧。所以到这里咱们其实曾经能够看出 Koa
和Express
的一个重大区别了,那就是:Express
应用老的 API,兼容性更强,能够在老的 Node.js
版本上运行;Koa
因为应用了新 API,只能在 v7.6.0
或者更高版本上运行了。
这段代码还有个点须要留神,那就是 Application
继承自 Node.js
原生的 EventEmitter
类,这个类其实就是一个公布订阅模式,能够订阅和公布音讯,我在另一篇文章外面具体讲过他的源码。所以他有些办法如果在 application.js
外面找不到,那可能就是继承自EventEmitter
,比方下图这行代码:
这里有 this.on
这个办法,看起来他应该是 Application
的一个实例办法,然而这个文件外面没有,其实他就是继承自 EventEmitter
,是用来给error
这个事件增加回调函数的。这行代码 if
外面的 this.listenerCount
也是 EventEmitter
的一个实例办法。
Application
类齐全是 JS 面向对象的使用,如果你对 JS 面向对象还不是很相熟,能够先看看这篇文章:https://segmentfault.com/a/1190000023201844。
app.use
从咱们后面的应用示例能够看出 app.use
的作用就是增加一个中间件,咱们在构造函数外面也初始化了一个变量 middleware
,用来存储中间件,所以app.use
的代码就很简略了,将接管到的中间件塞到这个数组就行:
use(fn) {
// 中间件必须是一个函数,不然就报错
if (typeof fn !== "function")
throw new TypeError("middleware must be a function!");
// 解决逻辑很简略,将接管到的中间件塞入到 middleware 数组就行
this.middleware.push(fn);
return this;
}
留神 app.use
办法最初返回了 this
,这个有点意思,为什么要返回this
呢?这个其实我之前在其余文章讲过的:类的实例办法返回 this
能够实现链式调用。比方这里的 app.use
就能够间断点点点了,像这样:
app.use(middlewaer1).use(middlewaer2).use(middlewaer3)
为什么会有这种成果呢?因为这里的 this
其实就是以后实例,也就是 app
,所以app.use()
的返回值就是 app
,app
上有个实例办法use
,所以能够持续点app.use().use()
。
app.use
的官网源码看这里: https://github.com/koajs/koa/blob/master/lib/application.js#L122
app.listen
在后面的示例中,app.listen
的作用是用来启动服务器,看过后面用原生 API 实现 web 服务器
的敌人都晓得,要启动服务器须要调用原生的 http.createServer
,所以这个办法就是用来调用http.createServer
的。
listen(...args) {const server = http.createServer(this.callback());
return server.listen(...args);
}
这个办法自身其实没有太多可说的,只是调用 http
模块启动服务而已,次要的逻辑都在 this.callback()
外面了。
app.listen
的官网源码看这里:https://github.com/koajs/koa/blob/master/lib/application.js#L79
app.callback
this.callback()
是传给 http.createServer
的回调函数,也是一个实例函数,这个函数必须合乎 http.createServer
的参数模式,也就是
http.createServer(function(req, res){})
所以 this.callback()
的返回值必须是一个函数,而且是这种模式function(req, res){}
。
除了模式必须合乎外,this.callback()
具体要干什么呢?他是 http
模块的回调函数,所以他必须解决所有的网络申请,所有解决逻辑都必须在这个办法外面。然而 Koa
的解决逻辑是以中间件的模式存在的,对于一个申请来说,他必须一个一个的穿过所有的中间件,具体穿过的逻辑,你当然能够遍历 middleware
这个数组,将外面的办法一个一个拿进去解决,当然也能够用业界更罕用的办法:compose
。
compose
一般来说就是将一系列办法合并成一个办法来不便调用,具体实现的模式并不是固定的,有面试中常见的用 reduce
实现的 compose
,也有像Koa
这样依据本人需要独自实现的 compose
。Koa
的compose
也独自封装了一个库 koa-compose
,这个库源码也是咱们必须要看的,咱们一步一步来,先把this.callback
写进去吧。
callback() {
// compose 来自 koa-compose 库,就是将中间件合并成一个函数
// 咱们须要本人实现
const fn = compose(this.middleware);
// callback 返回值必须合乎 http.createServer 参数模式
// 即 (req, res) => {}
const handleRequest = (req, res) => {const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
这个办法先用 koa-compose
将中间件都合成了一个函数 fn
,而后在http.createServer
的回调外面应用 req
和res
创立了一个 Koa
罕用的上下文 ctx
,而后再调用this.handleRequest
来真正解决网络申请。留神这里的 this.handleRequest
是个实例办法,和以后办法外面的局部变量 handleRequest
并不是一个货色。这几个办法咱们一个一个来看下。
this.callback
对应的官网源码看这里:https://github.com/koajs/koa/blob/master/lib/application.js#L143
koa-compose
koa-compose
尽管被作为了一个独自的库,然而他的作用却很要害,所以咱们也来看看他的源码吧。koa-compose
的作用是将一个中间件组成的数组合并成一个办法以便内部调用。咱们先来回顾下一个 Koa
中间件的构造:
function middleware(ctx, next) {}
这个数组就是有很多这样的中间件:
[function middleware1(ctx, next) {},
function middleware2(ctx, next) {}]
Koa
的合并思路并不简单,就是让 compose
再返回一个函数,返回的这个函数会开始这个数组的遍历工作:
function compose(middleware) {
// 参数查看,middleware 必须是一个数组
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
// 数组外面的每一项都必须是一个办法
for (const fn of middleware) {if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
// 返回一个办法,这个办法就是 compose 的后果
// 内部能够通过调用这个办法来开起中间件数组的遍历
// 参数模式和一般中间件一样,都是 context 和 next
return function (context, next) {return dispatch(0); // 开始中间件执行,从数组第一个开始
// 执行中间件的办法
function dispatch(i) {let fn = middleware[i]; // 取出须要执行的中间件
// 如果 i 等于数组长度,阐明数组曾经执行完了
if (i === middleware.length) {fn = next; // 这里让 fn 等于内部传进来的 next,其实是进行收尾工作,比方返回 404}
// 如果内部没有传收尾的 next,间接就 resolve
if (!fn) {return Promise.resolve();
}
// 执行中间件,留神传给中间件接管的参数应该是 context 和 next
// 传给中间件的 next 是 dispatch.bind(null, i + 1)
// 所以中间件外面调用 next 的时候其实调用的是 dispatch(i + 1),也就是执行下一个中间件
try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err);
}
}
};
}
下面代码次要的逻辑就是这行:
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
这里的 fn
就是咱们本人写的中间件,比方文章开始那个logger
,咱们略微改下看得更分明:
const logger = async (ctx, next) => {const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
};
app.use(logger);
那咱们 compose
外面执行的其实是:
logger(context, dispatch.bind(null, i + 1));
也就是说 logger
接管到的 next
其实是 dispatch.bind(null, i + 1)
,你调用next()
的时候,其实调用的是dispatch(i + 1)
,这样就达到了执行数组下一个中间件的成果。
另外因为中间件在返回前还包裹了一层 Promise.resolve
,所以咱们所有本人写的中间件,无论你是否用了Promise
,next
调用后返回的都是一个Promise
,所以你能够应用await next()
。
koa-compose
的源码看这里:https://github.com/koajs/compose/blob/master/index.js
app.createContext
下面用到的 this.createContext
也是一个实例办法。这个办法依据 http.createServer
传入的 req
和res
来构建 ctx
这个上下文,官网源码长这样:
这段代码外面 context
,ctx
,response
,res
,request
,req
,app
这几个变量互相赋值,头都看晕了。其实齐全没必要陷入这堆面条外面去,咱们只须要将他的思路和骨架拎分明就行,那怎么来拎呢?
- 首先搞清楚他这么赋值的目标,他的目标其实很简略,就是为了使用方便。通过一个变量能够很不便的拿到其余变量,比方我当初只有
request
,然而我想要的是req
,怎么办呢?通过这种赋值后,间接用request.req
就行。其余的相似,这种面条式的赋值我很难说好还是不好,然而应用时的确很不便,毛病就是看源码时容易陷进去。 - 那
request
和req
有啥区别?这两个变量长得这么像,到底是干啥的?这就要说到Koa
对于原生req
的扩大,咱们晓得http.createServer
的回调外面会传入req
作为申请对象的形容,外面能够拿到申请的header
啊,method
啊这些变量。然而Koa
感觉这个req
提供的 API 不好用,所以他在这个根底上扩大了一些 API,其实就是一些语法糖,扩大后的req
就变成了request
。之所以扩大后还保留的原始的req
,应该也是想为用户提供更多抉择吧。 所以这两个变量的区别就是request
是Koa
包装过的req
,req
是原生的申请对象。response
和res
也是相似的。 - 既然
request
和response
都只是包装过的语法糖,那其实Koa
没有这两个变量也能跑起来。所以咱们拎骨架的时候齐全能够将这两个变量踢出去,这下骨架就清晰了。
那咱们踢出 response
和request
后再来写下 createContext
这个办法:
// 创立上下文 ctx 对象的函数
createContext(req, res) {const context = Object.create(this.context);
context.app = this;
context.req = req;
context.res = res;
return context;
}
这下整个世界感觉都清新了,context
上的货色也高深莫测了。然而咱们的 context
最后是来自 this.context
的,这个变量还必须看下。
app.createContext
对应的官网源码看这里:https://github.com/koajs/koa/blob/master/lib/application.js#L177
context.js
下面的 this.context
其实就是来自 context.js
,所以咱们先在Application
构造函数外面增加这个变量:
// application.js
const context = require("./context");
// 构造函数外面
constructor() {
// 省略其余代码
this.context = context;
}
而后再来看看 context.js
外面有啥,context.js
的构造大略是这个样子:
const delegate = require("delegates");
module.exports = {inspect() {},
toJSON() {},
throw() {},
onerror() {},
};
const proto = module.exports;
delegate(proto, "response")
.method("set")
.method("append")
.access("message")
.access("body");
delegate(proto, "request")
.method("acceptsLanguages")
.method("accepts")
.access("querystring")
.access("socket");
这段代码外面 context
导出的是一个对象 proto
,这个对象自身有一些办法,inspect
,toJSON
之类的。而后还有一堆 delegate().method()
,delegate().access()
之类的。嗯,这个是干啥的呢?要晓得这个的作用,咱们须要去看 delegates
这个库:https://github.com/tj/node-delegates,这个库也是 tj
大神写的。个别应用是这样的:
delegate(proto, target).method("set");
这行代码的作用是,当你调用 proto.set()
办法时,其实是转发给了 proto[target]
,理论调用的是proto[target].set()
。所以就是proto
代理了对 target
的拜访。
那用在咱们 context.js
外面是啥意思呢?比方这行代码:
delegate(proto, "response")
.method("set");
这行代码的作用是,当你调用 proto.set()
时,理论去调用 proto.response.set()
,将proto
换成 ctx
就是:当你调用 ctx.set()
时,理论调用的是 ctx.response.set()
。这么做的目标其实也是为了使用方便,能够少写一个response
。而且ctx
不仅仅代理 response
,还代理了request
,所以你还能够通过ctx.accepts()
这样来调用到 ctx.request.accepts()
。一个ctx
就囊括了 response
和request
,所以这里的 context
也是一个语法糖。因为咱们后面曾经踢了 response
和request
这两个语法糖,context
作为包装了这两个语法糖的语法糖,咱们也一起踢掉吧。在 Application
的构造函数外面间接将 this.context
赋值为空对象:
// application.js
constructor() {
// 省略其余代码
this.context = {};}
当初语法糖都踢掉了,整个 Koa
的构造就更清晰了,ctx
下面也只有几个必须的变量:
ctx = {
app,
req,
res
}
context.js
对应的源码看这里:https://github.com/koajs/koa/blob/master/lib/context.js
app.handleRequest
当初咱们 ctx
和fn
都结构好了,那咱们解决申请其实就是调用 fn
,ctx
是作为参数传给他的,所以 app.handleRequest
代码就能够写进去了:
// 解决具体申请
handleRequest(ctx, fnMiddleware) {const handleResponse = () => respond(ctx);
// 调用中间件解决
// 所有解决完后就调用 handleResponse 返回申请
return fnMiddleware(ctx)
.then(handleResponse)
.catch((err) => {console.log("Somethis is wrong:", err);
});
}
咱们看到 compose
库返回的 fn
尽管反对第二个参数用来收尾,然而 Koa
并没有用他,如果不传的话,所有中间件执行完返回的就是一个空的 promise
,所以能够用then
接着他前面解决。前面要进行的解决就只有一个了,就是将处理结果返回给请求者的,这也就是 respond
须要做的。
app.handleRequest
对应的源码看这里:https://github.com/koajs/koa/blob/master/lib/application.js#L162
respond
respond
是一个辅助办法,并不在 Application
类外面,他要做的就是将网络申请返回:
function respond(ctx) {
const res = ctx.res; // 取出 res 对象
const body = ctx.body; // 取出 body
return res.end(body); // 用 res 返回 body
}
功败垂成
当初咱们能够用本人写的 Koa
替换官网的 Koa
来运行咱们结尾的例子了,不过 logger
这个中间件运行的时候会有点问题,因为他上面这行代码用到了语法糖:
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
这里的 ctx.method
和ctx.url
在咱们构建的 ctx
上并不存在,不过没关系,他不就是个 req
的语法糖嘛,咱们从 ctx.req
上拿就行,所以下面这行代码改为:
console.log(`${ctx.req.method} ${ctx.req.url} - ${ms}ms`);
总结
通过一层一层的抽丝剥茧,咱们胜利拎出了 Koa
的代码骨架,本人写了一个迷你版的Koa
。
这个迷你版代码曾经上传 GitHub,大家能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore
最初咱们再来总结下本文的要点吧:
Koa
是Express
原班人马写的一个新框架。Koa
应用了 JS 的新 API,比方async
和await
。Koa
的架构和Express
有很大区别。Express
的思路是大而全,内置了很多性能,比方路由,动态资源等,而且Express
的中间件也是应用路由同样的机制实现的,整个代码更简单。Express
源码能够看我之前这篇文章:手写 Express.js 源码Koa
的思路看起来更清晰,Koa
自身的库只是一个内核,只有中间件性能,来的申请会顺次通过每一个中间件,而后再进去返回给请求者,这就是大家常常据说的“洋葱模型”。- 想要
Koa
反对其余性能,必须手动增加中间件。作为一个web 服务器
,路由能够算是基本功能了,所以下一遍文章咱们会来看看Koa
官网的路由库@koa/router
,敬请关注。
参考资料
Koa 官网文档:https://github.com/koajs/koa
Koa 源码地址:https://github.com/koajs/koa/tree/master/lib
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。
作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges
我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢送关注~