Koajs 快速笔记 (废除)
官方地址: koajs.com
整理于: 2023年4月14日
简介
Koa 是 Express 团队开发的另一个 web 框架. 目的是为了让其更小, 更高效, 表现力更丰富.
Koa 基于 async
开发, 可以减少使用回调, 以减少错误.
Koa 本身未绑定其他中间件, 它提供了一个优雅的方式来进行扩展.
安装
基本要求: Node v12+
, 目的是为了支持 async
和 ES2015
Appliction
一个 Koa 应用程序就是一个对象, 它包含一组中间件函数, 这些中间件函数构成一个栈结构, 在请求中被依次执行. Koa 与其他中间件系统类似, 例如 Ruby 的 Rack, Connect, 等. 其中关键是在底层的中间件上提供高级应用的语法糖. 从而优化交互性, 健壮性, 并使得创建中间件更加容易.
它提供了通用任务的方法, 例如 内容协商 (content negotiation), 缓存刷新 (cache freshness), 代理支持 (proxy support), 以及重定向到其他地方等. 尽管提供了大量有用的方法, 但是由于 Koa 没有绑定中间件, 它依旧很小.
经典的 Hello World 应用.
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
级联 (Cascading)
Koa 中间件的级联模式更为传统, 你可能已经熟悉了该模式. 相比较 node 的回调模式它更加友好. 然后使用 async
函数, 我们可以使用真正的中间件. 不同于 Connect 的实现, 它只是简单的在一些列函数中传递控制, 直到某个个函数返回. 而 Koa 则是执行下游函数, 然后在下游函数结束后回到上游函数.
下面的案例响应 "Hello World", 然而, 第一个请求通过 x-response-time
和 logging
中间件, 来标记请求开始. 然后通过响应中间件. 当中间件中调用 next()
时, 当前函数会被挂起, 并将控制前交给下一个中间件. 直到后续没有中间件调用时, 又会依次返回到上游中间件.
洋葱模型, 或栈模型, 亦或 ASP.NET Core 的管道模型.
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// 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`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
设置 (settings)
应用程序 (Application) 的设置即是 app 的属性. 现在所支持的如下:
app.env
默认为NODE_ENV
或"development"
.app.keys
为签名过的cookie
键的数组 (array of signed cookie keys).app.proxy
设置为true
时, 允许使用proxy
头.app.subdomainOffset
需要忽略的.subdomain
的偏移量, 默认为2
.app.proxyIpHeader
就是 proxy id header. 默认值为X-Forwarded-For
.app.maxIpsCount
从 proxy ip header 读取的最大 ip 数, 默认为 0, 即不限制.
这些属性不晓得怎么用
可以在构造器中提供参数来设置:
const Koa = require('koa')
const app = new Koa({ proxy: true })
或动态的设置
const Koa = require('koa')
const app = new Koa()
app.proxy = true
app.listen(...)
Koa 应用与 HTTP 服务不是一一对应关系. 可以将多个 Koa 应用挂载到一起构成一个强大的应用, 并使用单个 HTTP 服务器来提供服务.
创建并返回 HTTP 服务器, 传入参数来设置监听的端口. 该参数基于 node 文档. 下面让 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(3000)
app.callback()
该方法返回一个回调函数 (就是返回一个新函数), 然后挂载到 http.createServer() 中来处理请求. 也可以使用该方法在 Connect 或 Express 中挂载 Koa 应用.
app.use(function)
添加给定的中间件函数. app.use()
方法返回 this
, 即表示它允许链式编程:
app.use(someMiddleware)
app.use(someOtherMiddleware)
app.listen(3000)
也可以写成:
app.use(someMiddleware)
.use(someOtherMiddleware)
.listen(3000)
参考 中间件 来了解更多细节.
jk: 这个需要重点整理.
app.keys =
设置签名的 cookie key.
这里使用 KeyGrip. 因此这里需要传入你的 KeyGrip 实例. 例如:
app.keys = [
'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',
'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7'
];
app.keys = new KeyGrip([
'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',
'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7'
],
'sha256'
);
为了保证安全, 请确保 key 足够的长, 以及具有随机性.
这些键可以被旋转, 在需要给 cookie 进行签名时, 使用 { signed: true }
选项:
ctx.cookies.set('name', 'tobi', { signed: true })
jk: 旋转是啥??? 应该是某种术语.
app.context
app.context
是创建 ctx
的原型. 你可以通过给 app.context
添加附加属性来为 ctx
添加成员. 这种为 ctx
添加属性与方法的办法非常有用, 并且可以在全应用范围被访问到. 这种方式可能更高效 (没有中间件), 或更容易 (少量的 require()
) 使用, 但更加依赖于 ctx
. 这种方式是被公认的反模式 (anti-pattern
).
例如, 添加一个从 ctx
访问数据库的引用:
app.context.db = db();
app.use(async ctx => {
console.log(ctx.db);
});
注意:
- 大多数在
ctx
上的属性都使用getter
,setter
, 或Object.defineProperty()
来定义. 你只能在app.context
上使用Object.defineProperty()
来编辑这些属性, 但不推荐这么处理. 详细参考: https://github.com/koajs/koa/issues/652 - 挂载的应用程序目前使用的是父级的
ctx
和 设置 (setting). 因此, 挂载的应用程序本质上是一组中间件.
错误处理 (Error Handling)
默认情况下, 所有的错误会在标准错误流中输出 (stderr), 除非将 app.silent
设置为 true
. 当 err.status
为 404
或者 err.expose
为 true
时, 默认的错误处理程序也不会输出错误. 去执行自定义的错误处理逻辑时, 例如中心化日志程序, 你可以添加 "error"
事件监听器 (listener
):
app.on('error', err => {
log.error('server error', err)
});
如果错误在 req/res 流程中, 并且它不应该返回到客户端, 也可以传入 Context
实例.
app.on('error', (err, ctx) => {
console.log('server error', err, ctx);
});
当出现错误时, 并需要将错误返回给客户端时. 也就是说没有数据写入连接, Koa 会在适当的时候返回 500 "服务器内部错误". 在任何一种情况下, 都会发出一个 应用级别错误来用于日志记录.
Context
Koa 的 Context
封装了 request
和 response
对象, 并提供许多有用的方法, 来编写 web 应用和 API. 这些方法在服务端开发中使用的非常频繁, 因此将其加入到该级别中.
Context
实例伴随请求而创建. 并且它在中间件中以参数的形式进行提供, 或以 ctx
标识符的形式进行访问, 例如下面代码片段:
app.use(async ctx => {
ctx; // 即是 Context
ctx.request; // 是一个 Koa Request
ctx.response; // 是一个 Koa Response
});
有很多 ctx
上的方法与属性会被代理到 request
或 response
上. 例如:
ctx.method
,ctx.path
会被代理到ctx.request
上ctx.type
,ctx.length
则是代理到ctx.response
上
API
Context
下的方法与访问器
ctx.req
Node 的 request 对象
ctx.res
Node 的 response 对象
不支持绕过 Koa 来处理响应, 避免使用下面的 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
Application 实例的引用.
ctx.app.emit
Koa 应用程序扩展了内部事件发射器. ctx.app.emit
使用一个 type 来发射事件, 类型 type 定义在第一个参数中. 针对每一个事件, 你都可以使用 "listeners" 来进行捕获, 它是一个方法, 在事件触发的时候被调用. 可以参考错误处理)来了解详细信息.
经测试:
app
与ctx.app
是一个对象.
参考代码:
import Koa from 'koa'
import dayjs from 'dayjs'
const app = new Koa()
app.on('custom-event', e => {
console.log('custom-event', e);
})
app.use(async ctx => {
console.log('app == ctx.app', app == ctx.app)
ctx.app.emit('custom-event', { name: 'jk' })
ctx.body = 'Hello Koa - ' + dayjs().format('YYYY-MM-DD HH:mm:ss')
})
app.listen(3000)
ctx.cookies.get(name, [options])
使用 options
来获得名为 name
的 cookie 值.
options
的可取值为: signed
, 它表示 cookie 需要被签名.
Koa 使用 Cookies 模块来处理 cookie
ctx.cookies.set(name, value, [options])
设置 cookie 值. 可用选项有:
maxAge
, 从当前Date.now()
开始到过期的毫秒数 (数字).expires
, 表示过期时间, 是一个Date
对象.path
, 默认为'/'
.domain
secure
, 默认为false
, 表示是否使用https
.httpOnly
, 默认为true
.sameSite
, 默认值为false
, 可取值为:strict
,lax
,none
, 或true
signed
, 默认为false
. 若为true
会连同一个签名文件一并发送.overwrite
, 默认为false
, 表示前一个 cookie 是否可以被后面的同名 cookie 覆写.
ctx.throw([status], [msg], [properties])
帮助方法, 抛出错误状态码. 默认为 500. 例如:
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
所标记. 实际上的运行时异常并非这样, 运行时的错误不应该返回给客户端.
可以传入一个对象, 对象会合并到错误中.
Koa 使用 http-errors 来创建错误.
错误的数据与消息, 使用
app.on('error', e => ...)
来捕获
ctx.assert(value, [status], [msg], [properties])
辅助方法, 在 value
为假的时候, 逻辑与 throw
一样. 在 value
为真时, 逻辑上相当于 assert
.
Koa 内部使用的 http-assert
ctx.respond
如果需要绕过 Koa 内置的响应处理程序, 你可以显式的设置 ctx.respond = true
. 如果你想使用原始的 res
对象来代替 Koa
内置的 response
, 可以这么使用.
注意, 不建议使用该方式, 会在中间件等模块中出现问题.
请求别名
下列访问器与别名映射到 Request
中
ctx.header
ctx.headers
ctx.method
ctx.method=
ctx.url
ctx.url=
ctx.originalUrl
ctx.origin
ctx.href
ctx.path
ctx.path=
ctx.query
ctx.query=
ctx.querystring
ctx.querystring=
ctx.host
ctx.hostname
ctx.fresh
ctx.stale
ctx.socket
ctx.protocol
ctx.secure
ctx.ip
ctx.ips
ctx.subdomains
ctx.is()
ctx.accepts()
ctx.acceptsEncodings()
ctx.acceptsCharsets()
ctx.acceptsLanguages()
ctx.get()
响应别名
下列访问器映射到 Response
中:
ctx.body
ctx.body=
ctx.status
ctx.status=
ctx.message
ctx.message=
ctx.length=
ctx.length
ctx.type=
ctx.type
ctx.headerSent
ctx.redirect()
ctx.attachment()
ctx.set()
ctx.append()
ctx.remove()
ctx.lastModified=
ctx.etag=
Request
Koa 中 Request 对象
API
request.header
Request Header 对象. 与 node 中 http.IncomingMessage
的 headers
字段一样.
request.header=
设置 request header 对象
request.headers
Request Header 对象, 是 request.header
的别名.
代码中判等为
true
request.headers =
设置 request header 对象, 是 request.header =
的别名
request.method
请求方法
request.method =
设置请求方法. 实现例如 methodOverride()
的中间件很方便.
request.length
表示 Content-Length
的值, 如果有的话.
request.url
请求的 url
request.url =
设置 url, 常用与 url 重写
request.originalUrl
获得源 url
request.origin
获得 Url, 包含协议, 主机等.
request.href
全 url, 包括, 协议, 主机, 请求路径与参数.
request.path
获得请求路径
request.path =
设置请求路径. 但是不会修改 查询参数.
request.querystring
获得查询参数字符串. 不包含 ?
.
request.querystring =
使用原始字符串来设置查询参数.
request.search
获得查询参数字符串, 并带有 ?
request.search =
设置查询字符串.
request.host
获得 host
(hostname:port
). 在 app.proxy
为 true
时也支持 X-Forwarded-Host
request.hostname
获取 hostname
.
request.URL
转换后的 URL 对象
request.type
对应于 Context-Type
, 但不会带有参数, 例如 charset
.
request.charset
如果有, 获取字符集. 否则为 undefined
request.query
转换后的查询参数对象.
request.query =
设置参数对象, 注意不支持嵌套对象.
request.fresh
检查请求缓存是否更新. 也就是内容是否发生改变.
该方法在 If-None-Match/ETag
与 If-Modified-Since
和 Last-Modified
之间协商. 在设置一个或多个响应头后应该被引用.
细节还是
HTTP
.
// 响应状态码 20x 或 304
ctx.status = 200;
ctx.set('ETag', '123');
// 缓存 OK
if (ctx.fresh) {
ctx.status = 304;
return;
}
// 缓存失效, 更新数据
ctx.body = await db.find('...');