Koa做为下一代Web开发框架,不只让咱们体验到了async/await语法带来同步方式书写异步代码的酸爽,并且自己简洁的特色,更加利于开发者结合业务自己进行扩展。node
本文从如下几个方面解读Koa源码:git
利用NodeJS能够很容易编写一个简单的应用程序:github
const http = require('http')
const server = http.createServer((req, res) => {
// 每一次请求处理的方法
console.log(req.url)
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello NodeJS')
})
server.listen(8080)
复制代码
注意:当浏览器发送请求时,会附带请求/favicon.ico。express
而Koa在封装建立应用程序的方法中主要执行了如下流程:json
// application.js
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
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;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 默认状态码为404
res.statusCode = 404;
// 中间件执行完毕以后 采用默认的 错误 与 成功 的处理方式
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码
首先咱们要知道NodeJS中的res和req是http.IncomingMessage和http.ServerResponse的实例,那么就能够在NodeJS中这样扩展req和res:api
Object.defineProperties(http.IncomingMessage.prototype, {
query: {
get () {
return querystring.parse(url.parse(this.url).query)
}
}
})
Object.defineProperties(http.ServerResponse.prototype, {
json: {
value: function (obj) {
if (typeof obj === 'object') {
obj = JSON.stringify(obj)
}
this.end(obj)
}
}
})
复制代码
而Koa中则是自定义request和response对象,而后保持对res和req的引用,最后经过getter和setter方法实现扩展。浏览器
// application.js
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; // 保存原生req对象
context.res = request.res = response.res = res; // 保存原生res对象
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// 最终返回完整的context上下文对象
return context;
}
复制代码
因此在Koa中要区别这两组对象:闭包
// request.js
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
复制代码
此时已经能够采用这样的方式访问header属性:app
ctx.request.header
复制代码
可是为了方便开发者调用这些属性和方法,Koa将response和request中的属性和方法代理到context上。框架
经过Object.defineProperty能够轻松的实现属性的代理:
function access (proto, target, name) {
Object.defineProperty(proto, name, {
get () {
return target[name]
},
set (value) {
target[name] = value
}
})
}
access(context, request, 'header')
复制代码
而对于方法的代理,则须要注意this的指向:
function method (proto, target, name) {
proto[name] = function () {
return target[name].apply(target, arguments)
}
}
复制代码
上述就是属性代理和方法代理的核心代码,这基本算是一个经常使用的套路。
代理这部分详细的源码,能够查看node-delegates, 不过这个包时间久远,有一些老方法已经废除。
在上述过程的源码中涉及到不少JavaScript的基础知识,例如:原型继承、this的指向。对于基础薄弱的同窗,还须要先弄懂这些基础知识。
首先须要明确是:中间件并非NodeJS中的概念,它只是connect、express和koa框架衍生的概念。
在connect中,开发者能够经过use方法注册中间件:
function use(route, fn) {
var handle = fn;
var path = route;
// 不传入route则默认为'/',这种基本是框架处理参数的一种套路
if (typeof route !== 'string') {
handle = route;
path = '/';
}
...
// 存储中间件
this.stack.push({ route: path, handle: handle });
// 以便链式调用
return this;
}
复制代码
use方法内部获取到中间件的路由信息(默认为'/')和中间件的处理函数以后,构建成layer对象,而后将其存储在一个队列当中,也就是上述代码中的stack。
connect中间件的执行流程主要由handle与call函数决定:
function handle(req, res, out) {
var index = 0;
var stack = this.stack;
...
function next(err) {
...
// 依次取出中间件
var layer = stack[index++]
// 终止条件
if (!layer) {
defer(done, err);
return;
}
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// 路由匹配规则
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
...
call(layer.handle, route, err, req, res, next);
}
next();
}
复制代码
handle函数中使用闭包函数next来检测layer是否与当前路由相匹配,匹配则执行该layer上的中间件函数,不然继续检查下一个layer。
这里须要注意next中检查路由的方式可能与想象中的不太同样,因此默认路由为'/'的中间件会在每一次请求处理中都执行。
function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
try {
if (hasError && arity === 4) {
// 错误处理中间件
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// 请求处理中间件
handle(req, res, next);
return;
}
} catch (e) {
// 记录错误
error = e;
}
// 将错误传递下去
next(error);
}
复制代码
在经过call方法执行中间件方法的时候,采用try/catch捕获错误,这里有一个特别须要注意的地方是,call内部会根据是否存在错误以及中间件函数的参数决定是否执行错误处理中间件。而且一旦捕获到错误,next方法会将错误传递下去,因此接下来普通的请求处理中间件即便经过了next中的路由匹配,仍然会被call方法给过滤掉。
下面是layer的处理流程图:
上述就是connect中间件设计的核心要点,总结起来有以下几点:
Koa中间件与connect中间件的设计有很大的差别:
固然,Koa中也是采用use方法注册中间件,相比较connect省去路由匹配的处理,就显得很简洁:
use(fn) {
this.middleware.push(fn);
return this;
}
复制代码
而且use支持链式调用。
Koa中间件的执行流程主要经过koa-compose中的compose函数完成:
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) {
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)
}
}
}
}
复制代码
看到这里本质上connect与koa实现中间件的思想都是递归,不难看出koa相比较connect实现得更加简洁,主要缘由在于:
上述就是connect中间件与Koa中间件的实现原理,如今在再看Koa中间件的这张执行流程图,应该没有什么疑问了吧?!
对于同步代码,经过try/catch能够轻松的捕获异常,在connect中间件的异常捕获则是经过try/catch完成。
对于异步代码,try/catch则没法捕获,这时候通常能够构造Promise链,在最后的catch方法中捕获错误,Koa就是这样处理,而且在catch方法中发送error事件,以便开发者自定义异常处理逻辑。
this.app.emit('error', err, this);
复制代码
前面也谈到Koa利用async/await语法带来同步方式书写异步代码的酸爽,另外也让错误处理更加天然:
// 也能够这样自定义错误处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500
ctx.body = err
}
})
复制代码
相信看到这里,再回忆一下以前遇到的那些问题,你应该会有新的理解,而且再次使用Koa时会更加驾轻就熟,这也是分析Koa源码的目的之一。