用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.jsconst 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.jsconst 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.jsconstructor() { // 省略其余代码 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
我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢送关注~