运行环境
node 14.x 及以上版本
初体验
- 新建一个空文件
- 执行
npm i koa - 在根目录新建 app.js
// 引入koa
const Koa = require('koa')
// 创建koa实例
const app = new Koa()
// 创建一个中间件 所有的请求和响应都可以在这个中间件中处理
// ctx: context 是koa提供的上下文对象
app.use(ctx => {
// ctx.request 请求 ctx.response 响应
ctx.response.body = 'hello world'
})
// 启动服务器
app.listen(3000, () => {
console.log('服务运行在 http://localhost:3000');
})
- 在终端运行
node app.js - 在浏览器访问
http://localhost:3000
Koa中处理get请求
...
app.use(ctx => {
console.log(ctx.request.query);
console.log(ctx.request.querystring);
ctx.response.body = 'hello world'
})
...
- ctx.request.query 获取url中参数的对象形式
- ctx.request.querystring 获取url中参数的字符串形式
Koa中别名声明
许多 context 的访问器和方法为了便于访问和调用,简单的委托给他们的 ctx.request 和 ctx.response 所对应的等价方法, 比如说 ctx.type 和 ctx.length 代理了 response 对象中对应的方法,ctx.path 和 ctx.method 代理了 request 对象中对应的方法。
...
app.use(ctx => {
console.log(ctx.request.query);
console.log(ctx.request.querystring);
ctx.response.body = 'hello world'
})
// 等价于
app.use(ctx => {
console.log(ctx.query);
console.log(ctx.querystring);
ctx.body = 'hello world'
})
...
并不是request 或 response 所有的属性/方法都有别名 详情参照官网 www.koajs.com.cn/ 搜索Request aliases 或 Response aliases
Koa中处理post请求
监听data和end事件
...
app.use(ctx => {
// ctx.request ctx.response koa的请求和响应对象
// ctx.req ctx.res nodejs原生的请求和响应对象
let paramsStr = '' // 参数字符串
//(1)监听原生 nodejs request 对象的data事件
// 每有一段数据过来,都会触发一次data,数据量大,一次请求,会触发多次data事件
ctx.req.on('data', (data) => {
paramsStr += data
})
// (2) 监听 原生nodejs request对象的end事件
// 一旦所有数据接收完成,触发end事件
ctx.req.on('end', () => {
// name=zs&age=16
console.log(paramsStr)
})
ctx.response.body = 'hello world'
})
...
对post请求的字符串参数做处理
...
ctx.req.on('end', () => {
// console.log(paramsStr) // name=zs&age=16
const params = new URLSearchParams(paramsStr)
console.log(params); // URLSearchParams { 'name' => 'zs', 'age' => '16' }
// params.get(key) 根据键获取
// params.has(key) 根据键判断
// params.keys(key) 拿到所有的键 返回的是ES6 Iterator 可供for of 遍历
})
...
响应一个页面
const Koa = require('koa')
const app = new Koa()
const fs = require('fs')
const path = require('path')
// 封装一个读取html文件的工具函数 return promise对象
function getHTMLFile (filePath) {
return new Promise((reslove, reject) => {
fs.readFile(path.join(__dirname, filePath), (err, data) => {
if (err) reject(err)
// console.log(data) // 读取的数据格式,是Buffer格式 要用toString方法 转成可用的字符串
reslove(data.toString())
})
})
}
app.use(async ctx => {
ctx.body = await getHTMLFile('./test.html')
})
app.listen(3000, () => {
console.log('服务 http://localhost:3000');
})
处理静态资源
...
const fs = require('fs')
const path = require('path')
// 封装一个读取静态资源图片的函数
function getImageFile(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(path.join(__dirname, filePath), (err, data) => {
if (err) return reject(err)
resolve(data) // 这里保持Buffer格式,因为图片就是二进制数据,不需要转成字符串
})
})
}
app.use(async ctx => {
// 重要:需要正确的设置静态资源文件(图片)的Content-Type响应头,否则在浏览器中,只会下载文件,不能查看图片
ctx.set('Content-Type', 'image/png')
ctx.body = await getImageFile('./static/001.png')
})
...
中间件
中间件,是一系列用来对请求进行处理的函数
洋葱圈模型
在 Koa 中可以为 Koa 实例添加多个中间件,让一个请求可以经过多个中间件函数的处理。
中间件函数的格式为:
function someMiddleware(ctx, next) {
// ... 对请求进行处理的逻辑 ...
}
app.use(someMiddleware)
每个中间件函数都有两个参数:
- ctx - 请求上下文对象,它包含和请求和响应相关的数据和操作
- next - 是一个函数,调用后会执行下一个中间件
创建好中间件函数后,可以将它添加到 Koa 实例上
Koa 中间件的执行模型
上面为 Koa 框架的中间件模型示意图,看上去像个洋葱,所以我们称它为《洋葱圈模型》。
它的含义就是说:
Koa 中间件的执行就像洋葱一样,最早被 use 的中间件会放在最外层,而后续被 use 的中间件会往里层放;
当接收到一个请求的时候,处理顺序是:
- 从左到右从洋葱的最外层到最里层,也就是从最早 use 的中间件到最后 use 的中间件依次执行;
- 在到达最里层中间件后,会继续向右逐层往外执行,直到最外层中间件,然后返回 response
示例代码:
app.use(async (ctx, next) => {
console.log('>>>>>>>> A 111')
await next()
console.log('>>>>>>>> A 222')
})
app.use(async (ctx, next) => {
console.log('>>>>>>>> B 111')
await next()
console.log('>>>>>>>> B 222')
})
当接收请求后,我们可以看到控制台打印如下结果:
>>>>>>>> A 111
>>>>>>>> B 111
>>>>>>>> B 222
>>>>>>>> A 222
由此可知,一般情况下 Koa 的中间件都会执行两次:
- 调用 next 之前为第一次。在调用 next 后,会把控制权传递给往里层的下一个中间件
- 当里层不再有任何中间件、或未调用 next 函数时,就开始依次往外层中间件执行,执行的是外层中间件中调用 next 函数之后的代码
Koa洋葱圈设计理解
两个现象:
- 中间件的执行了两次
- 执行顺序奇怪,以next函数为分界点:先use的中间件,next前的逻辑先执行,但next后的逻辑反而后执行
思考: 为什么 Koa 要这么设计? 正常不应该是中间件按顺序从开始到结束执行吗?
说明:
-
如果说使用中间件的场景, 不存在前后依赖的情况,从头到尾按顺序链式调用 => 完全没问题。
-
但是, 如何存在依赖的情况呢? 比如: 前一个中间件部分代码, 依赖于下一个中间件的处理结果?
链式一次执行就无法实现了!
结论:
- next 前的逻辑 : 进行前期处理
- 调用next,将控制流交给下个中间件,并await其完成,直到后面没有中间件或者没有next函数执行为止
- 完成后一层层回溯执行各个中间件的后期处理(next 后的逻辑)
用 koa-body 处理 POST 传参
我们自己来处理获取 POST 请求的参数比较繁琐,实际开发中可以使用封装好的开源中间件
我们可以在 Koa 官方wiki 上找到很多优秀的开源中间件。而 koa-body 是一个专门用于获取通过请求体传递的数据的中间件。
使用步骤:
- 安装依赖包
npm i koa-body
- 引入并使用 koa-body
// 引入 koa-body
const koaBody = require('koa-body')
// ...
// 为 Koa 实例设置 koa-body 中间件
app.use(koaBody())
app.use(async ctx => {
// 通过 ctx.request.body 获取请求体参数
console.log('请求体参数', ctx.request.body) // 请求体参数: { name: 'zs', age: '16' }
ctx.body = 'Hello'
})
用 koa-views 和 EJS 渲染页面
在 Koa 中,我们也可以使用模板引擎,通过模板语法将数据动态渲染成html页面内容,然后发送到客户端去。
koa-views 是一个支持使用多种模板引擎来渲染页面的中间件
使用步骤:
- 安装 koa-views 中间件 和 ejs 模板引擎
npm i koa-views ejs
- 引入并使用中间件
// 引入 koa-views
var views = require('koa-views');
// 配置和应用 koa-views 中间件(这里配置使用了 ejs 模板引擎,以及模板文件的存放目录)
// views方法:
// 参数1:配置模板存放的目录;
// 参数2:配置项,可以配置后缀名
app.use(views(__dirname + '/views', { extension: 'ejs' }))
// 引入 koa-views 后,就可以使用 ctx.render 函数渲染 ejs 模板文件了
app.use(async ctx => {
// render 函数的第一个参数是模板文件名;第二个参数是要渲染到模板中的动态数据
await ctx.render('test', {
name: '小明',
age: 18,
books: ['三国演义', '红楼梦', '西游记', '水浒传']
})
})
模板文件 views/test.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>姓名:<%= name %></div>
<div>年龄:<%= age %></div>
<ul>
<% books.forEach(function (book) { %>
<li><%= book %></li>
<% }) %>
</ul>
</body>
</html>
用koa-static处理静态资源
安装依赖包
npm i koa-static
使用
...
const koaStatic = require('koa-static')
// public 目录 直接开放给用户(用户可以直接通过路径,访问到文件夹内的所有文件)
app.use(koaStatic('./public'))
...
示例
使用koa-body 和 koa-static做登录和上传文件的接口
...
const koaStatic = require('koa-static')
app.use(koaStatic('./public'))
const koaBody = require('koa-body')
// app.use(koaBody()) 对普通的post请求,可以进行参数处理 但是不好处理图片(没有配全参数)
app.use(koaBody({
// 开启文件上传支持
multipart: true,
// 配置文件上传相关
formidable: {
// 文件上传目录
uploadDir: './public/upload',
// 默认不保留后缀名 需要保留后缀名
keepExtensions: true
}
}))
// 编写接口 测试
app.use(ctx => {
// ctx.method ctx.url
// 1. 登录接口
if (ctx.method === 'POST' && ctx.url === '/login') {
console.log('进行登录');
console.log(ctx.request.body);
const { username, password } = ctx.request.body
if (username === 'admin' && password === '123456') {
ctx.body = {
status: 200,
msg: '登录成功'
}
} else {
ctx.body = {
status: 201,
msg: '登录失败'
}
}
}
// 2. 上传文件
if (ctx.method === 'POST' && ctx.url === '/upload') {
console.log('进行了文件上传');
console.log(ctx.request.files.photo.path.replace('public', ''));
ctx.body = {
status: 200,
msg: '上传文件成功',
imgUrl: ctx.request.files.photo.path.replace('public', '')
}
}
})
...
用 koa-router 实现后端路由
自己实现不同的路由示例:
const Koa = require('koa')
const app = new Koa()
app.use(async ctx => {
// 获取客户端请求的 URL 路径
const url = ctx.url
const method = ctx.method
// 根据路径来判断具体要做的业务逻辑
if (method === 'GET' && url === '/login') {
ctx.body = '这是登录页'
} else if (method === 'POST' && url === '/login') {
ctx.body = '登录处理成功'
} else if (method === 'GET' && url === '/register') {
ctx.body = '这是注册页'
} else if (method === 'POST' && url === '/register') {
ctx.body = '注册处理成功'
} else {
ctx.body = '404 Not Found'
}
})
app.listen(3000, () => {
console.log('请访问 http://localhost:3000')
})
以上做法的弊端是:随着路由路径的增加,中间件代码变得很复杂。
而借助 koa-router 中间件,可以很清晰的创建和管理多个路由。
使用步骤:
- 安装依赖包
npm i @koa/router
- 引入 koa-router 并创建 Router 实例
// 引入 koa-router
const Router = require('@koa/router')
// 创建 Router 实例
const router = new Router()
- 在 Router 实例上创建路由处理器
// 静态路由 GET /login
router.get('/login', async ctx => {
ctx.body = '这是登录页'
})
// 静态路由 POST /login
router.post('/login', async ctx => {
ctx.body = '登录处理成功'
})
// 动态路由 GET /articles/123
router.get('/articles/:id', async ctx => {
const id = ctx.params.id
console.log(">>>>>>>> 动态路由参数 ID:", id);
ctx.body = `ID 为 ${id} 的内容`
})
- 根据 router 生成路由相关的实际中间件函数,并设置给 Koa 实例
app.use(router.routes())