Koa是一款很优秀的Node.js框架,它的内部实现逻辑也很清晰。通过阅读它的源代码,我们可以学到很多有用的前端开发技巧。接下来这篇文章,我将以Koa 2的源码为例,对其源码做一些总结。
基础知识
我们先来看一下,一个最基本用Koa来写的应用是怎样的:
const Koa = require("koa");
const app = new Koa();
app.use(async ctx => {
ctx.body = "Hello wolrd";
});
app.listen(3000);
可以看到,最核心的业务逻辑基本上都是写在app.use
这个方法的回调函数参数上的,这个回调函数我们称为中间件。利用多个中间件级联操作就能实现复杂的业务需求。
源码解析
Koa的源码结构十分清晰,核心模块只有4个,分别为application、context、request和response。其中application就相对于整个应用的入口,而context将用户的请求和响应进行封装,request和response模块分别封装了原生请求对象的request和response对象,并暴露出一些有用的接口。
application
module.exports = 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;
// 初始化中间件数组、上下文、请求响应对象。
this.middleware = [];
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;
}
// 原生web服务器对象的封装
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
// 核心处理逻辑,注册中间件
use(fn) {
//此处省略一些验证方法
...
// 将回调函数放入中间件数组中
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
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;
}
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);
}
// 利用req和res对象初始化上下文
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;
}
// 提供错误处理函数
onerror(err) {
if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
};
// 处理访问客户端的响应请求
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) 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 ('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();
}
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);
}
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
可以看到,application部分的代码,主要是通过构造request和response还有context对象来对外开放接口。
另外在处理请求时,所有中间件都会利用compose
方法进行封装成一个嵌套函数,然后调用这个嵌套函数进行执行:
compose(ctx, middlewares) {
function dispatch(index) {
if (index === middlewares.length) {
return Promise.resolve();
}
const middleware = middlewares[index];
return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))
}
return dispatch(0);
}
我们可以根据compose的原理,简单写出它的实现代码。一种实现方式就是利用递归调用,所有的中间件都会按顺序执行,然后返回一个Promise,最后调用方执行这个Promise就可以了。
context
koa和express框架最大的不同就是采用context来调用中间件,所以context对象主要是一个代理模式,通过劫持request和response上的对象让用户可以直接在ctx对象上进行操作。
const proto = module.exports = {
throw(...args) {
throw createError(...args);
},
// 默认的错误处理函数
onerror(err) {
if (null == err) return;
if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// 触发error对象
this.app.emit('error', err, this);
if (headerSent) {
return;
}
const { res } = this;
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// 设置headers
this.set(err.headers);
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// respond
const code = statuses[err.status];
const msg = err.expose ? err.message : code;
this.status = err.status;
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;
}
};
// 代理属性和方法
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');
这里值得一提的是,目前代理模式主要可以通过原型链上的__defineGetter__
和__defineSetter__
方法来实现。当属性发生变化的时候,自动触发对应的getter
方法执行。
request
request对象其实是在node原生请求对象上做了一层抽象,可以用来获取用户请求的一些参数信息。
module.exports = {
// 获取头部信息
get header() {
return this.req.headers;
},
// 设置头部信息
set header(val) {
this.req.headers = val;
},
get headers() {
return this.req.headers;
},
set headers(val) {
this.req.headers = val;
},
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
},
get origin() {
return `${this.protocol}://${this.host}`;
},
get href() {
// support: `GET http://example.com/foo`
if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
return this.origin + this.originalUrl;
},
get method() {
return this.req.method;
},
set method(val) {
this.req.method = val;
},
get path() {
return parse(this.req).pathname;
},
set path(path) {
const url = parse(this.req);
if (url.pathname === path) return;
url.pathname = path;
url.path = null;
this.url = stringify(url);
},
get query() {
const str = this.querystring;
const c = this._querycache = this._querycache || {};
return c[str] || (c[str] = qs.parse(str));
},
set query(obj) {
this.querystring = qs.stringify(obj);
},
get querystring() {
if (!this.req) return '';
return parse(this.req).query || '';
},
set querystring(str) {
const url = parse(this.req);
if (url.search === `?${str}`) return;
url.search = str;
url.path = null;
this.url = stringify(url);
},
get search() {
if (!this.querystring) return '';
return `?${this.querystring}`;
},
set search(str) {
this.querystring = str;
},
get host() {
const proxy = this.app.proxy;
let host = proxy && this.get('X-Forwarded-Host');
if (!host) {
if (this.req.httpVersionMajor >= 2) host = this.get(':authority');
if (!host) host = this.get('Host');
}
if (!host) return '';
return host.split(/\s*,\s*/, 1)[0];
},
get hostname() {
const host = this.host;
if (!host) return '';
if ('[' == host[0]) return this.URL.hostname || ''; // IPv6
return host.split(':', 1)[0];
},
get URL() {
/* istanbul ignore else */
if (!this.memoizedURL) {
const originalUrl = this.originalUrl || ''; // avoid undefined in template string
try {
this.memoizedURL = new URL(`${this.origin}${originalUrl}`);
} catch (err) {
this.memoizedURL = Object.create(null);
}
}
return this.memoizedURL;
},
get fresh() {
const method = this.method;
const s = this.ctx.status;
// GET or HEAD for weak freshness validation only
if ('GET' != method && 'HEAD' != method) return false;
// 2xx or 304 as per rfc2616 14.26
if ((s >= 200 && s < 300) || 304 == s) {
return fresh(this.header, this.response.header);
}
return false;
},
get stale() {
return !this.fresh;
},
get idempotent() {
const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
return !!~methods.indexOf(this.method);
},
get socket() {
return this.req.socket;
},
get charset() {
try {
const { parameters } = contentType.parse(this.req);
return parameters.charset || '';
} catch (e) {
return '';
}
},
get length() {
const len = this.get('Content-Length');
if (len == '') return;
return ~~len;
},
get protocol() {
if (this.socket.encrypted) return 'https';
if (!this.app.proxy) return 'http';
const proto = this.get('X-Forwarded-Proto');
return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http';
},
get secure() {
return 'https' == this.protocol;
},
get ips() {
const proxy = this.app.proxy;
const val = this.get(this.app.proxyIpHeader);
let ips = proxy && val
? val.split(/\s*,\s*/)
: [];
if (this.app.maxIpsCount > 0) {
ips = ips.slice(-this.app.maxIpsCount);
}
return ips;
},
get ip() {
if (!this[IP]) {
this[IP] = this.ips[0] || this.socket.remoteAddress || '';
}
return this[IP];
},
set ip(_ip) {
this[IP] = _ip;
},
// 获取子域信息
get subdomains() {
const offset = this.app.subdomainOffset;
const hostname = this.hostname;
if (net.isIP(hostname)) return [];
return hostname
.split('.')
.reverse()
.slice(offset);
},
get accept() {
return this._accept || (this._accept = accepts(this.req));
},
set accept(obj) {
this._accept = obj;
},
accepts(...args) {
return this.accept.types(...args);
},
acceptsEncodings(...args) {
return this.accept.encodings(...args);
},
acceptsCharsets(...args) {
return this.accept.charsets(...args);
},
acceptsLanguages(...args) {
return this.accept.languages(...args);
},
is(type, ...types) {
return typeis(this.req, type, ...types);
},
get type() {
const type = this.get('Content-Type');
if (!type) return '';
return type.split(';')[0];
},
get(field) {
const req = this.req;
switch (field = field.toLowerCase()) {
case 'referer':
case 'referrer':
return req.headers.referrer || req.headers.referer || '';
default:
return req.headers[field] || '';
}
},
inspect() {
if (!this.req) return;
return this.toJSON();
},
toJSON() {
return only(this, [
'method',
'url',
'header'
]);
}
};
response
reponse模块和request模块实现思路上基本相同:
module.exports = {
get socket() {
return this.res.socket;
},
//
get header() {
const { res } = this;
return typeof res.getHeaders === 'function'
? res.getHeaders()
: res._headers || {}; // Node < 7.7
},
get headers() {
return this.header;
},
get status() {
return this.res.statusCode;
},
// 设置响应码
set status(code) {
if (this.headerSent) return;
assert(Number.isInteger(code), 'status code must be a number');
assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
this._explicitStatus = true;
this.res.statusCode = code;
if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
if (this.body && statuses.empty[code]) this.body = null;
},
// 获取状态信息
get message() {
return this.res.statusMessage || statuses[this.status];
},
set message(msg) {
this.res.statusMessage = msg;
},
get body() {
return this._body;
},
// 设置响应体
set body(val) {
const original = this._body;
this._body = val;
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
this.remove('Content-Type');
this.remove('Content-Length');
this.remove('Transfer-Encoding');
return;
}
// set the status
if (!this._explicitStatus) this.status = 200;
// set the content-type only if not yet set
const setType = !this.has('Content-Type');
// string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
this.length = Buffer.byteLength(val);
return;
}
// buffer
if (Buffer.isBuffer(val)) {
if (setType) this.type = 'bin';
this.length = val.length;
return;
}
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, err => this.ctx.onerror(err));
// overwriting
if (null != original && original != val) this.remove('Content-Length');
if (setType) this.type = 'bin';
return;
}
// json
this.remove('Content-Length');
this.type = 'json';
},
// 设置content-length字段
set length(n) {
this.set('Content-Length', n);
},
get length() {
if (this.has('Content-Length')) {
return parseInt(this.get('Content-Length'), 10) || 0;
}
const { body } = this;
if (!body || body instanceof Stream) return undefined;
if ('string' === typeof body) return Buffer.byteLength(body);
if (Buffer.isBuffer(body)) return body.length;
return Buffer.byteLength(JSON.stringify(body));
},
// 判断header是否已经发送到socket中去
get headerSent() {
return this.res.headersSent;
},
// 验证字段有效性
vary(field) {
if (this.headerSent) return;
vary(this.res, field);
},
// 处理302,重定向
redirect(url, alt) {
// location
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
this.set('Location', encodeUrl(url));
// status
if (!statuses.redirect[this.status]) this.status = 302;
// html
if (this.ctx.accepts('html')) {
url = escape(url);
this.type = 'text/html; charset=utf-8';
this.body = `Redirecting to <a href="${url}">${url}</a>.`;
return;
}
// text
this.type = 'text/plain; charset=utf-8';
this.body = `Redirecting to ${url}.`;
},
// 文件传输过程中使用
attachment(filename, options) {
if (filename) this.type = extname(filename);
this.set('Content-Disposition', contentDisposition(filename, options));
},
// 设置content-type字段
set type(type) {
type = getType(type);
if (type) {
this.set('Content-Type', type);
} else {
this.remove('Content-Type');
}
},
// 设置头部lastModified字段
set lastModified(val) {
if ('string' == typeof val) val = new Date(val);
this.set('Last-Modified', val.toUTCString());
},
get lastModified() {
const date = this.get('last-modified');
if (date) return new Date(date);
},
// 设置etag字段
set etag(val) {
if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
this.set('ETag', val);
},
// 返回etag, 主要用于缓存控制
get etag() {
return this.get('ETag');
},
get type() {
const type = this.get('Content-Type');
if (!type) return '';
return type.split(';', 1)[0];
},
// 检测字段
is(type, ...types) {
return typeis(this.type, type, ...types);
},
// 返回特定字段
get(field) {
return this.header[field.toLowerCase()] || '';
},
// 判断是否存在特定的头部字段
has(field) {
return typeof this.res.hasHeader === 'function'
? this.res.hasHeader(field)
// Node < 7.7
: field.toLowerCase() in this.headers;
},
// 设置头部字段
set(field, val) {
if (this.headerSent) return;
if (2 == arguments.length) {
if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
else if (typeof val !== 'string') val = String(val);
this.res.setHeader(field, val);
} else {
for (const key in field) {
this.set(key, field[key]);
}
}
},
// 添加头部字段
append(field, val) {
const prev = this.get(field);
if (prev) {
val = Array.isArray(prev)
? prev.concat(val)
: [prev].concat(val);
}
return this.set(field, val);
},
// 清空header的特定字段
remove(field) {
if (this.headerSent) return;
this.res.removeHeader(field);
},
get writable() {
if (this.res.writableEnded || this.res.finished) return false;
const socket = this.res.socket;
if (!socket) return true;
return socket.writable;
},
inspect() {
if (!this.res) return;
const o = this.toJSON();
o.body = this.body;
return o;
},
toJSON() {
return only(this, [
'status',
'message',
'header'
]);
},
// 刷新headers信息
flushHeaders() {
this.res.flushHeaders();
}
};