使用 Koa 启动一个 HTTP 服务
npm init -y
# 安装 koa
npm i koa
// app.js
const Koa = require('koa')
const app = new Koa()
// Koa 没有路由系统,只有中间件功能
// ctx:context 上下文对象,包含请求和响应相关方法
app.use(ctx => {
// 发送响应
ctx.body = 'Hello Koa'
})
app.listen(3000, () => {
console.log('server is running on http://localhost:3000')
})
nodemon ./app.js
Koa 本身不自带路由功能,如果想要对不同的请求路径进行分发处理,需要自己手动编码处理。
app.use(ctx => {
const path = ctx.path
console.log(path);
if (path === '/') {
ctx.body = 'home page'
} else if (path === '/foo') {
ctx.body = 'foo page'
} else {
ctx.body = '404 Not Found.'
}
})
实际上 Koa 将一些功能单独封装成 npm 包,其中包括路由功能。
在 Koa.js (github.com) 中搜索 route
仓库可以找到几个官方提供的路由工具包,推荐使用 koajs/router,它使用 Express 风格的方法(app.get
、app.post
等),详情参考API 文档。
# 安装
npm i @koa/router
// app.js
const Koa = require('koa')
const Router = require('@koa/router')
const app = new Koa()
const router = new Router()
router.get('/', ctx => {
ctx.body = 'home page'
})
router.post('/', ctx => {
ctx.body = 'post /'
})
router.get('/foo', ctx => {
ctx.body = 'foo page'
})
router.get('/bar', ctx => {
// 重定向
ctx.redirect('/foo')
})
router.get('/users/:id', ctx => {
ctx.body = ctx.params
})
app
// 挂载路由配置
.use(router.routes())
// 配置允许的请求方法:OPTIONS 请求的 Allow 请求头
.use(router.allowedMethods())
app.listen(3000, () => {
console.log('server is running on http://localhost:3000')
})
koajs/static 模块封装了静态资源托管的功能。
npm i koa-static
// app.js
const Koa = require('koa')
const static = require('koa-static')
const path = require('path')
const app = new Koa()
// http://localhost:3000/css/style.css 访问 /public/css/style.css 文件
app.use(static(path.join(__dirname, './public')))
app.listen(3000, () => {
console.log('server is running on http://localhost:3000')
})
Koa 的中间件不能通过第一个参数设置请求路径,通过下面的方式给静态资源目录添加虚拟路径会报错:
app.use('/public', static(path.join(__dirname, './public')))
可以使用 Koa 的另一个模块 koajs/mount 将 Koa 应用或中间件装载到指定路径:
npm i koa-mount
// mount(<路径>, <中间件>)
app.use(mount('/public', static(path.join(__dirname, './public'))))
Koa 最大的特色和 Express 一样,也是完全基于中间件(middleware)构建的 web 框架。
Koa 中间件和 Express 中间件有很大的区别,Koa 中间件完全基于洋葱模型(如图):
next
函数的时候,会把执行权交给下一个中间件下面通过代码查看中间件的栈结构:
const one = (ctx, next) => {
console.log('>> one')
next()
console.log('<< one')
}
const two = (ctx, next) => {
console.log('>> two')
next()
console.log('<< two')
}
const three = (ctx, next) => {
console.log('>> three')
next()
console.log('<< three')
}
app.use(one)
app.use(two)
app.use(three)
打印结果:
>> one
>> two
>> three
<< three
<< two
<< one
如果中间件内部没有调用 next
函数,那么执行权就不会传递下去。例如将 two
函数中的 next()
注释,打印结果:
>> one
>> two
<< two
<< one
迄今为止,所有例子的中间件都是同步的,不包含异步操作。如果有异步操作(比如读取数据库、读取文件),中间件就必须写成 async 函数:
const Koa = require('koa')
const path = require('path')
const fsPromises = require('fs').promises
const app = new Koa()
app.use(async (ctx, next) => {
const data = await fsPromises.readFile(path.join(__dirname, './views/index.html'))
// 设置 content-type 为 text/html
// 或者在读取文件的时候设置编码为 `utf8`
ctx.type = 'html'
ctx.body = data
next()
})
app.listen(3000, () => {
console.log('server is running on http://localhost:3000')
})
Koa 默认只能一个一个挂载中间件:
app.use(one)
app.use(two)
app.use(three)
可以使用中间件组合工具 koajs/compose,将多个中间件合并成同一个中间件去挂载。
npm i koa-compose
app.use(compose([one, two, three]))
下面是捕获并手动响应错误的中间件:
app.use(ctx => {
try {
JSON.parse('string')
ctx.body = 'Hello Koa'
} catch (error) {
ctx.response.status = 500
ctx.response.body = '服务端内部错误'
}
})
也可以使用 Koa 提供的快捷方法 ctx.throw
来响应错误:
app.use(ctx => {
try {
JSON.parse('string')
ctx.body = 'Hello Koa'
} catch (error) {
// ctx.response.status = 500
// ctx.response.body = '服务端内部错误'
// ctx.throw(500) // body => Internal Server Error
ctx.throw(404) // body => Not Found
}
})
上例是对单个中间件的错误捕获处理,如果要统一的处理程序中的异常,就要利用洋葱中间件的模型,将捕获错误的代码写在最外层:
const Koa = require('koa')
const app = new Koa()
// 最外层添加异常捕获的中间件
app.use((ctx, next) => {
try {
next()
} catch (error) {
ctx.response.status = 500
ctx.response.body = error.message
}
})
app.use(ctx => {
JSON.parse('string')
ctx.body = 'Hello Koa'
})
...
可以看到 Koa 处理错误的方式相比 Express 方便一些:不需要在每个中间件中 try-catch
捕获错误并将错误传递给 next
。
注意上例使用的都是同步中间件,如果是异步中间件就要在 try-catch
中使用 await
:
// 最外层添加异常捕获的中间件
app.use(async (ctx, next) => {
try {
// 使用 await 等待下一个中间件处理完成
await next()
} catch (error) {
ctx.response.status = 500
ctx.response.body = error.message
}
})
// 同步中间件:A
app.use((ctx, next) => {
next()
})
// 异步中间件:B
app.use(async ctx => {
// 读取不存在的文件
await fsPromises.readFile('file.txt')
})
但是这样不会捕获中间件 B 的错误,因为上层的中间件 A 是同步的,并未等待下层中间件的结果,最外层的错误处理中间件 await
的是中间件 A 的执行结果。
解决办法有:
// 方案一,同步中间件返回下层中间件的执行结果
// 同步中间件:A
app.use((ctx, next) => {
return next()
})
// 方案二,将同步中间件改为异步中间件,await next()
// 同步中间件:A
app.use(async (ctx, next) => {
await next()
})
这属于 JS 的 Promise 语法问题,与 Koa 和中间件无关。
建议:所有的中间件都使用 asycn
异步函数,使用 await
等待 next()
。
除了使用外层中间件的方式捕获错误,Koa 还可以通过监听 error
事件捕获错误:
// 可以在任意位置监听事件
app.on('error', err => {
console.log(err)
})
**注意:**使用外层中间件 try-catch
捕获并处理了错误,并不会触发 error
事件。
如果既要在中间件中处理异常,又要触发 error
事件,可以在中间件中手动触发 error
事件:
// 最外层添加异常捕获的中间件
app.use(async (ctx, next) => {
try {
await next()
} catch (error) {
ctx.response.status = 500
ctx.response.body = error.message
// 触发 error 事件
ctx.app.emit('error', error, ctx)
}
})
实际项目中,使用二者之一即可(建议使用中间件)。