了解Koa
Koa
Koa是一款基于中间件理念的Nodejs框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。
中间件
中间件是koa中非常重要的概念,koa和express都根据这个概念设计而成。
用koa官方的一张图片很生动地展示中间件的概念:
Koa与Express的区别
koa和express都是基于中间件概念设计的,但是两者的风格却截然不同。koa采用更现代的ES6语法 async function
来实现中间件,而express而使用nodejs传统的回调函数的方式来实现中间件 ,因此两者在设计思想上有一些差异。
此外express原生集成了一些常用的中间件,例如路由处理、静态资源服务器等,而koa只是实现了一个中间件的框架,并不内置任何中间件,但是可以通过社区来集成这些中间件。
复制 | Feature | Koa | Express | Connect |
| ------------------: | ----- | --------- | --------- |
| Middleware Kernel | ✓ | ✓ | ✓ |
| Routing | | ✓ | |
| Templating | | ✓ | |
| Sending Files | | ✓ | |
| JSONP | | ✓ | |
Koa源码
概述
Koa的源码相对简单,非常简洁,总体而言由四个部分组成:
Request
和Response
很容易理解,它们分别是针对HTTP request
和response
对象的封装,而context
是koa上下文对象的原型(即app.ctx的原型),Application
是一个继承Emitter
的类。
Context
context
文件中定义了app.ctx
的原型。
context对象原生定义了一些实用的工具方法,比较常用的有:
assert: 添加断言,如果断言失败则返回服务器异常的HTTP响应,可以指定错误码和消息。 示例ctx.assert(1 === 2, 500, 'error')
throw: 让服务器返回异常响应(默认status=500),可以指定status和msg。 示例:ctx.throw(403, 'error')
onerror: 错误处理函数,当中间件函数发生错误时执行
ctx.assert和ctx.throw
context.assert()
和context.throw()
调用后可以返回指定HTTP响应,这是因为当在中间件函数内部调用 context.throw()
或者 context.assert()
断言为 false
时,内部会抛出了一个特殊的异常,而这一异常会被 context.onerror
捕获,处理后返回异常响应 :
复制 app .use ( async (ctx , next) => {
try {
// ctx.throw(403, '111')
ctx .assert ( 1 === 2 , 404 , "error msg" );
} catch (err) {
console .log (err);
}
});
// NotFoundError: error msg
当中间件函数执行时,ctx.onerror
会捕获异常:
复制 // 假设fnMiddleware函数内部会依次调用中间件,并返回一个promise
// 那么当中间件内部抛出异常时,除非用户手动捕获异常,否则ctx.onerror将会触发执行。
fnMiddleware (ctx) .catch ( ctx .onerror)
ctx.conerror
ctx.onerror当中间件函数出现异常且没有被手动捕获异常时执行,以便HTTP服务器能返回正确的响应来提示客户端服务器异常。
onerror
函数需要处理nodejs原生错误对象以及ctx.throw等api抛出的特殊错误对象。
复制 onerror (err) {
// 如果没有err,就直接退出。
if ( null == err) return ;
// 是否继承于Error,即判断是否是一个错误
// 某些情况下err instanceof Error无法识别node内部的错误
const isNativeError =
Object . prototype . toString .call (err) === '[object Error]' ||
err instanceof Error ;
if ( ! isNativeError) err = new Error ( util .format ( 'non-error thrown: %j' , err));
// ...
let statusCode = err .status || err .statusCode;
// ENOENT support
if ( 'ENOENT' === err .code) statusCode = 404 ;
// 默认500 服务器错误
if ( 'number' !== typeof statusCode || ! statuses[statusCode]) statusCode = 500 ;
// 返回异常响应,如果err有指定状态码和消息则直接使用,否则默认状态码为500
const code = statuses[statusCode];
const msg = err .expose ? err .message : code;
this .status = err .status = statusCode;
this . length = Buffer .byteLength (msg);
res .end (msg);
} ,
方法别名
koa为了方便,context提供了一些方法别名,例如:ctx.header
就是ctx.request.header
的映射,ctx.header
和 ctx.request.header
指向的是同一个对象。
koa是通过delegates
这个第三方库来实现别名的
复制 /**
* Response delegation.
*/
delegate (proto , 'response' )
.method ( 'attachment' )
.method ( 'redirect' )
.method ( 'remove' )
.method ( 'vary' )
.method ( 'has' )
.method ( 'set' )
.method ( 'append' )
.method ( 'flushHeaders' )
.access ( 'status' )
.access ( 'message' )
.access ( 'body' )
.access ( 'length' )
.access ( 'type' )
.access ( 'lastModified' )
.access ( 'etag' )
.getter ( 'headerSent' )
.getter ( 'writable' );
/**
* Request delegation.
*/
// ... Request也类似
通过Object.getOwnPropertyDescriptor
其实可以发现,这只不过是利用对象描述符 来实现的映射而已:
复制 // access
Object .getOwnPropertyDescriptor (proto , 'body' )
/*
{
configurable:true
enumerable:true
get:ƒ (){\n return this[target][name];\n }
set:ƒ (val){\n return this[target][name] = val;\n }
}
*/
// getter
Object .getOwnPropertyDescriptor (proto , 'writable' )
/*
{
configurable:true
enumerable:true
get:ƒ (){\n return this[target][name];\n }
set:ƒ undefined
}
*/
// method
Object .getOwnPropertyDescriptor (proto , 'set' )
/*
{
configurable: true
enumerable: true
value: ƒ (){\n return this[target][name].apply(this[target], arguments);\n }
writable: true
}
*/
从开始到现在最关键的两个属性ctx.response和ctx.request还没有被定义,这是因为这两个属性其实是在Application.prototype.listen()
执行的时候才被挂载到ctx中的。
Request和Response
request对象和response对象
Request和Response文件分别会导出的是一个对象,而这两个对象分别是 ctx.request
和 ctx.response
对象的原型对象,并且这两个对象会被挂载到context对象上,用户通过ctx.request和ctx.response访问。
复制 const response = require ( './response' )
const compose = require ( 'koa-compose' )
const context = require ( './context' )
const request = require ( './request' )
module . exports = class Application extends Emitter {
// ... 省略了无关代码
constructor (options) {
// ...
this .middleware = []
// 基于request对象和response为原型创建对象
this .context = Object .create (context)
// 基于request对象和response为原型创建对象
this .request = Object .create (request)
this .response = Object .create (response)
this .request = Object .create (request)
this .response = Object .create (response)
}
// 创建context
createContext (req , res) {
// ...
const context = Object .create ( this .context)
const request = context .request = Object .create ( this .request)
const response = context .response = Object .create ( this .response)
}
}
Application
Application上挂载了以下原型方法,每个原型方法各司其职,其中最重要的几个方法有:
linsten: http.createServer(this.callback()).linsten()
的语法糖
use: 将中间件函数添加到middleware
队列中
callback:返回一个供http.createServer
使用的回调函数。
respond: 处理HTTP响应,内部最终会调用res.end(body)
来返回响应。
初始化
Application类实例化时会初始化一些成员变量,其中值得关注的有:
复制 const response = require ( './response' )
const compose = require ( 'koa-compose' )
const context = require ( './context' )
const request = require ( './request' )
module . exports = class Application extends Emitter {
// ... 省略了无关代码
constructor (options) {
// ...
this .middleware = []
// 基于request对象和response为原型创建对象
this .context = Object .create (context)
// 基于request对象和response为原型创建对象
this .request = Object .create (request)
this .response = Object .create (response)
this .request = Object .create (request)
this .response = Object .create (response)
}
}
use
app.use函数其实就是将中间件函数添加到一个队列中。
复制 use (fn) {
if ( typeof fn !== 'function' ) {
throw new TypeError ( 'middleware must be a function!' )
}
debug ( 'use %s' , fn ._name || fn .name || '-' )
this . middleware .push (fn)
return this
}l
listen
app.listen()
其实是http.createServer(this.callback()).linsten()
的语法糖
复制 listen ( ... args) {
debug ( 'listen' )
const server = http .createServer ( this .callback ())
return server .listen ( ... args)
}
这里可以看到其实koa本质上也是借助nodejs原生HTTP模块实现的服务器,只是在上层进行了处理,以便用户可以更方便地使用。
createContext
app.createContext内部初始化context
并返回,也是在这时初始化的ctx.request
和ctx.response
复制 // 创建context
createContext (req , res) {
// ...
const context = Object .create ( this .context)
const request = context .request = Object .create ( this .request)
const response = context .response = Object .create ( this .response)
}
callback
app.callback返回一个供http.createServer()
消费的回调函数。在内部其实做了两件事:
利用koa-compose库将middleware中间件队列转变为一个能够依次调用中间件函数的函数。
当用户请求HTTP时就会触发handleRequest函数的执行,而在handleRequest函数内部又会执行app.handleRequest()
。
复制 callback () {
const fn = this .compose ( this .middleware)
if ( ! this .listenerCount ( 'error' )) this .on ( 'error' , this .onerror)
const handleRequest = (req , res) => {
const ctx = this .createContext (req , res)
if ( ! this .ctxStorage) {
return this .handleRequest (ctx , fn)
}
return this . ctxStorage .run (ctx , async () => {
return await this .handleRequest (ctx , fn)
})
}
return handleRequest
}
compose
默认情况下,app.compose是调用koa-compose库的compose函数来将middleware队列转变为执行函数的。
假设有这三个中间件函数
复制 app .use (async fn1 (ctx , next){
await next ()
})
app .use (async fn2 (ctx , next){
await next ()
})
app .use (async fn3 (ctx , next){
ctx.body = 1
})
那么app.middleware
中应当是: [fn1, fn2, fn3]
我们最终需要转变为一个能让这几个中间件函数按顺序依次执行,且返回后能够执行上一个中间件函数next()
后面代码的函数。
很明显这不能用遍历的方式,如果遍历middleware来执行的话虽然能达到按顺序执行的效果,但是没办法做到中间件函数执行完毕后返回到上一个函数继续执行next()
后面代码的要求。
要实现这个目的,我们要用递归来实现,并且需要将下一个中间件函数作为参数next传入给当前执行的中间件函数。
如果能够达到这种形式: fn1(ctx, fn2.bind(null, ctx, fn3.bind(null)))
,那么js就会依次进入每个中间件函数执行,但是我们不能直接这些写,可以封装成一个函数,在函数中判断需要执行哪个中间件函数,并在内部判断递归结束条件。
我们封装一个dispatch函数,并接受一个参数i表示要执行第几个中间件函数。dispatch会递归执行,dispatch(0)内部会执行middleware[0],并且将ctx和dispatch(1)作为参数传入,当第一个中间件函数执行next()函数时就会调用dispatch(1),依次类推,直到执行完所有的中间件函数。
另外我们需要设置好结束条件,那就是i≥middleware.length
时,此时不应该再继续递归。
完整代码如下:
复制 function compose (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!' )
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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 , dispatch .bind ( null , i + 1 )));
} catch (err) {
return Promise .reject (err)
}
}
}
}
handleRequest
app.handleRequest()
函数会调用之前compose生成的*fnMiddleware
,这会依次执行中间件函数,在中间件函数全部执行完毕后,通过app.respond和ctx.onerror来处理fulfilled和reject两种场景,即 fnMiddleware
* 函数执行成功和触发异常这两种情况。
fnMiddleware(*ctx*).then(handleResponse).catch(onerror)
代码:
复制 handleRequest (ctx , fnMiddleware) {
const res = ctx .res
res .statusCode = 404
const onerror = (err) => ctx .onerror (err)
const handleResponse = () => respond (ctx)
onFinished (res , onerror)
return fnMiddleware (ctx) .then (handleResponse) .catch (onerror)
}
respond
app.response是一次HTTP请求处理的最后的处理函数,它最终会通过res.end(body)
来返回HTTP响应。
完整的执行顺序
当调用app.use(fn)
其实就是将这个中间件函数fn
添加到middleware
这个队列中。
当调用app.linsten(3000)
其实就是调用了http.createServer(this.callback()).linsten(3000)
,因此在这个过程中又会调用app.callback()
来创建一个供HttpServer消费的回调函数
app.callback()
函数内部依次执行
利用koa-compose库将middleware
中间件队列转变为一个依次执行所有中间件的fn
函数。
返回一个名为handleRequest
的函数作为http.createServer
的回调函数。
当用户访问服务器时,HttpServer执行handleRequest
函数来处理请求和响应。
handleRequest
函数将原生req
和res
对象传递给app.createContext
来创建conext
对象。
将context上下文对象和fn函数传递给app.handleRequest
函数执行。
fnMiddleware(*ctx*).then(() => respond(*ctx*)).catch((*err*) => *ctx*.onerror(*err*))
fulfilled: 执行app.respond()
函数,处理响应对象,最终会调用node原生语法res.end(body)
返回响应。
reject: 执行ctx.onerror
函数处理异常。