源码
目录结构
Application
application.js
主要是对 App 做的一些操作,包括创建服务、在 ctx 对象上挂载 request、response 对象,以及处理异常等操作。接下来将对这些实现进行详细阐述。
Koa 创建服务的原理
- Node 原生创建服务
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
});
server.listen(4000, () => {
console.log("server start at 4000");
});
module.exports = class Application extends Emitter {
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
listen(...args) {
debug("listen");
const server = http.createServer(this.callback());
return server.listen(...args);
}
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
callback () {
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
/**
* Handle request in callback.
*
* @api private
*/
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)
}
};
中间件实现原理
中间件使用例子
const Koa = require("koa")
const app = new Koa()
app.use(async (ctx, next) => {
console.log('---1--->')
await next()
console.log('===6===>')
})
app.use(async (ctx, next) => {
console.log('---2--->')
await next()
console.log('===5===>')
})
app.use(async (ctx, next) => {
console.log('---3--->')
await next()
console.log('===4===>')
})
app.listen(4000, () => {
console.log('server is running, port is 4000')
})
注册中间件
Koa注册中间件是用app.use()
方法实现的
module.exports = class Application extends Emitter {
constructor (options) {
this.middleware = []
}
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
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
}
}
Application类的构造函数中声明了一个名为middleware的数组,当执行use()方法时,会一直往middleware中的push()方法传入函数。其实,这就是Koa注册中间件的原理,middleware就是一个队列,注册一个中间件,就进行入队操作。
koa-compose
中间件注册后,当请求进来的时候,开始执行中间件里面的逻辑,由于有next的分割,一个中间件会分为两部分执行。
midddleware队列是如何执行?
const compose = require('koa-compose')
module.exports = class Application extends Emitter {
callback () {
// 核心代码:处理队列中的中间件
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
}
探究下koa-compose
的核心源码实现:
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
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
*/
// 返回闭包,由此可知koa this.callback的函数后续一定会使用这个闭包传入过滤的上下文
return function (context, next) {
// last called middleware #
// 初始化中间件函数数组执行下标值
let index = -1
// 返回递归执行的Promise.resolve去执行整个中间件数组
// 从第一个开始
return dispatch(0)
function dispatch (i) {
// 检验上次执行的下标索引不能大于本次执行的下标索引i,如果大于,可能是下个中间件多次执行导致的
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 当前执行的中间件函数
let fn = middleware[i]
// 如果当前执行下标等于中间件数组长度,放回Promise.resolve()即可
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)
}
}
}
}
如何封装ctx
module.exports = class Application extends Emitter {
// 3个属性,Object.create分别继承
constructor (options) {
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
callback () {
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
// 创建context对象
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
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)
context.app = request.app = response.app = this
context.req = request.req = response.req = req
context.res = request.res = response.res = res
request.ctx = response.ctx = context
request.response = response
response.request = request
context.originalUrl = request.originalUrl = req.url
context.state = {}
return context
}
}
中间件中的ctx
对象经过createContext()
方法进行了封装,其实ctx
是通过Object.create()
方法继承了this.context
,而this.context
又继承了lib/context.js
中导出的对象。最终将http.IncomingMessage
类和http.ServerResponse
类都挂载到了context.req
和context.res
属性上,这样是为了方便用户从ctx
对象上获取需要的信息。
单一上下文原则: 是指创建一个context对象并共享给所有的全局中间件使用。也就是说,每个请求中的context对象都是唯一的,并且所有关于请求和响应的信息都放在context对象里面。
function respond (ctx) {
// allow bypassing koa
if (ctx.respond === false) return
if (!ctx.writable) return
const res = ctx.res
let body = ctx.body
const code = ctx.status
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null
return res.end()
}
if (ctx.method === 'HEAD') {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response
if (Number.isInteger(length)) ctx.length = length
}
return res.end()
}
// status body
if (body == null) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type')
ctx.response.remove('Transfer-Encoding')
ctx.length = 0
return res.end()
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code)
} else {
body = ctx.message || String(code)
}
if (!res.headersSent) {
ctx.type = 'text'
ctx.length = Buffer.byteLength(body)
}
return res.end(body)
}
// responses
if (Buffer.isBuffer(body)) return res.end(body)
if (typeof body === 'string') return res.end(body)
if (body instanceof Stream) return body.pipe(res)
// body: json
body = JSON.stringify(body)
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body)
}
res.end(body)
}
错误处理
onerror (err) {
// When dealing with cross-globals a normal `instanceof` check doesn't work properly.
// See https://github.com/koajs/koa/issues/1466
// We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error
if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err))
if (err.status === 404 || err.expose) return
if (this.silent) return
const msg = err.stack || err.toString()
console.error(`\n${msg.replace(/^/gm, ' ')}\n`)
}