内容简介:这两天看了参考官网给的示范:跑起来后在浏览器输入
这两天看了 koa 的源码,惊叹于它的简练,仅仅聚焦最核心的功能,其他全部以中间件的形式扩展出去,给了开发者最大的个性化定制。这篇文章用于记录源码的学习笔记,方便日后借鉴思想时能快速回忆起来。
基础用法
参考官网给的示范:
const Koa = require('koa');
const app = new Koa();
// 定义一个middleware
app.use(async ctx => {
ctx.body = 'Hello World';
});
// 启动server并监听3000端口
app.listen(3000);
跑起来后在浏览器输入 localhost:3000
就能看到返回 Hello World
了。
入口
首先从构造函数开始:
class Application extends Emitter {
constructor() {
super();
this.proxy = false;
this.middleware = []; // 中间件列表
this.subdomainOffset = 2; // 从hostname解析子域的偏移起点,如 www.test.t1.t2 在偏移为2时的子域为www.test
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context); // 上下文对象,贯穿所有中间件
this.request = Object.create(request); // 包装的请求对象
this.response = Object.create(response); // 包装的响应对象
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
}
以上就是 new Koa()
会执行的所有逻辑了,仅仅是一些变量的初始化,关于 context
、 request
、 response
这三个对象在后面会说到。
listen
注意 new Koa
并没有启动 Server
,那么显然只能在 listen
中启动了。
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
看看 http.createServer
的函数签名 http.createServer([options][, requestListener])
就大致能猜到 this.callback()
返回的是 options
或 requestListener
。
// Return a request handler callback for node's native http server.
callback() {
// koa-compose 组合middleware的运行方式。意味着在listen之后的app.use不会起作用
const fn = compose(this.middleware);
// listenerCount和on均是父类Emitter中的成员
if (!this.listenerCount('error')) this.on('error', this.onerror); // 监听应用级error
const handleRequest = (req, res) => {
// 每个请求过来时,都创建一个context
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
所以我们最终传给 http.createServer
的是
(req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
也就是
每次都先创建一个 context
对象
,然后调用 this.handleRequest
。
同时我们遇到了 koa
的最核心的库: koa-compose
。 它用于精心组合所有 middleware
,并按照期望的顺序调用。我们后面会用专门的章节来描述它。
先看看 createContext
和 handleRequest
的实现。
createContext
用于创建一个 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));
context.app = request.app = response.app = this;
// 注意request.req和response.request的差别;
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;
}
很简单,就是创建各种对象,然后各种赋值绕的很。注意 request.req
、 response.req
指向的是 http
模块原生的 IncomingMessage
对象,而 request.response
、 response.request
指向的都是 koa
封装后的对象。
handleRequest
这个函数用于真正的进行业务逻辑处理了。
// fnMiddleware: 经koa-compose包装后的函数
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx); // 调用res.end返回最终结果
onFinished(res, onerror);
return fnMiddleware(ctx) // 调用各个middleware
.then(handleResponse)
.catch(onerror);
}
fnMiddleware
是经 koa-compose
包装后的函数,函数签名是 (context, next) => Promise
, 内部会依次调用每个中间件,不管是同步还是异步的中间件。在处理完所有中间件逻辑后, Promise
会 resolve
或 reject
。
onFinished
是 一个帮助库
,用于在请求 close
、 finish
、 error
时执行传入的回调。
respond
函数用于将中间件处理后的结果通过 res.end
返回客户端:
// Response helper.
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return; // ctx.respond = false用于设置自定义的Response策略
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();
}
// HEAD请求不返回body
if ('HEAD' == ctx.method) {
// headersSent表示是否发送过header
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
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 ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res); // 流式响应使用pipe,更好的利用缓存
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
middleware 处理流程
终于到 koa
最核心的逻辑了,可以想象 middleware
是 koa
得以流行的关键所在,各式各样的中间件使得框架异常灵活,非常方便定制。 从整体上看, middleware
的处理类似于 DOM
事件处理,先从前往后,再从后往前。
上面也说到所有中间件会传给 koa-compose
,并返回一个签名为 (context, next) => Promise
的函数。我们来仔细分析一下:
/**
* Compose `middleware` returning a fully valid middleware comprised of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
*/
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}
*/
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 {
// 执行下一个中间件逻辑,并将next参数设置为dispatch(i+1)
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
在 koa
框架中,当我们执行 fnMiddleware(ctx)
时,就会开始执行 dispatch(0)
,然后开始不断递归。这里需要仔细琢磨的是这两句:
if (i === middleware.length) fn = next; if (!fn) return Promise.resolve();
当 i === middleware.length
成立时,实际上所有传入的 middleware
已经执行完,那么 fn = next
意味着什么呢?
其实我们调用 fnMiddleware
可以传入两个参数的,第二个可选参数表示最终的回调函数。例如:
fnMiddleware(ctx, () => {
console.log('所有中间件全部执行完了,此时', ctx);
});
这个时候我们的 fn = next
表示 fn
被赋值给了这个传入的最终回调。接下来判断如果没有传入最终回调,那么整个中间件执行流程就到此结束。
另外,细细体会每个回调的执行顺序,
可以发现 middleware
的处理类似于 DOM
事件处理,先从前往后,再从后往前,并且 middleware
可以是异步函数,
因为 middleware
的执行被包裹在了 Promise.resolve
中。例如:
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
请求 localhost:3000
会打印出形如:
GET / - 4ms
在当前中间件中调用 next
时,会将控制权交给下一个中间件,当下一个中间件执行完毕时,才会执行当前中间件的 next
之后逻辑。
context、request、reponse
request、reponse
都是对原生 res、req
的封装, context
本质上也是一个普通的对象,他们的代码都不是很难,在这里就不一一赘述了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
编程珠玑(续)(修订版)
【美】Jon Bentley 乔恩•本特利 / 钱丽艳、刘田 / 人民邮电出版社 / 2015-2 / CNY 35.00
历史上最伟大的计算机科学著作之一 融深邃思想、实战技术与趣味轶事于一炉的奇书 带你真正领略计算机科学之美 多年以来,当程序员们推选出最心爱的计算机图书时,《编程珠玑》总是位于前列。正如自然界里珍珠出自细沙对牡蛎的磨砺,计算机科学大师Jon Bentley以其独有的洞察力和创造力,从磨砺程序员的实际问题中凝结出一篇篇不朽的编程“珠玑”,成为世界计算机界名刊《ACM通讯》历史上最受欢......一起来看看 《编程珠玑(续)(修订版)》 这本书的介绍吧!