一、Koa洋葱模型
现象
- 中间件的执行两次
- 执行顺序以next函数为分界点:先use的中间件,next前的逻辑先执行,但next后的逻辑反而后执行
原理
Express的中间件是顺序执行,从第一个中间件执行到最后一个中间件,发出响应。
Koa是从第一个中间件开始执行,遇到await next()
就进入下一个中间件,一直到执行到最后一个中间件。然后再逆序执行上一个中间件await next()
后面的代码,一直到第一个中间件await next()
后面的代码执行完毕才发出响应。
koa把很多async函数组成一个处理链,每个async函数都可以做一些自己的事情,然后用await next()来调用下一个async函数。我们把每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。
app.use(async (ctx, next) => {
console.log('1');
await next(); // 调用下一个middleware
console.log('5')
});
app.use(async (ctx, next) => {
console.log('2');
await next(); // 调用下一个middleware
console.log('4');
});
app.use(async (ctx, next) => {
console.log('3');
});
输出结果: 12345
在这个例子里,通过输出结果可以看出三个中间件的执行顺序是:
中间件1 -> 中间件2 -> 中间件3 -> 中间件2 -> 中间件1
二、基本使用
npm init // 初始化package.json
cnpm install koa // 安装koa2
目录结构
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
// koa-bodyparser模块
const bodyParser = require('koa-bodyparser');
// art-template模板引擎
const views = require('koa-views');
app.use(views('views', { map: { html: 'ejs' } }));
// koa-static静态资源中间件
const static = require('koa-static');
app.use(bodyParser());
app.use(static(
path.join(__dirname, 'static')
))
// mysql数据库模块
const mysql = require('mysql')
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '666123',
database: 'vue'
});
connection.connect();
app.use(async (ctx, next) => {
console.log(new Date());
await next();
})
router.get('/', function (ctx, next) {
ctx.body = "Hello koa";
})
router.get('/news', (ctx, next) => {
// GET请求数据以及获取GET数据
console.log(ctx.query);
console.log(ctx.querystring);
console.log(ctx.request.query);
console.log(ctx.request.querystring);
ctx.body = "新闻页面"
});
router.get('/add', async (ctx) => {
let title = 'hello koa2'
await ctx.render('index', {
title
})
})
router.post('/api/add', async (ctx) => {
console.log(typeof ctx.request.body);
console.log(ctx.request.body);
ctx.set("Content-Type", "application/json");
ctx.body = ctx.request.body;
})
router.get("/api/list", (req, res) => {
let obj = {
ret: false,
total: 0,
rows: [],
msg: ""
}
const pageSize = Number(req.query.pageSize);
const pageNumber = Number(req.query.pageNumber) - 1;
connection.query(`SELECT * FROM blog`, (error, results, fields) => {
if (!error) {
obj.total = results.length;
// 分页查询 begin
connection.query(`SELECT * FROM blog limit ${pageNumber * pageSize}, ${pageSize}`, (error, results, fields) => {
if (!error) {
obj.ret = true;
obj.rows = results.map((value, key) => {
return {
id: value.id,
title: value.title,
content: value.content.toString(),
datetime: value.datetime
}
});
}
})
// 分页查询 end
}
})
})
app.use(router.routes()); //作用:启动路由
app.use(router.allowedMethods()); //作用: 当请求出错时的处理逻辑
app.listen(3000, () => {
console.log('starting at port 3000');
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<!-- <img src="/1.jpg"/> -->
<input type="input" id="content" name="content" /><input type="button" value="submit" id="submit-button" />
<div class="showmsg"></div>
<script>
document.getElementById("submit-button").onclick = function () {
const content = document.getElementById("content").value; // 获取文件对象
var xhr = new XMLHttpRequest();
xhr.open("post", "/api/add");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("content=" + content);
xhr.onreadystatechange = function () {
// 判断服务器是否响应,判断异步对象的响应状态
if (xhr.status == 200 && xhr.readyState == 4) {
document.querySelector(".showmsg").innerHTML = xhr.responseText;
}
}
}
</script>
</body>
</html>
1、art-template模板引擎
cnpm install --save art-template
cnpm install --save koa-art-template
const Koa = require('koa');
const render = require('koa-art-template');
const app = new Koa();
render(app, {
root: path.join(__dirname, 'view'),
extname: '.html',
debug: process.env.NODE_ENV !== 'production'
});
app.use(async function (ctx) {
await ctx.render('user');
});
app.listen(8080);
2、 静态资源托管
app.js:
const static = require("koa-static")
const mount = require("koa-mount") // 和koa-static配合使用
// 静态资源托管
app.use(
mount("/public", static(path.join(__dirname, "./static")))
)
index.html:
<div><img src="/public/1.jpg"/></div>
3、异步中间件
app.js:
const Router = require("koa-router")
const fs = require("fs")
const util = require("util")
const router = new Router();
router.get("/file", async (ctx, next) => {
const data = await util.promisify(fs.readFile)('./package.json')
ctx.type = "html"
ctx.body = data
})
4、重定向针对的是同步请求
router.get("/foo", async ctx => {
ctx.redirect("/add")
})
5、mysql数据库模块
const mysql = require('mysql')
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'react'
});
connection.connect();
router.get("/api/list", (req, res) => {
let obj = {
ret: false,
total: 0,
rows: [],
msg: ""
}
const pageSize = Number(req.query.pageSize);
const pageNumber = Number(req.query.pageNumber) - 1;
connection.query(`SELECT * FROM blog`, (error, results, fields) => {
if (!error) {
obj.total = results.length;
// 分页查询 begin
connection.query(`SELECT * FROM blog limit ${pageNumber * pageSize}, ${pageSize}`, (error, results, fields) => {
if (!error) {
obj.ret = true;
obj.rows = results.map((value, key) => {
return {
id: value.id,
title: value.title,
content: value.content.toString(),
datetime: value.datetime
}
});
}
})
// 分页查询 end
}
})
})
6、异常处理
const Koa = require('koa');
const { promisify } = require("util");
const fs = require("fs");
const app = new Koa();
app.use(async (ctx, next) => {
JSON.parse("{}");
ctx.body = "hello koa";
await next();
});
app.use(async (ctx, next) => {
const data = await promisify(fs.readFile)("./test.txt");
ctx.body = data;
})
app.on("error", err => {
console.log("error :", err);
})
app.listen(3000, () => {
console.log("http://localhost:3000");
});
ctx.throw()
koa 提供了 ctx.throw() 方法用来抛出错误,我们可以直接 ctx.throw(500,就是抛出500的错误,类似下面例子:
app.use(async (ctx,next) => {
ctx.throw(500);
});
值得注意的是,如果将 ctx.response.status 设置成对应的状态码,效果跟 ctx.throw() 是一样的,比如设置成 404。
app.use(async (ctx,next) => {
ctx.response.status = 404;
ctx.response.body = 'Page Not Found';
});
try…catch…
为了更加方便的处理错误,使用 try…catch… 将错误捕获是比较好的,但是每个请求都写感觉太麻烦,其实我们可以定义一个中间件负责对所有的错误进行处理:
const Koa = require('koa');
const { promisify } = require("util");
const fs = require("fs");
const app = new Koa();
// 要放在最前面
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = 500;
ctx.body = err.message;
}
});
app.use(async (ctx, next) => {
JSON.parse("{}");
ctx.body = "hello koa";
await next();
});
app.use(async (ctx, next) => {
const data = await promisify(fs.readFile)("./test.txt");
ctx.body = data;
})
app.listen(3000, () => {
console.log("http://localhost:3000");
});
监听 error 事件
其实,koa 还有一个 error 事件监听,如果代码运行发生错误,就会出发这个事件,我们也可以用它来处理错误。
app.on('error', (err, ctx) =>
console.error('server error', err);
);
有一点需要注意的是,如果错误已经被 try…catch 捕获,那么就不会出发 error 事件。这是就必须释放 error 事件,调用 ctx.app.emit() ,手动释放 error 事件,koa 监听函数才会生效,否则监听事件是不会触发的。
const Koa = require('koa');
const { promisify } = require("util");
const fs = require("fs");
const app = new Koa();
// 要放在最前面
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = 500;
ctx.body = err.message;
ctx.app.emit("error", err, ctx)
}
});
app.use(async (ctx, next) => {
JSON.parse("{}");
ctx.body = "hello koa";
await next();
});
app.use(async (ctx, next) => {
const data = await promisify(fs.readFile)("./test.txt");
ctx.body = data;
})
app.on("error", err => {
console.log("error :", err);
})
app.listen(3000, () => {
console.log("http://localhost:3000");
});
以上三种方式都可以。
7、跨域
cnpm i koa2-cors -S
const Koa = require("koa")
const cors = require('koa2-cors');
const app = new Koa()
app.use(cors())
允许跨域,并且允许附带cookie:
app.use(cors({ credentials: true })) //允许跨域,并且允许附带cookie
8、获取POST请求数据
前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div>
<input type="text" id="name" /><button id="btn">提交</button>
</div>
<script>
document.getElementById("btn").onclick = function () {
const name = document.getElementById("name").value;
console.log(name)
const url = "/api/user";
const xhr = new XMLHttpRequest();
xhr.open("post", url, true);
xhr.onload = function () {
alert("上传完成!");
};
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send('ck=bJNY&signature=intro');
// 接收数据
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(typeof JSON.parse(xhr.responseText))
console.log(JSON.parse(xhr.responseText));
} else {
console.log(xhr.statusText);
}
};
}
</script>
</body>
</html>
后端:
cnpm i koa-bodyparser -S
const Router = require("koa-router");
const router = new Router();
const bodyparser = require("koa-bodyparser")
router.use(bodyparser()) // app.use(bodyparser())也行
router.post("/login", async ctx => {
console.log(ctx.request.body);
ctx.body = ctx.request.body; // 返回JSON数据:{ ck: 'bJNY', signature: 'intro' }
})
9、获取GET请求数据
router.get("/list", async ctx => {
console.log(ctx.query); // 转换为对象格式
console.log(ctx.querystring); // 原始查询字符串
})
10、设置返回数据类型
ctx.set('Content-Type', 'text/html; chartset=utf-8');
router.get('/api/file', async (ctx, next) => {
const content = await promisify(fs.readFile)('./index.html');
ctx.set('Content-Type', 'text/html; chartset=utf-8');
ctx.body = content;
})
11、路由不单独文件、单独文件、路由拆分
1)路由不单独文件
const fs = require("fs")
const Koa = require("koa")
const Router = require("koa-router")
const { promisify } = require("util")
const bodyparser = require("koa-bodyparser")
const cors = require('koa2-cors');
const app = new Koa()
const router = new Router()
router.use(bodyparser())
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.body = err.message
// ctx.app.emit("error", err, ctx)
}
});
router.get("/api/data", async ctx => {
console.log("object");
ctx.body = {
status: 0,
data: "hello world!"
};
})
router.get('/news', async (ctx, next) => {
// GET请求数据以及获取GET数据
ctx.body = "新闻页面"
});
router.post('/api/login', async (ctx, next) => {
ctx.body = ctx.request.body;
});
router.get('/api/file', async (ctx, next) => {
const content = await promisify(fs.readFile)('./index.html');
ctx.set('Content-Type', 'text/html; chartset=utf-8');
ctx.body = content;
})
// app.use(async ctx => {
// const data = await promisify(fs.readFile)("./add.js")
// ctx.body = data
// })
app.on("error", err => {
console.log("error", err.toString())
})
app.use(router.routes()); //作用:启动路由
app.use(router.allowedMethods()); //作用: 当请求出错时的处理逻辑
app.listen(3000, () => {
console.log("start at port 3000!")
})
2)路由单独文件
first.js
const Router = require('koa-router')
const router = new Router()
router.get('/first/bar', async (ctx, next) => {
ctx.body = '/first/bar'
})
router.get('/first/foo', async (ctx, next) => {
ctx.body = '/first/foo'
})
module.exports = router
app.js
const fs = require("fs")
const Koa = require("koa")
const Router = require("koa-router")
const { promisify } = require("util")
const bodyparser = require("koa-bodyparser")
const cors = require('koa2-cors')
const firstRouter = require('./router/first')
const app = new Koa()
const router = new Router()
router.use(bodyparser())
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.body = err.message
// ctx.app.emit("error", err, ctx)
}
});
app.on("error", err => {
console.log("error", err.toString())
})
app.use(firstRouter.routes()).use(firstRouter.allowedMethods())
app.listen(3000, () => {
console.log("start at port 3000!")
})
3)路由拆分
list.js
const Router = require('koa-router')
const router = new Router()
router.get("/foo", async (ctx, next) => {
ctx.body = "/list/foo"
})
router.get('/bar', async (ctx, next) => {
ctx.body = '/list/bar'
})
module.exports = router
role.js
const Router = require('koa-router')
const router = new Router()
router.get('/bar', async (ctx, next) => {
ctx.body = '/role/bar'
})
router.get('/foo', async (ctx, next) => {
ctx.body = '/role/foo'
})
module.exports = router;
app.js
const fs = require("fs")
const Koa = require("koa")
const Router = require("koa-router")
const { promisify } = require("util")
const bodyparser = require("koa-bodyparser")
const cors = require('koa2-cors')
const listRouter = require('./router/list')
const roleRouter = require('./router/role')
const app = new Koa()
const router = new Router()
router.use(bodyparser())
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.body = err.message
// ctx.app.emit("error", err, ctx)
}
});
app.on("error", err => {
console.log("error", err.toString())
})
router.use('/list', listRouter.routes()).use(listRouter.allowedMethods())
router.use('/role', roleRouter.routes()).use(roleRouter.allowedMethods())
app.listen(3000, () => {
console.log("start at port 3000!")
})
12、文件上传: koa-better-body 、koa-convert
const Koa = require("koa");
const body = require("koa-better-body");
const convert = require("koa-convert");
const path = require("path");
const app= new Koa();
app.use(convert(
body({
uploadDir: path.resolve(__dirname, 'upload'),
maxFileSize: 100*1024
// 单位kb
})
))
server.listen(8000, error => {
console.log(error);
});
三、return next() 与 await next():
var http = require('http')
const Koa = require('koa')
const app = new Koa()
app.use(async(ctx,next)=>{
console.log(1)
await next()
ctx.body = 'hello world0'
console.log(2)
})
app.use(async(ctx,next)=>{
console.log(3)
ctx.body = 'hello world1'
return next()
console.log(4)
})
app.use(async(ctx,next)=>{
console.log(5)
ctx.body = 'hello world2'
console.log(6)
})
http.createServer(app.callback()).listen(80, () => console.log(`listening on port 80`))
依次执行:
console.log( 1 )
console.log( 3 )
ctx.body = 'hello world1'
console.log( 5 )
ctx.body = 'hello world2'
console.log( 6 )
ctx.body = 'hello world0'
console.log( 2 )
所以最后浏览器输出:hello world0