Koa插件之类

基本组成

  1. application.js:框架入口;负责管理中间件,以及处理请求
  2. context.js:context对象的原型,代理request与response对象上的方法和属性
  3. request.js:request对象的原型,提供请求相关的方法和属性
  4. response.js:response对象的原型,提供响应相关的方法和属性

开始

新建test.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const koa = require('koa');
const app = new koa();

app.use(async (ctx, next) => {
console.log(`1 start`);
await next();
console.log('1 end');
})

app.use(async (ctx, next) => {
console.log(`2 start`);
await next();
console.log('2 end');
})

app.use(async (ctx, next) => {
console.log(`3 start`);
await next();
console.log('3 end');
})

app.listen(3000);
console.log('koa server is listening port 3000');

通过node --inspect-brk test运行,进入调试模式。

WX20191005-125153@2x

浏览器打开http://127.0.0.1:9229/,点击这个

WX20191005-125329@2x

首先通过new Koa()创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = class Application extends Emitter {
constructor() {
super();

this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
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;
}
}
...
}

对象实例:

WX20191005-190445@2x

app.use方法

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
  • 判断了传入的fn类型是否是function
  • 然后判断是否是generator函数
  • 之后pushmiddleware数组里

app.listen方法

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
  • 先执行this.callback(),返回一个handleRequest函数
1
2
3
4
5
6
7
8
9
10
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;
}

compose是由一个名为koa-compose的库,它可以将多个中间件组合成洋葱式调用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)
}
}
}
}
  • 调用http.createServer,该函数用来创建一个HTTP服务器,并将 handleRequest 作为 request 事件的监听函数。

  • server.listen(...args)启动服务器监听连接,3000端口号

发送请求到http://localhost:3000/,执行回调函数

WX20191005-201601@2x

1
2
3
4
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
  • 首先调用createContext创建上下文对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
  • 调用this.handleRequest(ctx, fn)
1
2
3
4
5
6
7
8
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(ctx).then(handleResponse).catch(onerror);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
}
}
}

小结

1)首先先创建koa实例

2)app.use是往middleware数组里push进回调函数

3)app.listen启动服务端器监听,并且添加了回调函数

4)触发接口请求回调函数的时候,会将创建的上下文对象与next(next其实就是dispatch方法),作为参数传入到middleware中每项的函数调用中,如果调用了next就是调用的dispatch.bind(null, i + 1)

koa-router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const koa = require('koa');
const app = new koa();

const Router = require('koa-router');
const router = new Router();

router.get('/list/:id', async (ctx, next) => {
ctx.body = {
success: true,
data: [{
name: 'name1',
},
{
name: 'name2',
}
]
};
})

app.use(router.routes());

app.listen(3000);
console.log('koa server is listening port 3000');

看实例化Router的过程之前,先看一下,遍历数组为Router构造函数添加prototype属性

WX20191005-224823@2x

Router构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Router(opts) {
if (!(this instanceof Router)) {
return new Router(opts);
}

this.opts = opts || {};
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];

this.params = {};
this.stack = [];
};

Router的实例

WX20191005-225339@2x

router.get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Router.prototype[method] = function (name, path, middleware) {
var middleware;

if (typeof path === 'string' || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}

this.register(path, [method], middleware, {
name: name
});

return this;
};

在这里调用register并且传入pathmethodnamemiddleware(这里有对router.get传入的值做了一些修改),进入regiseter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};

var router = this;
var stack = this.stack;

// support array of paths
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});

return this;
}

// create route
var route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
ignoreCaptures: opts.ignoreCaptures
});

if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}

// add parameter middleware
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);

stack.push(route);

return route;
};

这里实例化了route,并且push到了stack

app.use(router.routes())

先看router.routes()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;

var dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);

var path = router.opts.routerPath || ctx.routerPath || ctx.path;
var matched = router.match(path, ctx.method);
var layerChain, layer, i;

if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}

ctx.router = router;

if (!matched.route) return next();

var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}

layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);

return compose(layerChain)(ctx, next);
};

dispatch.router = this;

return dispatch;
};

声明dispatch方法并且把Router实例挂载到了这个方法上

之后调用app.usedispatch方法push进去了middleware数组

app.listen方法

与上面一样,就是传入回调,启动个服务。

访问http://localhost:3000/list/1触发回调

进入回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
}
}
}

在这里,取得middleware的第一个回调并且调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);

var path = router.opts.routerPath || ctx.routerPath || ctx.path;
var matched = router.match(path, ctx.method);
var layerChain, layer, i;

if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}

ctx.router = router;

if (!matched.route) return next();

var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}

layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);

return compose(layerChain)(ctx, next);
};

最后调用到compose方法,传入的layerChina是个数组,第一个是匿名函数

1
2
3
4
5
6
function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
}

第二个是访问/list的回调函数

WX20191006-231742@2x

最后又调用进这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}

也就是先调用了上面的匿名函数,这个匿名函数为上下文对象添加了一些属性

WX20191006-234337@2x

之后调用next方法,走到我们为访问/list添加的回调那里

WX20191006-234604@2x

小结

1)创建Router实例

2)router.get,注册路由规则,并且pushstack数组

3)app.use(router.routes()),将router.routes()返回的dispatch方法pushmiddleware数组里

4)app.listen(3000),添加回调函数,并且启动服务器监听

5)访问http://localhost:3000/list/1,调用middleware数组中第一个函数调用,这个函数调用返回compose(layerChain)(ctx, next);

6)layerChain是由两个函数组成的数组,里面的第一个函数被调用时候,解析path,为ctx上下文对象添加一些属性,之后调用next()

7)也就是走进layerChain里的第二个函数调用,也就是router.get的回调函数

koa-cors

test.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Document</title>
</head>

<body>
</body>
<script>
function send() {
var xmlHttp;
if (XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
} else {
xmlHttp = new ActiveXObject();
}

xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4) {
if ((xmlHttp.status >= 200 && xmlHttp.status <= 300) || xmlHttp.status == 304) {
console.log(xmlHttp.response);
}
}
}
xmlHttp.open('GET', 'http://127.0.0.1:3000/list/1', true);
xmlHttp.send();
}

send();
</script>

</html>

test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const koa = require('koa');
const app = new koa();

const Router = require('koa-router');
const router = new Router();

var cors = require('koa2-cors');

router.get('/list/:id', async (ctx, next) => {
ctx.body = {
success: true,
data: [{
name: 'name1',
},
{
name: 'name2',
}
]
};
})

app.use(cors());
app.use(router.routes());

app.listen(3000);
console.log('koa server is listening port 3000');

app.use(cors())调用

先进入cors()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
module.exports = function crossOrigin(options = {}) {
const defaultOptions = {
allowMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
};

// set defaultOptions to options
options = Object.assign({}, defaultOptions, options); // eslint-disable-line no-param-reassign

// eslint-disable-next-line consistent-return
return async function cors(ctx, next) {
// always set vary Origin Header
// https://github.com/rs/cors/issues/10
ctx.vary('Origin');

let origin;
if (typeof options.origin === 'function') {
origin = options.origin(ctx);
} else {
origin = options.origin || ctx.get('Origin') || '*';
}
if (!origin) {
return await next();
}

// Access-Control-Allow-Origin
ctx.set('Access-Control-Allow-Origin', origin);

if (ctx.method === 'OPTIONS') {
// Preflight Request
if (!ctx.get('Access-Control-Request-Method')) {
return await next();
}

// Access-Control-Max-Age
if (options.maxAge) {
ctx.set('Access-Control-Max-Age', String(options.maxAge));
}

// Access-Control-Allow-Credentials
if (options.credentials === true) {
// When used as part of a response to a preflight request,
// this indicates whether or not the actual request can be made using credentials.
ctx.set('Access-Control-Allow-Credentials', 'true');
}

// Access-Control-Allow-Methods
if (options.allowMethods) {
ctx.set('Access-Control-Allow-Methods', options.allowMethods.join(','));
}

// Access-Control-Allow-Headers
if (options.allowHeaders) {
ctx.set('Access-Control-Allow-Headers', options.allowHeaders.join(','));
} else {
ctx.set('Access-Control-Allow-Headers', ctx.get('Access-Control-Request-Headers'));
}

ctx.status = 204; // No Content
} else {
// Request
// Access-Control-Allow-Credentials
if (options.credentials === true) {
if (origin === '*') {
// `credentials` can't be true when the `origin` is set to `*`
ctx.remove('Access-Control-Allow-Credentials');
} else {
ctx.set('Access-Control-Allow-Credentials', 'true');
}
}

// Access-Control-Expose-Headers
if (options.exposeHeaders) {
ctx.set('Access-Control-Expose-Headers', options.exposeHeaders.join(','));
}

try {
await next();
} catch (err) {
throw err;
}
}
};

返回了一个异步函数,之后app.use把这个函数pushmiddleware数组里

刷新html页面,触发send方法,发送请求,触发回调,之后先调用middleware数组里第一个函数调用,就是上面的这个方法,这个方法(在这个例子中)做的事情:

  • 获取origin,通过ctx.set('Access-Control-Allow-Origin', origin);设置头部
  • 调用next()

这里的next调用就是路由Router.routes()返回的那个方法,调用步骤与上面路由的地方一样

小结

1)app.use(cors())把回调传入middleware数组

2)发送请求,设置响应头

因为这边是简单请求,所以没有OPTIONS预检请求

  • 当替换请求方法为PUT的时候,判断是OPTIONS请求的时候,会设置这个响应头

WX20191007-153846@2x

  • 当添加请求头name属性的时候,会设置这个响应头

WX20191007-154107@2x

koa-bodyparser

1
2
3
4
5
...
const bodyParser = require('koa-bodyparser')
...
app.use(bodyParser())
...

先进入bodyParser()

最后往middleware数组push进去这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function bodyParser(ctx, next) {
if (ctx.request.body !== undefined) return await next();
if (ctx.disableBodyParser) return await next();
try {
const res = await parseBody(ctx);
ctx.request.body = 'parsed' in res ? res.parsed : {};
if (ctx.request.rawBody === undefined) ctx.request.rawBody = res.raw;
} catch (err) {
if (onerror) {
onerror(err, ctx);
} else {
throw err;
}
}
await next();
};

收到请求后根据不同的content-type调用不同的方法解析请求体,这边使用的是application/x-www-form-urlencoded

WX20191024-215420@2x

获取请求的content-encodingcontent-length和编码方式utf-8

  • 先根据contentn-encoding的值判断是否需要解压,还是直接返回

WX20191028-095029@2x

  • 根据编码方式,Buffer转字符串

WX20191028-095418@2x

WX20191028-094333@2x

  • 解析完成后返回str

WX20191028-093318@2x

最后挂载在ctx.request.body上

WX20191024-220208@2x