携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情
koa 源码解析
由 package.json 文件的 main 字段可以得出,入口文件为 lib/application.js。
class Application extends Emitter {}
Application 类继承自 Emitter 类,class Emitter 是 node 内置模块。
class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
// 保存通过app.use(middleware)注册的中间件
this.middleware = [];
// Object.create => 都是对象
// Context对象里就封装了上下文的所有变量和对象。
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
/**
* 调用原生 http 模块开启服务并监听端口
*/
listen(...args) {
// this.callback()返回的就是回调函数,所以callback是一个高阶函数。
const server = http.createServer(this.callback());
return server.listen(...args);
}
/**
* 挂载中间件
* use方法很简单,接受一个函数作为参数,并加入middleware数组。由于koa最开始支持使用generator函数作为中间件使用,但将在3.x的版本中放弃这项支持,因此koa2中对于使用generator函数作为中间件的行为给与未来将被废弃的警告,但会将generator函数转化为async函数。返回this便于链式调用。
* @param {Function} fn
* @return {Application} self
* @api public
* 只是把传入的回调函数先存到middleware数组里。
*/
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
// 用app.use的时候,实际上就是将函数push进middleware数组中,等待之后的调用。
this.middleware.push(fn);
return this;
}
/**
* 执行返回 http 请求的回调函数
*
* 把handleRequest返回出去,供http.createServer回调,注意这里形成了一个闭包,能获得fn
* @returns fn
*/
callback() {
// compose 函数将中间件数组转换成执行链函数 fn
// 创建洋葱模型的入口函数
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 函数将中间件数组转换成执行链函数 fn
(源码部分在 node_modules/koa-compose)
/**
* 接收一个middleware数组为参数,返回一个函数
*/
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} 返回Promise
*/
return function (context, next) {
// last called middleware #
let index = -1
// 返回第一个 use 的中间件函数
return dispatch(0)
// 每个 dispatch 的返回值也是一个 Promise,因此我们的中间件中可以方便地使用async函数进行定义,内部使用await next()调用“下游”,然后控制流回“上游”,这是更准确也更友好的中间件模型
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 获取当前传入下标的中间件函数
let fn = middleware[i]
// 防止最后一个中间件执行next进行无限循坏
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 中间件的执行是异步的,并且中间件执行完毕后返回的是一个Promise
// 洋葱模型的触发方式,先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
类似于这样的结构
const [fn1, fn2, fn3] = this.middleware;
// 先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
const fnMiddleware = function (context) {
return Promise.resolve(
fn1(context, function next() {
return Promise.resolve(
fn2(context, function next() {
return Promise.resolve(
fn3(context, function next() {
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
koa-convert 源码(转换 generator 函数)
koa1中主要是generator函数。koa2中会自动转换generator函数。
app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。
(源码部分在 node_modules/koa-convert)
function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
if (mw.constructor.name !== 'GeneratorFunction') {
// assume it's Promise-based middleware
return mw
}
const converted = function (ctx, next) {
// 转换重新赋值,再存入middleware,后续再使用
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}
function * createGenerator (next) {
return yield next()
}
co 源码
首先要明白 generator 函数是不会自动执行的,需要一步步调用 next()。 co 就是让 generator向 async、await 函数一样自动执行。
(源码部分在 node_modules/co)
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
// 反复执行调用自己
function next(ret) {
// 检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
// 确保返回值是promise对象
var value = toPromise.call(ctx, ret.value);
// 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
小结👉👉👉:
koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise。
koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。
co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。
以上是整个 koa 洋葱模型的核心概念。
application.js 文件中还定义了一些用于请求和响应的函数
handleRequest --- 请求真正的回调函数
/**
* 请求真正的回调函数
* 核心在于执行 fnMiddleware,这个其实就是 const fn = compose(this.middleware)
* compose会串联顺序执行中间件 next 之前的代码,完成后倒序执行各中间件next之后的代码
*/
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 一开始就将res的statusCode定义为404
// 如果在我们没有设置body的情况下,默认就会返回404。
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
respond --- 请求在经过所有中间件全部处理后的自动响应函数
function respond(ctx) {
// 当 ctx 的 respond 为false 直接返回
if (false === ctx.respond) return;
// 当请求是scoket将根据socket的writable,否则都未true
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的一些处理
if ('HEAD' === ctx.method) {
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 (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
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
// 处理 Buffer 类型返回
if (Buffer.isBuffer(body)) return res.end(body);
// 处理字符串类型返回
if ('string' === typeof body) return res.end(body);
// 处理 Stream 类型返回
if (body instanceof Stream) return body.pipe(res);
// body: json 对象处理,转为JSON字符串返回
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
createContext --- 根据 req,res 创建上下文对象
/**
* 根据 req,res 创建上下文对象
* @api private
* 每次回调过来都是创建新的context、request和response实例,这样本质上也对应了HTTP是无状态的。
*/
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;
// 注意context、request、response都有成员变量双向绑定在一起,从一个对象上能获取另外两个对象,后面可以看到很多context上的方法都被代理到了request和response上。
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
//公共的存储内容,后端模板也会把这里的属性作为视图的上下文用于渲染
context.state = {};
return context;
}
context.js
// COOKIES是一个Symbol类型的私有变量
const COOKIES = Symbol('context#cookies');
const proto = module.exports = {
/**
* util.inspect() implementation, which
* just returns the JSON output.
*
* @return {Object}
* @api public
*/
inspect() {
if (this === proto) return this;
return this.toJSON();
},
/**
* 获取当前ctx的内容
*/
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
};
},
/**
* http-assert,对http-errors的封装,一些基本的断言并设置http返回体
*/
assert: httpAssert,
throw(...args) {
throw createError(...args);
},
/**
*
* @param {Error} err
* @api private
*/
onerror(err) {
// 没有错误时什么也不做
if (null == err) return;
// err不是Error实例时,使用err创建一个Error实例
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
// headerSent变量表示响应头是否发送,如果响应头已经发送,或者响应处于不可写状态,将无法在响应中添加错误信息,直接退出该函数,否则需要将之前写入的响应头部信息清空。
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// 触发koa实例app的error事件
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
return;
}
const { res } = this;
// 移除所有设置过的响应头
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// 设置错误头部
this.set(err.headers);
// 设置错误时的Content-Type
this.type = 'text';
let statusCode = err.status || err.statusCode;
// 找不到文件错误码设为404
if ('ENOENT' === err.code) statusCode = 404;
// 不能被识别的错误将错误码设为500
if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
},
// 处理Cookie
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
});
}
return this[COOKIES];
},
set cookies(_cookies) {
this[COOKIES] = _cookies;
}
};
/**
* 本质上是一个构造函数,接受两个参数,第一个参数是代理对象,第二个参数是被代理的对象
*
* context使用了delegates这个库。主要是将context中的一些值和函数代理到request和response中,这样实际上我们调用ctx.hostname获取值的时候,实际上是调用了req.hostname。从而方便调用。
*/
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');
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
request.js 和 response.js 是对原生 req 和 res 的封装,同时也提供一些额外的值和函数。
整体流程🔎🔎:
初始化阶段: new初始化一个实例,use搜集中间件到middleware数组,listen 合成中间件fnMiddleware,返回一个callback函数给http.createServer,开启服务器,等待http请求。
请求阶段: 每次请求,createContext生成一个新的ctx,传给fnMiddleware,触发中间件的整个流程。
响应阶段: 整个中间件完成后,调用respond方法,对请求做最后的处理,返回响应给客户端。