本教程代码仓库请访问:
https://github.com/chencl1986/koa2-tutorial
Koa与Express的区别
Express是基于回调函数开发。
Koa是基于Promise思想开发。
Koa1基于Generator,Koa2同时支持Generator和Async/await,但使用Generator会收到警告,因为Koa3是完全基于Async/await。
使用Koa创建一个服务器
const Koa = require('koa')
const server = new Koa() // 使用new创建一个Server
server.listen(8080) // 监听8080端口
1
2
3
4
5
引入Koa路由
请查看server01.js
与Express不同,Koa并不自带路由,需要引入koa-router模块才可以使用路由。
/**
* 代码请查看:/server01.js
*/
const router = new Router()
// router常用方法有get(匹配get请求)、post(匹配post请求)、all(匹配所有请求)
// ctx为上下文对象,其中包括原生nodejs包含的request/response对象,分别为ctx.req/ctx.res。
// next函数被调用时,会暂停该中间件的运行,并将控制传递给下一个中间件。
// 判断以get方法请求的路由
router.get('/a', async (ctx, next) => {
console.log(ctx)
ctx.body = 'aaa' // 通过body属性向前台传输数据
ctx.body += 'bbb' // body属性的值可以添加或替换,与普通对象的属性相同。
})
server.use(router.routes()) // 使用use方法,将router中间件添加到Server中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当然在项目中,我们往往会有多级路由,这时候就需要创建嵌套路由。
嵌套路由
请查看server01.js
通过路由嵌套,实现多级路由访问。
/**
* 代码请查看:/server01.js
*/
// 嵌套路由
const router = new Router() // 创建一个主路由
const userRouter = new Router() // 创建一个路由
const companyRouter = new Router()
companyRouter.get('/a', async (ctx, next) => { // 匹配路由后将输出相应内容
ctx.body = '企业的a'
})
userRouter.use('/company', companyRouter.routes()) // 将company路由添加到父级,表示当访问/company的子路由时,匹配company下的路由。
userRouter.get('/', async (ctx, next) => {
ctx.body = 'user'
})
userRouter.get('/company', async (ctx, next) => {
ctx.body = '企业'
})
router.use('/user', userRouter.routes()) // 将userRouter添加到主路由router中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
此时当浏览器访问不同路由时,显示相应内容:
访问路径 显示内容
/user user
/user/company 企业
/user/company/a 企业的a
Koa之所以选择这样的嵌套方式,更多的原因是为了支持大型项目。在大型项目中,若采用配置JSON的方式配置路由,会因为代码量过大造成阅读困难。
当然,若采用当前的配置方式,将所有路由都配置在一个文件中,对代码阅读毫无帮助,因此我们需要将每个层级的路由都配置到单独文件中,通过互相引用模块的方式,实现路由配置。
路由嵌套时,最终访问的路径是将各层级的路径,按字符串拼接起来的,因此需要注意不要出现拼接错误,例如出现’//user’。
’’ 空字符串代表根路由
’’ 字符串代表匹配所有路由
模块化嵌套路由
请查看server02.js
先在入口js文件中,将根节点的路由添加到server中。
server.use(require('./routers')) // 使用根路由配置
1
在根路由的配置中,引用下级路由的配置,以此类推。
const Router = require('koa-router')
const router = new Router()
// 此时require('./user')实际为router.routes(),为一个中间件,需要使用router.use
router.use('/user', require('./user'))
router.get('/user', async (ctx, next) => {
ctx.body = 'user'
})
module.exports = router.routes()
1
2
3
4
5
6
7
8
9
10
11
12
这样,我们就完成了整个项目的路由配置。
路由参数
请查看server03.js
在Koa中,支持通过路由传参。
router.get('/news/:id', async (ctx, next) => {
console.log(ctx.params) // 通过params属性获取参数
ctx.body = `新闻ID为:${ctx.params.id}`
})
1
2
3
4
同时也可以进行多级传参,此时需要注意的是,只有当传入参数数量与路由数量相同时,才可以正确匹配,同时参数的顺序是固定的。
router.get('/news/:id1/:id2/:id3', async (ctx, next) => {
console.log(ctx.params)
ctx.body = `多级新闻ID为:${ctx.params.id1}/${ctx.params.id2}/${ctx.params.id3}`
})
1
2
3
4
假设同时匹配了两层同样的路由,则以先匹配的路由为准,后一级路由不会执行。
router.get('/news/:id') // 只会执行该路由匹配。
router.get('/news/1')
1
2
如果希望匹配完第一级路由之后,还可以匹配第二级,则可以通过执行next(),将方法传入下一级,但因为此时next()返回值为Promise,则需要使用await。
// 若不执行next,则只会匹配该路由
router.get('/news/1', async (ctx, next) => {
console.log(ctx.params) // 通过params属性获取参数
ctx.body = `新闻ID为:固定的1`
await next() // 可通过next函数,执行下一级的匹配,同时因为此时next()返回值为Promise,则需要使用await。
})
router.get('/news/:id', async (ctx, next) => {
console.log(ctx.params) // 通过params属性获取参数
ctx.body = `新闻ID为:${ctx.params.id}`
await next()
})
router.get('/news/:key', async (ctx, next) => {
console.log(ctx.params) // 通过params属性获取参数
ctx.body = `新闻ID为:${ctx.params.key}`
await next()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Urlencoded传参与路由传参的区别
请查看server03.js
Urlencoded:http://localhost:8080/news/1/2/3/user/1/2/3
路由传参:http://localhost:8080/news?id1=1&id2=2&id3=3
Urlencoded传参 路由传参
通过ctx.query获取参数 通过ctx.params获取参数
顺序灵活 顺序固定
可省略 不可省略
动态地址,搜索引擎会认为只是一个地址,不利于SEO 静态地址,搜索引擎会判断为多个地址,利于SEO
参数之后不可挂载路由 参数之后可以挂载下级路由
context上下文
请查看server04.js
context是ctx的原型prototype,通常用来向整个项目添加全局属性或方法。
最经常的用法是添加全局数据库的连接池。
server.context.db = db();
server.use(async ctx => {
console.log(ctx.db);
});
1
2
3
4
5
添加一个全局属性并打印。
server.context.a = 123
router.get('/', async (ctx, next) => {
ctx.body = `abc${server.context.a}`
})
1
2
3
4
5
ctx.throw与ctx.assert
请查看server05.js
ctx.throw([status], [msg], [properties])
ctx.throw方法用于向前端抛出一个错误。
router.get('/login', async (ctx, next) => {
if (!ctx.query.user || !ctx.query.pass) {
ctx.throw(400, '用户名或密码不存在')
} else {
ctx.body = '成功'
}
})
1
2
3
4
5
6
7
ctx.assert(value, [status], [msg], [properties])
ctx.assert简单来说就是对throw的封装,第一个参数是触发条件。
等价于如下代码:
if (!value) {
ctx.throw(code, msg)
}
1
2
3
因此前一段throw的代码可以简写为:
router.get('/login', async (ctx, next) => {
ctx.assert(ctx.query.user, 400, '用户名不存在')
ctx.assert(ctx.query.pass, 400, '密码不存在')
ctx.body = '成功'
})
1
2
3
4
5
ctx.redirect
请查看server06.js
ctx.redirect用于重定向,可重定向到站内或站外,同时会向前端传一个302状态码。
router.get('/google', async (ctx, next) => {
ctx.redirect('https://www.google.com/')
})
router.get('/login', async (ctx, next) => {
ctx.redirect('/user')
})
1
2
3
4
5
6
7
使用koa-static处理静态文件
请查看server07.js
const Static = require('koa-static')
// 通过路由使用静态文件时,表示路由直接接受访问,所以需要用all方法。
server.use(Static('./static', { // 使用./static文件夹中的静态文件
maxage: 86400 * 1000, // 告知浏览器缓存时间
index: '1.html' // 当黄文根路由时,默认渲染的文件,前提是路由未匹配根路由
}))
1
2
3
4
5
6
7
使用路由处理静态文件
请查看server08.js
在项目开发中批量处理静态文件,可以使用路由匹配不同的文件类型,通过koa-static处理并添加不同缓存时间等参数。
const staticRouter = new Router()
staticRouter.all(/(\.jpg|\.png|\.gif)/, Static('./static', {
maxage: 60 * 86400 * 1000
}))
staticRouter.all(/(\.css)/, Static('./static', {
maxage: 1 * 86400 * 1000
}))
staticRouter.all(/(\.html|\.htm|\.shtml)/, Static('./static', {
maxage: 20 * 86400 * 1000
}))
staticRouter.all(/(\.js|\.jsx)/, Static('./static', {
maxage: 1 * 86400 * 1000
}))
// '' 空字符串代表根路由
// '*' 字符串*代表匹配所有路由
staticRouter.all('*', Static('./static', {
maxage: 30 * 86400 * 1000
}))
server.use(staticRouter.routes())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
koa-better-body中间件
请查看server09.js
koa-better-body中间件是用来解析post请求,包括数据和文件。
https://github.com/tunnckoCoreLabs/koa-better-body
新建一个post.html,用来上传表单:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
名字:<input type="text" name="user" /><br>
头像:<input type="file" name="f1" /><br>
<input type="submit" value="提交">
</form>
</body>
</html>