Nodejs-Koa(二)

回顾

npm install koa

  • application 核心功能创建一个应用
  • request koa自己封装的文件 目的是扩展req
  • response koa自己封装的文件 目的是扩展res
  • context 上下文 整合req和res 核心功能就是代理, 集成了原生的req、res

中间件

  • koa-bodyparser
  • koa-static
  • koa-router

koa-bodyparser

应用 一

写web服务 核心就是接收用户请求的数据 来解析,返回数据

功能

1) 有一个表单,当我访问 /form的时候显示表单
2) 当我点击 按钮时能提交

实现

新建form.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--form.html-->
<!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>
<form action="/login" method="POST">
<input type="text" name="username">
<input type="text" name="password">
<button>提交</button>
</form>
</body>
</html>

新建 1.koa.js

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 1.koa.js
const Koa = require('koa');
const app = new Koa();
const fs = require('fs');
const path = require('path');

// 中间件的特点 可以组合
app.use(async(ctx,next)=>{
// 写代码以后都用ctx 不用req这些
if(ctx.path === '/form' && ctx.method === 'GET'){
// 实现文件下载 指定下载 这样运行之后会下载名字为FileName.txt的文件
// ctx.set('Content-Disposition', "attachment;filename=FileName.txt");
ctx.set('Content-Type', 'text/html;charset=utf-8');
// 将文件读取返回
ctx.body = fs.createReadStream(path.resolve(__dirname,'form.html'));
// 运行 nodemon 1.koa.js 浏览器打开 http://localhost:3000/form
// koa这里默认不设置line16会直接下载了form.html文件 ,而line15是可以设置下载及下载文件的名称
}else{
await next(); // await next next()函数执行完成后,没有等待下一个中间件执行完成
}
});
app.use(async (ctx, next)=>{
// *********** 1
if(ctx.path === '/login' && ctx.method === 'POST'){
let arr = [];
ctx.req.on('data', function(chunk){
arr.push(chunk);
})
ctx.req.on('end', function () {
let result = Buffer.concat(arr).toString();
console.log(result); // username=1&password=2
ctx.body = result;
})
}
// *********** 1 end
// 1part 这样执行 命令行能够打印出 username=1&password=2 但是服务器没有返回数据给浏览器,因为见line19文字
// 这里面line19 是等待next()这个promise完成,即等待的是
// 下面这个line40-line52
//****************** 2
async (ctx, next)=>{
if(ctx.path === '/login' && ctx.method === 'POST'){
let arr = [];
ctx.req.on('data', function(chunk){
arr.push(chunk);
})
ctx.req.on('end', function () {
let result = Buffer.concat(arr).toString();
console.log(result);
ctx.body = result;
})
}
}
//****************** 2 end
// 2part 这个函数 而这个函数里面on('data'),on('end')是异步的,相当于最后这个函数返回的是 return undefined,而异步的还没执行完,所以对应ctx.body也是没有值的
})
app.listen(3000); // 监听3000端口

改进如下:
koa 所有异步都封装成 promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.use(async (ctx, next)=>{
if(ctx.path === '/login' && ctx.method === 'POST'){
return new Promise((resolve, reject)=>{
let arr = [];
ctx.req.on('data', function(chunk){
arr.push(chunk);
})
ctx.req.on('end', function () {
let result = Buffer.concat(arr).toString();
console.log(result);
ctx.body = result;
resolve();
})
})
// 运行 可以看到浏览器输出 username=1&password=2
}
})
优化

koa-bodyparser

统一处理请求体 安装第三方包 koa-bodyparser

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
26
 // 1.koa.js
const Koa = require('koa');
const app = new Koa();
const fs = require('fs');
const path = require('path');
const bodyparser = require('koa-bodyparser'); // 它是一个函数

app.use(bodyparser()); // 添加这一行

app.use(async(ctx,next)=>{
if(ctx.path === '/form' && ctx.method === 'GET'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = fs.createReadStream(path.resolve(__dirname,'form.html'));
}else{
await next();
}
});

app.use(async (ctx, next)=>{
if(ctx.path === '/login' && ctx.method === 'POST'){
// 统一处理请求体
ctx.body = ctx.request.body;
// 运行可以看到 服务器端将接收到的值返回给客户端
}
})
app.listen(3000);

手写 bodyparser
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 1.koa.js
const Koa = require('koa');
const app = new Koa();
const fs = require('fs');
const path = require('path');
// const bodyparser = require('koa-bodyparser');

function bodyparser(){
// app.use里面是async函数,现在放置bodyparser()函数,表示bodyparser()执行之后返回的是async,所以如下return async
return async (ctx, next)=>{
await new Promise((resolve,reject)=>{
let arr = [];
ctx.req.on('data', function(chunk){
arr.push(chunk);
})
ctx.req.on('end', function () {
let result = Buffer.concat(arr).toString();
console.log(result);
// ctx.body = result;
ctx.request.body = result;// 这里相比于原来的改一下
resolve();
})
})
await next(); // 再走下面的中间件逻辑
}
}

app.use(bodyparser()); // 添加这一行

app.use(async(ctx,next)=>{
if(ctx.path === '/form' && ctx.method === 'GET'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = fs.createReadStream(path.resolve(__dirname,'form.html'));
}else{
await next();
}
});

app.use(async (ctx, next)=>{
if(ctx.path === '/login' && ctx.method === 'POST'){
// 统一处理请求体
ctx.body = ctx.request.body;
// 运行可以看到 服务器端将接收到的值返回给客户端
}
})
// 错误处理
app.on('error',function(err){ // catch方法

})
app.listen(3000);
// 梳理
// 中间件的执行顺序是从上往下 所以这里会先执行line27 将值拿到 再执行下面的

应用 二

新建 form-bodyparser.html, 新增了上传文件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--from-bodyparser.html-->
<!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>
<form action="/login" method="POST" enctype="multipart/form-data">
<input type="text" name="username">
<input type="text" name="password">
<!--添加图片、文件-->
<input type="file" name="avatar">
<button>提交</button>
</form>
</body>
</html>

上传图片,提交。

  • 上传图片 这里因为 1.koa-bodyparser.js line56 打印数据,所以命令行即服务端打印出了图片的二进制数据,因为是二进制 并且这个文件下载了
  • 新建一个 1.txt文件 上传txt文件 可以看到浏览器显示的如图的是form-data格式 现在来自己解析下 通过分隔符将内容分成5分

新建 1.txt

1
hello word

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// 1.koa-bodyparser.js
const Koa = require('koa');
const app = new Koa();
const fs = require('fs');
const path = require('path');
const uuid = require('uuid');
// const bodyparser = require('koa-bodyparser');
Buffer.prototype.split = function(sep){
let len = Buffer.from(sep).length; // 分割符的长度
let offset = 0;
let result = [];
let current;
// 把找到的位置赋给current 看一下是否为-1
while((current = this.indexOf(sep,offset))!==-1){
result.push(this.slice(offset,current)); // 把每次的记过push到数组中
offset = current + len // 增加查找偏移量
}
result.push(this.slice(offset)); // 最后一段追加进去
return result;
}

function bodyparser(){
return async (ctx, next)=>{
await new Promise((resolve,reject)=>{
let arr = [];
ctx.req.on('data', function(chunk){
arr.push(chunk);
})
ctx.req.on('end', function () {
// 如果当前提交过来的数据不是正常json,表单格式,我们需要自己解析
let type = ctx.get('content-type');
let obj = {};
if(type.includes('multipart/form-data')){
// let result = Buffer.concat(arr).toString();
// 这里不能toString 因为提交的数据可能是二进制的,那么如图hello word的位置就会是一堆乱码,如果,根据分隔符将内容分成5分,如图我们要取123部分
let buff = Buffer.concat(arr);
let boundary = type.split("=")[1];
boundary = '--' + boundary;
let lines = buff.split(boundary).slice(1,-1);
// 如图①②③,头 和 体之间由2个换行回车分割, 这个是规定的
lines.forEach((line) => {
let [head,content] = line.split('\r\n\r\n');
head = head.toString();
let key = head.match(/name="(.+?)"/)[1]; // ?非贪婪模式
if(head.includes('filename')){ // 文件 把它放到upload里面
// 产生随机名 第三方库uuid
let filename = uuid.v4();
fs.writeFileSync(path.resolve(__dirname,"upload",filename),content.slice(0,-2),'utf8');
obj[key] = filename;
// 到这里可以看到上传的文件都存储在了./upload/下 如图 upload
}else{
// 这里像 1 2 hello word后面还有一个\r\n 所以最后2位不取
obj[key] = content.slice(0,-2).toString();
}
})
console.log(obj);
ctx.request.body = obj;
resolve();

}else{
resolve();
}

})
})
await next();
}
}

app.use(bodyparser());

app.use(async(ctx,next)=>{
if(ctx.path === '/form-bodyparser' && ctx.method === 'GET'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = fs.createReadStream(path.resolve(__dirname,'form-bodyparser.html'));
}else{
await next();
}
});

app.use(async (ctx, next)=>{
if(ctx.path === '/login' && ctx.method === 'POST'){
ctx.body = ctx.request.body;
}
})
// 错误处理
app.on('error',function(err){ // catch方法

})
app.listen(3000);

上传的文件都被放在 /upload/ 文件夹下

koa-static

应用

静态文件中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--form-bodyparser.html-->
<!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="./upload/bd3fe091-6bef-4403-b2ed-04c63adfb768.png" />
<form action="/login" method="POST" enctype="multipart/form-data">
<input type="text" name="username">
<input type="text" name="password">
<!--添加图片-->
<input type="file" name="avatar">
<button>提交</button>
</form>
</body>
</html>

如果 页面中有引用图片 如form-bodyparser.html line11 但是页面中没有显示

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 2.static.js
const Koa = require('koa');
const app = new Koa();
const {createWriteStream, createReadStream} = require('fs');
const fs = require('fs').promises;
const path = require('path');
const uuid = require('uuid');
const mime = require('mime');
// const static = require('koa-static');
// const bodyparser = require('koa-bodyparser');
Buffer.prototype.split = function(sep){
.....
}

function bodyparser(){
.....
}
app.use(bodyparser());

// 静态文件中间件 koa-static
function static(dirname){
return async (ctx,next)=>{
// 这里先明确 当前这个async方法如果处理的了就不用调next(),处理不了再调next()
try {
let filePath = path.join(dirname, ctx.path);
let statObj = await fs.stat(filePath);
console.log('静态文件路径1', filePath);
// /Users/jiafei/Desktop/Architecture-Course/20.koa/upload/bd3fe091-6bef-4403-b2ed-04c63adfb768.png
console.log('静态文件路径2', dirname);
// /Users/jiafei/Desktop/Architecture-Course/20.koa
console.log('静态文件路径3', ctx.path);
// /upload/bd3fe091-6bef-4403-b2ed-04c63adfb768.png
if(statObj.isDirectory()){
filePath = path.join(filePath, 'index.html');
await fs.access(filePath);
}
ctx.set('Content-Type',mime.getType(filePath) + ';charset=utf-8');
ctx.body = createReadStream(filePath);
} catch (error) {
await next();
}

}
} // 这样页面上就能显示图片了

// 这里把它当做插件来用,中间件都是函数,好处是可以传参,根据用户访问的路径,去对应的目录下查找,查找到返回即可,找不到交给下一个中间件,如下:
app.use(static(__dirname));
app.use(static(path.resolve(__dirname,'upload')));

app.use(async(ctx,next)=>{
if(ctx.path === '/form-bodyparser' && ctx.method === 'GET'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = createReadStream(path.resolve(__dirname,'form-bodyparser.html'));
}else{
await next();
}
});

app.use(async (ctx, next)=>{
if(ctx.path === '/login' && ctx.method === 'POST'){
ctx.body = ctx.request.body;
}
})
// 错误处理
app.on('error',function(err){ // catch方法
console.log(err);
})
app.listen(3000);
/*
// 如果 页面中有引用图片 如form-bodyparser.html line10 但是页面中没有显示
app.use(async(ctx,next)=>{
ctx.path === '/upload/xxxxx'
})
// 这是就需要再写一个app.use....来判断upload下的xxx,或者有其他的文件再写ctx.path===xxx来判断,一直重复做这个功能,并且这样也不好扩展,希望把这些功能统一处理,可以放到最上面,写一个中间件,中间件能够处理的话就不用下面处理了 如上 static()

koa-router

应用

1】

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
26
27
28
29
30
const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const static = require('koa-static');
const Router = require('koa-router');
const fs = require('fs');
const path = require('path');

let app = new Koa();

// 如果处理了静态文件,就不用处理请求体了,所以顺序先line11再line12
app.use(static(__dirname));
app.use(bodyparser());

// app.use((ctx,next)=>{
// if(...) 将一系列的路径判断用下面路由的方式匹配
// })

let router = new Router();
// 路由匹配 会匹配方法和路径
router.get('/form', async(ctx,next)=>{
ctx.set('Content-Type', 'text/html');
ctx.body = fs.createReadStream(path.resolve(__dirname,'form.html'));
})

router.post('/login', async(ctx,next)=>{
ctx.body = ctx.request.body;
})
// 挂载
app.use(router.routes());
app.listen(3000);

2】

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
26
27
28
29
30
31
32
33
const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const static = require('koa-static');
const Router = require('koa-router');

let app = new Koa();

app.use(static(__dirname));
app.use(bodyparser());

let router = new Router();

router.get('/', async(ctx,next)=>{
console.log(1);
next();
})
router.get('/', async(ctx,next)=>{
console.log(2);
})
router.get('/user', async(ctx,next)=>{
console.log(3);
next();
})
router.get('/user', async(ctx,next)=>{
console.log(4);
next();
})
app.use(router.routes());

app.use(async(ctx,next)=>{
console.log(5);
})
app.listen(3000);

浏览器访问 http://localhost:3000/ 服务端打印 1 2
浏览器访问 http://localhost:3000/user 服务端打印 3 4 5

匹配问题

  • 默认路由是严格匹配
    • 1、路径相同
    • 2、写成正则的方式 //a/b+/
    • 3、/a/:name 路径参数 这个name表示占位符 必须要有值
1) 路径严格相同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router();

router.get('/', function (ctx) {
ctx.body = 'hello';
})

app
.use(router.routes())
.use(router.allowedMethods()) // 表示当前这个路由可以允许哪些方法 405

app.listen(3000);

运行 可以看到浏览器输出 hello
如果把line7换成post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router();

router.post('/', function (ctx) {
ctx.body = 'hello';
})

app
.use(router.routes())
.use(router.allowedMethods()) // 表示当前这个路由可以允许哪些方法 405

app.listen(3000);

浏览器运行 http://localhost:3000/ 如图:

当前只运行 POST 访问

2) 正则匹配:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1)路径严格相同 如下line9 如果写的是/a/b  浏览器访问/a就访问不到
// 2)放正则,能匹配上也行 如下line10 浏览器访问 /a/123
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router();

// router.get('/a/b', function (ctx) {
router.get(/\/a\/\d+/, function (ctx) {
ctx.body = 'hello';
})

app
.use(router.routes())
.use(router.allowedMethods())

app.listen(3000);
3) 路径参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router();

router.get('/a/:name/:id', function (ctx,next) { // /a/1/id/2 => {name:1,id:2}
ctx.body = ctx.params;
// next();
})
router.get(/\/a\/\d+/, function (ctx) {
ctx.body = 'hello';
})

app
.use(router.routes())
.use(router.allowedMethods())

app.listen(3000);

运行 http://localhost:3000/a/1/2 浏览器输出 {“name”:”1”,”id”:”2”}
如果把line9注释打开 浏览器输出 hello ,如果把正则改成 //ab/\d+/ 正则这个匹配不上 还是输出上一个结果

拓展

可以给一个默认的路由前缀如line5 浏览器 http://localhost:3000/a/1/2 也能够正常访问到 并且输出{“name”:”1”,”id”:”2”}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const router = new Router({prefix:'/a'});

// 如果这时访问首页
router.get('/',function(ctx,next) {
ctx.body = 'home';
})
// http://localhost:3000/a 可以输出home 但是访问必须带/a 路由现在已经限制死了,必须带/a前缀 解决: 如下part

app
.use(router.routes())
.use(router.allowedMethods())

app.listen(3000);

解决:

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
26
27
28
29
// 描述:有个大路由(一级路由)  下面可以挂载2个小路由 (二级路由(/a /b))
const Koa = require('koa');
const app = new Koa();

const Router = require('koa-router');
const parent = new Router();
const child1 = new Router();
const child2 = new Router();

parent.get('/',function (ctx,next) {
ctx.body = 'home';
})

child1.get('/', function(ctx,next) {
ctx.body = '/a';
})
child2.get('/', function(ctx,next) {
ctx.body = '/b';
})

parent.use('/a',child1.routes());
parent.use('/b',child2.routes());


app
.use(parent.routes())
.use(parent.allowedMethods())

app.listen(3000);

以上用法一般情况下不用自己建 koa2里面提供了自带的模板

npm install koa-generator -g

里面会生成2个 koa、koa2, koa里面是基于generatore语法的,现在基本不用了,现在用koa2
查看命令行用法 koa2 –help
生成项目 koa2 -e(代表ejs) my-project 生成项目