koa介绍
koa有1.x和2.x两个版本,koa1.x基于generator,依赖co的包装。koa2.x基于promise,在node v7.6+可以直接运行,低版本可以babel。下面是基于koa2.13源码的介绍。
简单使用
// 1 导包
const Koa = require('koa');
// 2 new一个koa的实例 说明koa是一个类
const app=new Koa();
// 3 挂载中间件
app.use(async function (ctx, next) {
const startTime = new Date()
console.log(`${startTime} ${ctx.method} ${ctx.url}`);
await next();
const endTime = new Date()
console.log(`${endTime} ${endTime.getTime() - startTime.getTime()}ms`);
});
app.use(ctx => {
ctx.body='hello world';
});
// 4 指定监听端口
app.listen('3000', () => {
console.log('start suc!!');
console.log('http://127.0.0.1:3000');
});
目录结构和依赖
lib目录下有四个文件:
- application.js: 应用程序。
- context.js:上下文。
- request.js: 请求类。
- response.js: 响应类。
{
"dependencies": {
"accepts": "^1.3.5",
"cache-content-type": "^1.0.0",
"content-disposition": "~0.5.2",
"content-type": "^1.0.4",
"cookies": "~0.8.0",
"debug": "~3.1.0",
"delegates": "^1.0.0",
"depd": "^1.1.2",
"destroy": "^1.0.4",
"encodeurl": "^1.0.2",
"escape-html": "^1.0.3",
"fresh": "~0.5.2",
"http-assert": "^1.3.0",
"http-errors": "^1.6.3",
"is-generator-function": "^1.0.7",
"koa-compose": "^4.1.0",
"koa-convert": "^1.2.0",
"on-finished": "^2.3.0",
"only": "~0.0.2",
"parseurl": "^1.3.2",
"statuses": "^1.5.0",
"type-is": "^1.6.16",
"vary": "^1.1.2"
},
"deprecated": false,
"description": "Koa web app framework",
"devDependencies": {
"egg-bin": "^4.13.0",
"eslint": "^6.5.1",
"eslint-config-koa": "^2.0.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"gen-esm-wrapper": "^1.0.6",
"mm": "^2.5.0",
"supertest": "^3.1.0"
},
"engines": {
"node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
},
"exports": {
".": {
"require": "./lib/application.js",
"import": "./dist/koa.mjs"
},
"./": "./"
},
"files": [
"dist",
"lib"
],
"main": "lib/application.js",
"name": "koa",
"version": "2.13.0"
}
入口文件: lib/application.js
一、 application.js
module.exports = class Application extends Emitter
...
module.exports.HttpError = HttpError;
Application 是继承Emitter的类,并且同时导出了HttpErrorr。为什么会继承Emitter?往下看。
1 constructor
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);
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
option配置分以下几种
1 主要配置反向代理,客户端ip相关的配置(proxy subdomainOffset proxyIpHeader maxIpsCount )
2 middleware是中间件数组。
3 响应头、请求头、上下文(request response Context) 通过Object.create继承原有对象的属性并且创建新的对象。
4 util.inspect.custom是一个Symbol类型的值,通过定义对象的[util.inspect.custom]属性为一个函数,简单来说就是自定义对象的查看函数。
2 use
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 || '-');
this.middleware.push(fn);
return this;
}
由于koa最开始支持使用generator函数作为中间件使用,如果是generator函数这里使用convert进行了一次转换,并且push到了middleware数组中。
3 listen
listen(...args) {
debug('listen');
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) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
listen方法先调用http模块的createServer, 传入回调函数callback,listen方法的入参合http的listen的入参是一致的。创建http服务成功之后会调用callback函数。先调用compose返回fn。之后就是handleRequest生成ctx,并按中间件加载的顺序执行逻辑了。到这里就可以回到继承Emitter,因为这里用到事件触发器(on和emit)完成事件监听和发送。
4 createContext(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;
}
createContext主要做的就是构造一个ctx对象。
5 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);
}
fnMiddleware就是koa-compose函数的返回值
6 compose(koa-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) {
// 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 {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
按app.use的顺序执行,形成了洋葱模型。compose的返回值是一个匿名函数,有context和next两个函数。首次执行时候context是由fnMiddleware函数传入的,next为undefined。fn为执行当前次的中间件函数,next其实就是为dispatch函数。整体上使用递归+闭包的方式进行了实现。
7 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();
}
// 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
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
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
当所有的中间件都成功执行了,就会进到respond函数里,这里会判断ctx.body是不是有值的,如果没有值则返回404页面。
二、request.js
整体的属性就是对http.req中比较重要的字段重新定义了出来,比如length字段,就是res.header.Content-Length字段。
三、response.js
挑出几个比较常用的。
1 set body
set body(val) {
const original = this._body;
this._body = val;
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
if (val === null) this._explicitNullBody = true;
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 (val instanceof Stream) {
onFinish(this.res, destroy.bind(null, val));
if (original != val) {
val.once('error', err => this.ctx.onerror(err));
// overwriting
if (null != original) this.remove('Content-Length');
}
if (setType) this.type = 'bin';
return;
}
// json
this.remove('Content-Length');
this.type = 'json';
}
当ctx.body = ''时候调用。有下面几种场景。
1 ctx.body = null 或者undefined时候,相当于状态码是204
2 ctx.body非null 则 状态码为200
3 ctx.body为字符串类型,则Type为html或text
4 ctx.body为buffer类型,则Type为bin
5 ctx.body为Stream类型,则Type为bin
5 ctx.body其他,则Type为json
2 status
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;
}
四、context.js
重点看下别名怎么实现的。如ctx.body是ctx.response.body的别名
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');
可以看出来是通过delegate,深入到delegate内部看下
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
/**
* Delegator setter `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
get、set其实就是通过defineGetter和defineSetter进行实现的。
五、总结
koa中间件执行流程图