Koa2 学习
Koa 学习
历史
Express
Express是第一代最流行的web框架,它对Node.js的http
进行了封装; Express的API很简单,但是它是基于ES5的语法,要实现异步代码,只有一个方法:回调(回调地狱)。如果异步嵌套层次过多,代码写起来就非常难看.
app.get('/test', function (req, res) {
fs.readFile('/file1', function (err, data) {
if (err) {
res.status(500).send('read file1 error');
}
fs.readFile('/file2', function (err, data) {
if (err) {
res.status(500).send('read file2 error');
}
res.type('text/plain');
res.send(data);
});
});
});
虽然可以用async这样的库来组织异步代码,但是用回调写异步实在是太痛苦了!
Koa 1.0
随着新版Node.js开始支持ES6,Express的团队又基于ES6的generator重新编写了下一代web框架koa。和Express相比,koa 1.0使用generator实现异步,代码看起来像同步的:
var koa = require('koa');
var app = koa();
app.use('/test', function *() {
yield doReadFile1();
var data = yield doReadFile2();
this.body = data;
});
app.listen(3000);
用generator
实现异步比回调简单了不少,但是generator
的本意并不是异步。Promise
才是为异步设计的,但是Promise
的写法……想想就复杂。为了简化异步代码,ES7(目前是草案,还没有发布)引入了新的关键字async
和await
,可以轻松地把一个function
变为异步模式:
async function () {
var data = await fs.read('/file1');
}
这是JavaScript未来标准的异步代码,非常简洁,并且易于使用。
Koa2
koa团队并没有止步于koa 1.0,他们非常超前地基于ES7开发了koa2,和koa 1相比,koa2完全使用Promise
并配合async
来实现异步。
koa2的代码看上去像这样:
app.use(async (ctx, next) => {
await next();
var data = await doReadFile();
ctx.response.type = 'text/plain';
ctx.response.body = data;
});
出于兼容性考虑,目前koa2仍支持generator的写法,但下一个版本将会去掉。
Generator 知识点
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征:
-
function
关键字与函数名之间有一个星号
; - 函数体内部使用
yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个 Generator
函数helloWorldGenerator
,它内部有两个yield
表达式(hello
和world
),即该函数有三个状态:hello
,world
和 return
语句(结束执行)
然后,Generator
函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator
函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object
)。
下一步,必须调用遍历器对象的next
方法,使得指针移向下一个状态。也就是说,每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。换言之,Generator
函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
第一次调用,Generator
函数开始执行,直到遇到第一个yield
表达式为止。next
方法返回一个对象,它的value
属性就是当前yield
表达式的值hello
,done
属性的值false
,表示遍历还没有结束。
第二次调用,Generator
函数从上次yield
表达式停下的地方,一直执行到下一个yield
表达式。next
方法返回的对象的value
属性就是当前yield
表达式的值world
,done
属性的值false
,表示遍历还没有结束。
第三次调用,Generator
函数从上次yield
表达式停下的地方,一直执行到return
语句(如果没有return
语句,就执行到函数结束)。next
方法返回的对象的value
属性,就是紧跟在return
语句后面的表达式的值(如果没有return
语句,则value
属性的值为undefined
),done
属性的值true
,表示遍历已经结束。
第四次调用,此时 Generator
函数已经运行完毕,next
方法返回对象的value
属性为undefined
,done
属性为true
。以后再调用next
方法,返回的都是这个值。
总结一下,调用 Generator
函数,返回一个遍历器对象,代表 Generator
函数的内部指针。以后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
由于 Generator
函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function
关键字后面。本书也采用这种写法。
简介
koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套, 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
安装
Koa 依赖 node v7.6.0 或 ES2015及更高版本和 async 方法支持.
$ npm install koa
应用程序
Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的. 关键的设计点是在其低级中间件层中提供高级“语法糖”。 这提高了互操作性,稳健性,并使书写中间件更加愉快。
这包括诸如内容协商,缓存清理,代理支持和重定向等常见任务的方法。 尽管提供了相当多的有用的方法 Koa 仍保持了一个很小的体积,因为没有捆绑中间件。
第一个koa应用程序:
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa');
// 创建一个Koa对象表示web app本身:
const app = new Koa();
// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => { // 对于每一个http请求,koa将调用我们传入的异步函数来处理:
await next();
// 设置response的Content-Type:
ctx.response.type = 'text/html';
// 设置response的内容:
ctx.response.body = '<h1>Hello, koa2!</h1>';
});
// 在端口3000监听:
app.listen(3000, () => {
console.log('Server is running...');
});
其中,参数ctx
是由koa
传入的封装了request
和response
的变量,我们可以通过它访问request
和response
,next
是koa
传入的将要处理的下一个异步函数。
上面的异步函数中,我们首先用await next();
处理下一个异步函数,然后,设置response
的Content-Type
和内容。
由async
标记的函数称为异步函数,在异步函数中,可以用await
调用另一个异步函数,这两个关键字将在ES7中引入。
级联
Koa 中间件以更传统的方式级联,您可能习惯使用类似的工具 - 之前难以让用户友好地使用 node 的回调。然而,使用 async 功能,我们可以实现 “真实” 的中间件。对比 Connect 的实现,通过一系列功能直接传递控制,直到一个返回,Koa 调用“下游”,然后控制流回“上游”。
根据上一个例子,让我们再仔细看看koa的执行逻辑。核心代码是:
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});
每收到一个http
请求,koa
就会调用通过app.use()
注册的async
函数,并传入ctx
和next
参数。
我们可以对ctx操作,并设置返回内容。但是为什么要调用await next()?
原因是koa
把很多async
函数组成一个处理链,每个async
函数都可以做一些自己的事情,然后用await next()
来调用下一个async
函数。我们把每个async
函数称为middleware
,这些middleware
可以组合起来,完成很多有用的功能。
下面以 “Hello World” 的响应作为示例,首先请求流通过 x-response-time 和 logging 中间件来请求何时开始,然后继续移交控制给 response 中间件。当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。
const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - 耗时:${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
上面这个例子的返回结果可以看出,遇到 await next();
开始执行下一个,直到没有中间件了,才逆流而上,从耗时上可以看出。
middleware的顺序很重要,也就是调用app.use()的顺序决定了middleware的顺序。
此外,如果一个middleware没有调用await next(),会怎么办?答案是后续的middleware将不再执行了。这种情况也很常见,例如,一个检测用户权限的middleware可以决定是否继续处理请求,还是直接返回403错误:
app.use(async (ctx, next) => {
if (await checkUserPermission(ctx)) {
await next();
} else {
ctx.response.status = 403;
}
});
理解了middleware,我们就已经会用koa了!
最后注意ctx
对象有一些简写的方法,例如:
-
ctx.url
相当于ctx.request.url
-
ctx.type
相当于ctx.response.type
设置
应用程序设置是 app 实例上的属性,目前支持如下:
-
app.env
默认是NODE_ENV
或 "development" -
app.proxy
当真正的代理头字段将被信任时 -
app.subdomainOffset
对于要忽略的.subdomains
偏移[2]
app.listen(...)
Koa 应用程序不是 HTTP 服务器的1对1展现。 可以将一个或多个 Koa 应用程序安装在一起以形成具有单个HTTP服务器的更大应用程序。
创建并返回 HTTP 服务器,将给定的参数传递给 Server#listen()。这些内容都记录在 nodejs.org.
以下是一个无作用的 Koa 应用程序被绑定到 3000 端口:
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
这里的 app.listen(...) 方法只是以下方法的语法糖:
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
这意味着您可以将同一个应用程序同时作为 HTTP 和 HTTPS 或多个地址:
const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001);
app.callback()
返回适用于 http.createServer() 方法的回调函数来处理请求。你也可以使用此回调函数将 koa 应用程序挂载到 Connect/Express 应用程序中。
app.use(function)
将给定的中间件方法添加到此应用程序。
app.keys=
设置签名的 Cookie
密钥。
这些被传递给 KeyGrip
,但是你也可以传递你自己的 KeyGrip
实例。
例如,以下是可以接受的:
app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');
这些密钥可以倒换,并在使用 { signed: true }
参数签名 Cookie
时使用。
ctx.cookies.set('name', 'tobi', { signed: true });
app.context
app.context
是从其创建 ctx
的原型。您可以通过编辑 app.context
为 ctx
添加其他属性。这对于将 ctx
添加到整个应用程序中使用的属性或方法非常有用,这可能会更加有效(不需要中间件)和/或 更简单(更少的 require()
),而更多地依赖于ctx
,这可以被认为是一种反模式。
例如,要从 ctx
添加对数据库的引用:
app.context.db = db();
app.use(async ctx => {
console.log(ctx.db);
});
注意:
-
ctx
上的许多属性都是使用getter
,setter
和Object.defineProperty()
定义的。你只能通过在app.context
上使用Object.defineProperty()
来编辑这些属性(不推荐) - 安装的应用程序目前使用其父级的 ctx 和设置。 因此,安装的应用程序只是一组中间件。
错误处理
默认情况下,将所有错误输出到 stderr
,除非 app.silent
为 true
。 当 err.status
是 404
或 err.expose
是 true
时默认错误处理程序也不会输出错误。 要执行自定义错误处理逻辑,如集中式日志记录,您可以添加一个 “error” 事件侦听器:
app.on('error', err => {
log.error('server error', err)
});
如果 req/res 期间出现错误,并且 无法 响应客户端,Context实例仍然被传递:
app.on('error', (err, ctx) => {
log.error('server error', err, ctx)
});
当发生错误 并且 仍然可以响应客户端时,也没有数据被写入 socket 中,Koa 将用一个 500 “Internal Server Error (内部服务器错误)” 进行适当的响应。在任一情况下,为了记录目的,都会发出应用级 “错误”。
上下文(Context)
Koa Context
将 node
的 request
和 response
对象封装到单个对象中,为编写 Web
应用程序和 API
提供了许多有用的方法。 这些操作在 HTTP
服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。
每个请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符,如以下代码片段所示:
app.use(async ctx => {
ctx; // 这是 Context
ctx.request; // 这是 koa Request
ctx.response; // 这是 koa Response
});
为方便起见许多上下文的访问器和方法直接委托给它们的 ctx.request
或 ctx.response
,不然的话它们是相同的。 例如 ctx.type
和 ctx.length
委托给 response
对象,ctx.path
和ctx.method
委托给 request
。
API
Context 具体方法和访问器.
-
ctx.req Node 的 request 对象
-
ctx.res Node 的 response 对象
绕过 Koa 的 response处理是 不被支持的. 应避免使用以下 node 属性:**
- res.statusCode
- res.writeHead()
- res.write()
- res.end()
-
ctx.request koa 的 Request 对象.
-
ctx.response koa 的 Response 对象.
-
ctx.state 推荐的命名空间,用于通过中间件传递信息和你的前端视图。
ctx.state.user = await User.find(id);
-
ctx.app 应用程序实例引用
-
ctx.cookies.get(name, [options])
通过
options
获取cookie name
:-
signed
所请求的cookie
应该被签名 -
koa
使用cookies
模块,其中只需传递参数。基于Keygrip签名和未签名的Cookie
-
-
ctx.cookies.set(name, value, [options])
通过 options 设置 cookie name 的 value :
- maxAge 一个数字表示从 Date.now() 得到的毫秒数
- signed cookie 签名值
- expires cookie 过期的 Date
- path cookie 路径, 默认是'/'
- domain cookie 域名
- secure 安全 cookie
- httpOnly 服务器可访问 cookie, 默认是 true
- overwrite 一个布尔值,表示是否覆盖以前设置的同名的 cookie (默认是 false). 如果是 true, 在同一个请求中设置相同名称的所有
- Cookie(不管路径或域)是否在设置此Cookie 时从 Set-Cookie 标头中过滤掉。
-
ctx.throw([status], [msg], [properties])
Helper 方法抛出一个 .status 属性默认为 500 的错误,这将允许 Koa 做出适当地响应。
ctx.throw(400); ctx.throw(400, 'name required'); ctx.throw(400, 'name required', { user: user });
例如 ctx.throw(400, 'name required') 等效于:
const err = new Error('name required'); err.status = 400; err.expose = true; throw err;
请注意,这些是用户级错误,并用 err.expose 标记,这意味着消息适用于客户端响应,这通常不是错误消息的内容,因为您不想泄漏故障详细信息。
你可以根据需要将 properties 对象传递到错误中,对于装载上传给请求者的机器友好的错误是有用的。这用于修饰其人机友好型错误并向上游的请求者报告非常有用。
ctx.throw(401, 'access_denied', { user: user });
koa
使用http-errors
来创建错误。