本文基于 [email protected]
Koa简介(废话篇)
Koa
是基于
Node
.
js
的
HTTP
框架,由
Express
原班人马打造。是下一代的
HTTP
框架,更简洁,更高效。
我们来看一下下载量(2018.3.4)
Koa
:471,451 downloads in the last month
Express
:18,471,701 downloads in the last month
说好的
Koa
是下一代框架呢,为什么下载量差别有这么大呢,
Express
一定会说:
你大爷还是你大爷!
。
确实,好多知名项目还是依赖
Express
的,比如webpack的dev-server就是使用的
Express
,所以还是看场景啦,如果你喜欢DIY,喜欢绝对的控制一个框架,那么这个框架就应该什么功能都不提供,只提供一个基础的运行环境,所有的功能由开发者自己实现。
正式由于
Koa
的高性能和简洁,好多知名项目都在基于
Koa
,比如阿里的
eggjs
,360奇舞团的
thinkjs
。
所以,虽然从使用范围上来讲,
Express
对于
Koa
是
你大爷还是你大爷!
,但是如果
Express
很好,为什么还要再造一个
Koa
呢?接下来我们来了解下
Koa
到底带给我们了什么,
Koa
到底做了什么。
如何着手分析Koa
先来看两段demo。
下面是
Node
官方给的一个HTTP的示例。
const http = require('http');
const
hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
下面是最简单的一个
Koa
的官方实例。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Koa
是一个基于
Node
的框架,那么底层一定也是用了一些
Node
的API。
jQuery
很好用,但是
jQuery
也是基于DOM,逃不过也会用
element
.
appendChild
这样的基础API。
Koa
也是一样,也是用一些
Node
的基础API,封装成了更好用的HTTP框架。
那么我们是不是应该看看
Koa
中
http
.
createServer
的代码在哪里,然后顺藤摸瓜,了解整个流程。
Koa核心流程分析
Koa
的源码有四个文件:
-
application.js // 核心逻辑
-
context.js // 上下文,每次请求都会生成一个
-
request.js // 对原生HTTP的req对象进行包装
-
response.js // 对原生HTTP的res对象进行包装
我们主要关心
application
.
js
中的内容,直接搜索
http
.
createServer
,会搜到
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
刚好和
Koa
中的这行代码
app
.
listen
(
3000
);
关联起来了。
找到源头,现在我们就可以梳理清楚主流程,大家对着源码看我写的这个流程
fn:listen
∨
fn:callback
∨
[fn:compose] // 组合中间件 会生成后面的 fnMiddleware
∨
fn:handleRequest // (@closure in callback)
∨
[fn(req, res):createContext] // 创建上下文 就是中间件中用的ctx
∨
fn(ctx, fnMiddleware):handleRequest // (@koa instance)
∨
code:fnMiddleware(ctx).then(handleResponse).catch(onerror);
∨
fn:handleResponse
∨
fn:respond
∨
code:res.end(body);
从上面可以看到最开始是
listen
方法,到最后HTTP的
res
.
end
方法。
listen
可以理解为初始化的方法,每一个请求到来的时候,都会经过从
callback
到
respond
的生命周期。
在每个请求的生命周期中,做了两件比较核心的事情:
-
将多个中间件组合
-
创建ctx对象
多个中间件组合后,会先后处理ctx对象,ctx对象中既包含的req,也包含了res,也就是每个中间件的对象都可以处理请求和响应。
这样,一次HTTP请求,接连经过各个中间件的处理,再到返回给客户端,就完成了一次完美的请求。
Koa中的ctx
app.use(async ctx => {
ctx.body = 'Hello World';
});
上面的代码是一个最简答的中间件,每个中间件的第一个参数都是
ctx
,下面我们说一下这个
ctx
是什么。
创建
ctx
的代码:
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.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req
);
context.state = {};
return context;
}
直接上代码,Koa每次请求都会创建这样一个ctx对象,以提供给每个中间件使用。
参数的
req
,
res
是Node原生的对象。
下面解释下这三个的含义:
-
context
:Koa封装的带有一些和请求与相应相关的方法和属性
-
request
:Koa封装的req对象,比如提了供原生没有的
host
属性。
-
response
:Koa封装的res对象,对返回的
body
hook了getter和setter。
其中有几行一堆
xx
=
xx
=
xx
,这样的代码。
是为了让ctx、request、response,能够互相引用。
举个例子,在中间件里会有这样的等式
ctx.request.ctx === ctx
ctx.response.ctx === ctx
ctx.request.app === ctx.app
ctx.response.app === ctx.app
ctx.req === ctx.response.req
// ...
为什么会有这么奇怪的写法?其实只是为了互相调用方便而已,其实最常用的就是ctx。
打开
context
.
js
,会发现里面写了一堆的
delegate
:
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
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')
.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
、
response
中的属性也挂在
ctx
下,我们为了拿到请求的路径需要
ctx
.
request
.
path
,但是由于代理过
path
这个属性,
ctx
.
path
也是可以的,即
ctx
.
path
===
ctx
.
request
.
path
。
ctx
模块大概就是这样,没有讲的特别细,这块是重点不是难点,大家有兴趣自己看看源码很方便。
一个小tip: 有时候我也会把
context
.
js
中最下面的那些
delegate
当成文档使用,会比直接看文档快一点。
Koa中间件机制
中间件函数的参数解释
一个小栗子
我们来看这样的代码:
// 第一个中间件
app.use(async(ctx, next) => {
console.log('m1.1', ctx.path);
ctx.body = 'Koa m1';
ctx.set('m1', 'm1');
next();
console.
log('m1.2', ctx.path);
});
// 第二个中间件
app.use(async(ctx, next) => {
console.log('m2.1', ctx.path);
ctx.body = 'Koa m2';
ctx.set('m2', 'm2');
next();
debugger
console.log('m2.2', ctx.path);
});
// 第三个中间件
app
.use(async(ctx, next) => {
console.log('m3.1', ctx.path);
ctx.body = 'Koa m3';
ctx.set('m3', 'm3');
next();
console.log('m3.2', ctx.path);
});
会输出什么呢?来看下面的输出:
m1.1 /
m2.1 /
m3.1 /
m3.2 /
m2
.2 /
m1.2 /
来解释一下上面输出的现象,由于将
next
理解为是下一个中间件,在第一个中间件执行
next
的时候,第一个中间件就将
执行权限
给了第二个中间件,所以
m1
.
1
后输出的是
m2
.
1
,在之后是
m3
.
1
。
那么为什么
m3
.
1
后面输出的是
m3
.
2
呢?第三个中间件之后已经没有中间件了,那么第三个中间件里的
next
又是什么?
我先偷偷告诉你,最后一个中间件的
next
是一个立刻resolve的Promise,即
return
Promise
.
resolve
()
,一会再告诉你这是为什么。
所以第三个中间件(即最后一个中间件)可以理解成是这样子的:
app.use(async (ctx, next) => {
console.log('m3.1',
ctx.path);
ctx.body = 'Koa m3';
ctx.set('m3', 'm3');
new Promise.resolve(); // 原来是next
console.log('m3.2', ctx.path);
});
从代码上看,
m3
.
1
后面就会输出
m3
.
2
。
那为什么
m3
.
2
之后又会输出
m2
.
2
呢?,我们看下面的代码。
let f1 = () => {
console.log(1.1);
f2
();
console.log(1.2);
}
let f2 = () => {
console.log(2.1);
f3();
console.log(2.2);
}
let f3 = () => {
console.log(3.1);
Promise.resolve();
console.log(3.2);
}
f1();
/*
outpout
1.1