内容简介:这两天看了参考官网给的示范:跑起来后在浏览器输入
这两天看了 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源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Book of CSS3
Peter Gasston / No Starch Press / 2011-5-13 / USD 34.95
CSS3 is the technology behind most of the eye-catching visuals on the Web today, but the official documentation can be dry and hard to follow. Luckily, The Book of CSS3 distills the heady technical la......一起来看看 《The Book of CSS3》 这本书的介绍吧!