jk's notes
  • Koajs 快速笔记 (废除)

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('...');
Last Updated:
Contributors: jk