剖析并实现一个简单版的koa框架

发布 : 2020-05-12 分类 : nodejs 浏览 :

本文代码放在 Github

一、关于 node http 的用法

node http 的用法很简单,http 的每次请求,都会带有 request 和 response 两个对象。

1
2
3
4
5
6
7
8
9
10
11
const server = http.createServer((request, response) => {
// request 可以获取到 http 请求头相关的东西
console.log(request.method);
console.log(request.url);
console.log(request.headers['cookie']);
console.log(request.httpVersion);
// response 响应流
response.write('response data.');
response.end();
});
server.listen(3000);

通过 request 可以获取到请求头相关的东西,比如请求头的 method、url、http header 等等,另外,request 继承自可读流,因此 request 具备可读流的所有特点,比如可以监听 on(‘data’) 获取到请求体的内容,也可以on(‘end’) 监听流的结束,代码如下:

1
2
3
4
5
6
7
8
const data = [];
  request.on('data'function (dataBuffer{
    data.push(dataBuffer);
  });
  request.on('end'function ({
    const buffer = Buffer.concat(data).toString();
    console.log(buffer);
  });

response 继承自可写流,拥有可写流的所有特点,比如 write、end 、close操作,
通过 response.wirte(‘响应数据’) + response.end() 可以向客户端响应数据。
response.end(‘xxx’) 相当于 response.write(‘xxx’) + response.close();
代码如下:

1
2
response.write('response data.');
  response.end();

二、koa 是基于 node http 的

koa 是一个 http 框架,因此,koa 是基于 node http 模块的。
最简单的 koa 实现如下(其实内部就是封装了 http):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Koa {
  constructor(fn) {
    this.fn = fn;
  }
  listen(...args) {
    const server = http.createServer((req, res) => {
      this.fn(req, res);
    });
    return server.listen(...args);
  }
}
function handleHttp(req, res{
  res.end('response data.');
}
const koa = new Koa(handleHttp);
koa.listen(3000);

三、koa 的上下文 Context

1、在 koa 的Context 中,实现了如下功能:

  • 1)分别定义了 req和res 属性来保留原生的 request和response 对象,因此我们可以通过 ctx.req 和 ctx.res 访问到原来的 request和response 对象。
  • 2)分别定义了 request 和 response 属性,用于扩展 原生的request和response 对象。
  • 3)实现了委托,把 context 上的属性,委托给 request 或 response 对象,比如,当我访问 ctx.body=xxxx 的时候,其实访问的是 response.body=xxx,当我访问 ctx.method 的时候,其实访问的是 request.method。

2、委托的实现原理如下:

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
function Delegator(proto, target{
  if (!(this instanceof Delegator)) { // 这里是为了保证 Delegator 的实例是 new 出来的
    return new Delegator(proto, target);
  }
  this.proto = proto;
  this.target = target;
}

Delegator.prototype.getter = function (name{
  const proto = this.proto;
  const target = this.target;
  proto.__defineGetter__(name, function ({
    return this[target][name]; // 当访问 proto[name] 的时候,访问的是 proto[target][name]
  });
  return this;
};
Delegator.prototype.setter = function (name{
  const proto = this.proto;
  const target = this.target;
  proto.__defineSetter__(name, function (value{
    return this[target][name] = value; // 当设置 proto[name] 的时候,访问的是 proto[target][name]
  });
  return this;
};
Delegator.prototype.access = function (name{
  return this.getter(name).setter(name);
};

Delegator.prototype.method = function (name{
  const proto = this.proto;
  const target = this.target;
  proto[name] = function ({
    // 当调用 proto[name]() 函数的时候,访问的是 proto[target][name]()
    return this[target][name].apply(target, arguments);
  };
  return this;
};

const context = {
  request: {
    a: 123,
    fn: function ({
      return 'fn';
    }
  }
};
Delegator(context, 'request')
  .getter('a')
  .setter('a')
  .method('fn');

console.log(context.a); // 123
console.log(context.fn()); // fn

输出如下:

1
2
123
fn

3、Context 在 koa 的应用

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
const request = {
  get method() {
    return this.req.method;
  },
};
const response = {
  get body() {
    return this._body;
  },
  set body(val) {
    this._body = val;
  },
};
const context = {};
Delegator(context, 'request').getter('method');
Delegator(context, 'response').access('body');

class Koa {
  constructor(fn) {
    this.fn = fn;
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
  listen(...args) {
    const server = http.createServer((req, res) => {
      const context = this.createContext(req, res);
      Promise.resolve(this.fn(context)).then(() => {
        let body = 'koa response';
        if (context.body) {
          body = context.body;
        }
        res.write(body);
        res.end();
      });

    });
    return server.listen(...args);
  }
  createContext(req, res) {
    const context = Object.create(this.context);
    context.res = res;
    context.req = req;
    context.request = Object.create(this.request);
    context.response = Object.create(this.response);
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    return context;
  }
}
function handleHttp(ctx{
  console.log(ctx.method); // ctx.method 会被委托到 ctx.request.method
  ctx.body = 'reponse data.'// ctx.body 会被委托到 ctx.response.body
}
const koa = new Koa(handleHttp);
koa.listen(3000);

四、middleware(中间件)洋葱模型

koa 的核心思想在于基于中间件的洋葱模型,koa 通过一个 compose 函数,把一个一个的中间件串联起来,组合成了一个大的函数,在每个中间件里,通过 next() 执行下一个中间件。
引用官方的一张洋葱模型图,如下:

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
31
32
33
34
35
36
37
38
function compose(middlewares{
  return function (context, next{
    function dispatch(i{
      let fn = middlewares[i];
      if (i === middlewares.length) fn = next;
      if (!fn) {
        return Promise.resolve();
      }
      try {
        return Promise.resolve(fn(context, () => {
          return dispatch(i + 1);
        }));
      } catch (e) {
        return Promise.reject(e);
      }
    }
    return dispatch(0);
  };
}

const middlewares = [];
async function a(ctx, next{
  console.log('a1');
  const r = await next();
  console.log(r);
  console.log('a2');
}
async function b(ctx, next{
  console.log('b1');
  await next();
  console.log('b2');
  return 'b';
}
middlewares.push(a);
middlewares.push(b);
compose(middlewares)({}).then(v => {
  console.log('ddd');
});

以上代码的执行顺序如下:

1
2
3
4
5
6
in
in
b out
b
a out
end

五、在 koa 应用中间件洋葱模型

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const http = require('http');
const EventEmitter = require('events')

function compose(middlewares) {
return function (context, next) {
function dispatch(i) {
let fn = middlewares[i];
if (i === middlewares.length) fn = next;
if (!fn) {
return Promise.resolve();
}
try {
return Promise.resolve(fn(context, () => {
return dispatch(i + 1);
}));
} catch (e) {
return Promise.reject(e);
}
}
return dispatch(0);
};
}

function Delegator(proto, target) {
if (!(this instanceof Delegator)) { // 这里是为了保证 Delegator 的实例是 new 出来的
return new Delegator(proto, target);
}
this.proto = proto;
this.target = target;
}

Delegator.prototype.getter = function (name) {
const proto = this.proto;
const target = this.target;
proto.__defineGetter__(name, function () {
return this[target][name]; // 当访问 proto[name] 的时候,访问的是 proto[target][name]
});
return this;
};
Delegator.prototype.setter = function (name) {
const proto = this.proto;
const target = this.target;
proto.__defineSetter__(name, function (value) {
return this[target][name] = value; // 当设置 proto[name] 的时候,访问的是 proto[target][name]
});
return this;
};
Delegator.prototype.access = function (name) {
return this.getter(name).setter(name);
};

Delegator.prototype.method = function (name) {
const proto = this.proto;
const target = this.target;
proto[name] = function () {
// 当调用 proto[name]() 函数的时候,访问的是 proto[target][name]()
return this[target][name].apply(target, arguments);
};
return this;
};


const request = {
get method() {
return this.req.method;
},
};
const response = {
get body() {
return this._body;
},
set body(val) {
this._body = val;
},
};
const context = {};
Delegator(context, 'request').getter('method');
Delegator(context, 'response').access('body');

class Koa extends EventEmitter {
constructor() {
super();
this.middlewares = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
listen(...args) {
const server = http.createServer((req, res) => {
this.handleHttp(req, res);
});
return server.listen(...args);
}
use(fn) {
this.middlewares.push(fn);
}
handleHttp(req, res) {
const middleware = compose(this.middlewares);
const context = this.createContext(req, res);
middleware(context).then(() => {
let body = 'koa response';
if (context.body) {
body = context.body;
}
res.write(body);
res.end();
});
}
createContext(req, res) {
const context = Object.create(this.context);
context.res = res;
context.req = req;
context.request = Object.create(this.request);
context.response = Object.create(this.response);
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
return context;
}
}
const koa = new Koa();
async function middlewareA(ctx, next) {
console.time('responseTime');
console.log('middlewareA in');
console.log(ctx.method); // ctx.method 会被委托到 ctx.request.method
await next();
console.log(ctx.body);
console.log('middlewareA out');
console.timeEnd('responseTime');
}
function middlewareB(ctx, next) {
console.log('middlewareB in');
ctx.res.setHeader('Content-Type', 'text/html;charset=utf-8');
ctx.body = 'reponse data.'// ctx.body 会被委托到 ctx.response.body
console.log('middlewareB out');
}
koa.use(middlewareA);
koa.use(middlewareB);
const server = koa.listen(3000);
server.on('listening', function () {
console.log('listening on localhost:3000');
});

六、代码放在 github 上

https://github.com/SimpleCodeCX/simple-koa

本文作者 : Simple
原文链接 : https://simplecodecx.github.io/blog/20200512/50328b1d.html
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
留下足迹