nodejs学习笔记08-web服务器

http模块,express,koa

1. Http模块

1.1. Web服务器

  • 什么是Web服务器?

    • 当应用程序(客户端)需要某一个资源时,可以向一个台服务器,通过Http请求获取到这个资源;

    • 提供资源的这个服务器,就是一个Web服务器;

  • 目前有很多开源的Web服务器:Nginx、Apache(静态)、Apache Tomcat(静态、动态)、Node.js

1.2. Web服务器初体验

1
2
3
4
5
6
7
8
9
const http = require("http");

const server = http.createServer((request,response)=>{
response.end("Hello Node");
})

server.listen("5500",()=>{
console.log("开启 http://localhost:5500/");
});

1.3. 创建服务器

  • 创建服务器对象,通过 createServer 来完成

    • http.createServer会返回服务器的对象;

    • 底层其实使用直接 new Server 对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function createServer(opts, requestListener) {
      return new Server(opts, requestListener);
      }

      type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;

      // 故request,response本质还是stream
      class IncomingMessage extends stream.Readable {}
      class ServerResponse extends OutgoingMessage {}
      class OutgoingMessage extends stream.Writable {}
    • 创建Server时会传入一个回调函数,这个回调函数在被调用时会传入两个参数:

      • req:request请求对象,包含请求相关的信息;
      • res:response响应对象,包含要发送给客户端的信息;
  • 也可以通过new方式创建web服务器

    1
    2
    3
    4
    5
    6
    7
    8
    const http = require("http");

    const server2 = new http.Server((req,res)=>{
    res.end("server2");
    })
    server2.listen("5501",()=>{
    console.log("开启 http://localhost:5501/");
    });

1.4. 监听主机和端口号

1
2
// ?表示可选参数
listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this;
  • Server通过listen方法来开启服务器,并且在某一个主机和端口上监听网络请求:
  • 也就是通过 ip:port 的方式发送到监听的Web服务器上时,就可以对其进行相关的处理;
  • listen函数有三个参数:
  • 端口port:可以不传, 系统会默认分配端,通过 server.adderss().port获取当前端口号;
  • 主机host:通常可以传入localhost、ip地址127.0.0.1、或者ip地址0.0.0.0,默认是0.0.0.0;
    • localhost:本质上是一个域名,通常情况下会被解析成127.0.0.1;
    • 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是主机自己发出去的包,直接被自己接收;
      • 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ;
      • 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
      • 比如监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的;
      • 0.0.0.0:
        • 监听IPV4上所有的地址,再根据端口找到不同的应用程序;
        • 比如监听 0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的;
  • 回调函数:服务器启动成功时的回调函数;

1.5. request对象

  • 在向服务器发送请求时,会携带很多信息,比如:

    • 本次请求的URL,服务器需要根据不同的URL进行不同的处理;
    • 本次请求的请求方式,比如GET、POST请求传入的参数和处理的方式是不同的;
    • 本次请求的headers中也会携带一些信息,比如客户端信息、接受数据的格式、支持的编码格式等;
  • 这些信息,Node会封装到一个request的对象中,可以直接来处理这个request对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const http = require("http");

    const server = http.createServer((request,response)=>{
    console.log(request.url);
    console.log(request.method);
    console.log(request.headers);
    response.end("Hello Node");
    })

    server.listen("5500",()=>{
    console.log("开启 http://localhost:5500/");
    });

1.6. URL的处理

  • 客户端在发送请求时,会请求不同的数据,那么会传入不同的请求地址:

  • 比如 http://localhost:5500/login;

  • 比如 http://localhost:5500/products;

  • 服务器端需要根据不同的请求地址,作出不同的响应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const http = require("http");

    const server = http.createServer((request,response)=>{
    const url = request.url;

    if(url === '/login'){
    response.end("Hello login");
    }
    else if( url === '/products'){
    response.end("Hello products");
    }else{
    response.end("error message");
    }
    })

    server.listen("5500",()=>{
    console.log("开启 http://localhost:5500/");
    });

1.7. URL的解析

  • 如果用户发送的地址中还携带一些额外的参数呢?

  • 如何对它进行解析呢?

    • 使用内置模块url

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const http = require("http");
      const url = require("url");

      const server = http.createServer((request,response)=>{
      const info = url.parse(request.url);
      console.log(info);
      response.end("Hello");
      })

      server.listen("5500",()=>{
      console.log("开启 http://localhost:5500/");
      });

      得到数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      Url {
      protocol: null,
      slashes: null,
      auth: null,
      host: null,
      port: null,
      hostname: null,
      hash: null,
      search: '?username=why&password=123',
      query: 'username=why&password=123',
      pathname: '/login',
      path: '/login?username=why&password=123',
      href: '/login?username=why&password=123'
      }
  • 但是 query 信息如何可以获取呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const http = require("http");
    const url = require("url");
    const qs = require("querystring");

    const server = http.createServer((request,response)=>{

    const {query} = url.parse(request.url);
    const {name,password} = qs.parse(query);

    response.end(name+" "+password);
    });

1.8. method的处理

  • 在Restful规范(设计风格)中,对于数据的增删改查应该通过不同的请求方式

  • GET:查询数据;

  • POST:新建数据;

  • PATCH:更新数据;

  • DELETE:删除数据;

  • 所以,可以通过判断不同的请求方式进行不同的处理,比如创建一个用户:

    • 请求接口为 /login;
    • 请求方式为 POST 请求;
    • 携带数据 username 和 password;

1.9. 创建用户接口

  • 在程序中如何进行判断以及获取对应的数据呢?

    • 需要判断接口是 /users,并且请求方式是POST方法去获取传入的数据;

    • 获取这种body携带的数据,需要通过监听 req 的 data事件来获取;

    • 将JSON字符串格式化转成对象类型,通过JSON.parse方法即可;

      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
      const http = require("http");

      const server = http.createServer((request,response)=>{

      const {pathname,query} = url.parse(request.url);
      const method = request.method;
      // console.log(method);

      if(pathname === '/users'){
      if(method === 'GET'){
      const {username,password} = qs.parse(query);
      response.end(method+" "+username+" "+password);
      }
      else{
      request.setEncoding("utf8");
      request.on("data",(data)=>{
      // console.log(data);
      const {username,password} = JSON.parse(data);
      response.end(method+" "+username+" "+password);
      console.log(username+" "+password);

      });
      request.on("end",()=>{
      console.log("传输结束");
      });
      }
      }
      else{
      response.end("error message");
      }
      })

1.10. headers属性

在request对象的header中包含很多有用的信息,客户端会默认传递过来一些信息

1
2
3
4
5
6
7
8
9
10
11
{
'content-type': 'application/json',
'user-agent': 'PostmanRuntime/7.29.0',
accept: '*/*',
'cache-control': 'no-cache',
'postman-token': '58e449ab-f431-40d4-82eb-a03d21bfcf16',
host: 'localhost:5500',
'accept-encoding': 'gzip, deflate, br',
connection: 'keep-alive',
'content-length': '49'
}
  • content-type是这次请求携带的数据的类型:

    • application/json表示是一个json类型;
    • text/plain表示是文本类型;
    • application/xml表示是xml类型;
    • multipart/form-data表示是上传文件;
  • content-length:文件的大小和长度

  • keep-alive

    • http是基于TCP协议的,但是通常在进行一次请求和响应结束后会立刻中断;

    • 在http1.0中,如果想要继续保持连接:

      • 浏览器需要在请求头中添加 connection:keep-alive;
      • 服务器需要在响应头中添加 connection:keey-alive;
      • 当客户端再次放请求时,就会使用同一个连接,直接一方中断连接;
    • 在http1.1中,所有连接默认是 connection:keep-alive的;

  • accept-encoding:告知服务器,客户端支持的文件压缩格式,比如js文件可以使用gzip编码,对应 .gz文件;

  • accept:告知服务器,客户端可接受文件的格式类型;

  • user-agent:客户端相关的信息;

1.11. 返回响应结果

  • 如果希望给客户端响应的结果数据,可以通过两种方式:

    • Write方法:这种方式是直接写出数据,但是并没有关闭流;

      1
      2
      response.write("Hello write()");
      response.end();
    • end方法:这种方式是写出最后的数据,并且写出后会关闭流;

      1
      response.end("Hello end()");
  • 如果没有调用 end,客户端将会一直等待结果:

    • 所以客户端在发送网络请求时,都会设置超时时间。

1.12. 返回状态码

  • Http状态码(Http Status Code)是用来表示Http响应状态的数字代码:

  • Http状态码非常多,可以根据不同的情况,给客户端返回不同的状态码;

  • 常见的状态码

    状态代码 状态描述 说明
    200 OK 客户端请求成功
    400 Bad Request 由于客户端请求有语法错误,不能被服务器理解
    401 Unauthorized 请求未经授权。这个状态代码必须和www-Authenticate报头域一起使用
    403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
    404 Not Found 请求的资源不存在,例如,输入了错误的URL
    500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求
    503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常
  • 设置状态码常见的有两种方式

    1
    2
    3
    response.statusCode = 404;

    response.writeHead(400);

1.13. 响应头文件

  • 返回头部信息,主要有两种方式:

  • res.setHeader:一次写入一个头部信息;

    1
    response.setHeader("Content-Type","application/json;charaset=utf8");
  • res.writeHead:同时写入header和status;

    1
    2
    3
    response.writeHead(200,{
    "Content-Type":"application/json;charaset=utf8"
    })
  • Header设置 Content-Type有什么作用呢?

  • 默认客户端接收到的是字符串,客户端会按照自己默认的方式进行处理;

    1
    2
    response.setHeader("Content-Type","text/html;charaset=utf8");
    response.end("<h1>why 123</h1>");

    1
    2
    response.setHeader("Content-Type","application/json;charaset=utf8");
    response.end("<h1>why 123</h1>");

1.14. http请求

  • axios库可以在浏览器中使用,也可以在Node中使用:

    • 在浏览器中,axios使用的是封装xhr;

    • 在Node中,使用的是http内置模块;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      const http = require("http");

      const server = http.createServer((request,response)=>{
      const obj = {
      username: 'root',
      password: '123'
      }
      response.end(JSON.stringify(obj));
      }

      server.listen("5500",()=>{
      console.log("开启 "+server.address().port);
      });

      发送请求

      1
      2
      3
      4
      5
      6
      7
      8
      const http = require("http");

      http.get("http://localhost:5500/",(res)=>{
      res.on("data",data=>{
      console.log(data.toString());
      console.log(JSON.parse(data));
      });
      });

      或者是

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const req = http.request({
      path: '/login?username=root&&password=132',
      port: 5500,
      hostname: 'localhost',
      method: 'GET'
      },res=>{
      res.on("data",data=>{
      console.log(data.toString());
      });
      });
      req.end();

1.15. 文件上传

1.15.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
const http = require("http");
const fs = require("fs");

const server = http.createServer((req,res)=>{
const fileWriter = fs.createWriteStream("./file.png");
req.pipe(fileWriter);

const fileSize = req.headers['content-length'];
console.log(fileSize);

let curSize = 0;

req.on("data",data=>{
curSize += data.length;
console.log(curSize);
res.write(`文件上传进度:${curSize/fileSize*100}%\n`);
});
req.on("end",()=>{
res.end("文件上传完成");
});
});

server.listen("5555",()=>{
console.log("开启 5555");
})

写入的数据并不只有图片信息

1.15.2. 实际数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require("http");
const fs = require("fs");

const server = http.createServer((req,res)=>{
let body = '';

req.on("data",data=>{
body+=data;
});
req.on("end",()=>{
console.log(body);
res.end("OK");
});
});

server.listen("5555",()=>{
console.log("开启 5555");
})

断点调试

shift+command+D 选择运行和调试 node.js

得到body的数据中是含有文件信息

1.15.3. 正确做法

可以传入任何类型的文件

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
const http = require("http");
const fs = require("fs");
const qs = require("querystring");

const server = http.createServer((req,res)=>{
// 图片文件必须设置为二进制
req.setEncoding("binary");

// 获取content-type中的boundary的值
// multipart/form-data; boundary=<calculated when request is sent>
const boundary = req.headers['content-type'].split('; ')[1].replace('boundary=','');

// 记录整体数据的信息
const fileSize = req.headers['content-length'];
// 记录当前数据信息
let curSize = 0;

let body = '';

req.on("data",data=>{
curSize += data.length;
body+=data;
res.write(`文件上传进度:${curSize/fileSize*100}%\n`);
});
req.on("end",()=>{
// 切割数据
const payload = qs.parse(body,"\r\n",":");
// 获取最后的类型
const fileType = payload["Content-Type"].substring(1);

// 文件名
const fileInfo = qs.parse(payload["Content-Disposition"],"; ","=");
/**
* [Object: null prototype] {
' form-data': '',
name: '"file"',
filename: '"10_http模å\x9D\x97.pdf"',
'filename*': "UTF-8''10_http模块.pdf"
}
*/
// console.log(fileInfo);
let fileName = '';
// 中文
if(fileInfo["filename*"]){
let fileNamePosition = fileInfo["filename*"].lastIndexOf("'");
fileName = fileInfo["filename*"].substring(fileNamePosition+1).replace('"','');
}
// 英文
else{
fileName = fileInfo["filename"].replace('"','');
fileName = fileName.replace('"','');
}
// console.log(fileName);

// 获取要截取的长度
// 从 image/jpeg 开始截
const fileTypePosition = body.indexOf(fileType)+fileType.length;
let binaryData = body.substring(fileTypePosition);
// 去除空格
binaryData = binaryData.replace(/^\s\s*/,'');
// 去除最后的信息
const finalData = binaryData.substring(0,binaryData.indexOf('--'+boundary+'--'));

// 创建文件夹
const dirname = 'uploads';
if(!fs.existsSync(dirname)){
fs.mkdir(dirname,err=>{
if(err){
console.log(err);
}
})
}
// 写入文件
fs.writeFile("./uploads/"+fileName,finalData,{
encoding: 'binary'
},err=>{
if(err){
console.log(err);
}
res.end("文件上传完成");
});

});
});

server.listen("5555",()=>{
console.log("开启 5555");
})

2. Express框架

中文:https://www.expressjs.com.cn

英文:https://www.expressjs.com

2.1. 认识Web框架

  • 使用http内置模块来搭建Web服务器,为什么还要使用框架?
    • 原生http在进行很多处理时,会较为复杂;
    • 有URL判断、Method判断、参数处理、逻辑代码处理等,都需要自己来处理和封装;
    • 并且所有的内容都放在一起,会非常的混乱;
  • 目前在Node中比较流行的Web服务器框架是 express、koa;
  • express早于koa出现,并且在Node社区中迅速流行起来:
    • 可以基于express快速、方便的开发自己的Web服务器;
    • 并且可以通过一些实用工具和中间件来扩展功能;

Express整个框架的核心就是中间件,理解了中间件其他一切都非常简单!

2.2. Express安装

  • express的使用过程有两种方式

    1. 通过express提供的脚手架,直接创建一个应用的骨架;
    2. 从零搭建自己的express应用结构;
  • 方式一:安装express-generator

    • 安装脚手架

      1
      npm install -g express-generator
    • 创建项目

      1
      express express-demo
    • 安装依赖

      1
      npm install
    • 启动项目

      1
      node bin/www
  • 方式二:从零搭建自己的express应用结构;

    1
    2
    npm init -y
    npm install express

2.3. Express的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");

const app = new express();

// 路由
app.get("/",(req,res,next)=>{
res.end("Hello GET");
});

app.post("/",(req,res,next)=>{
res.end("Hello POST");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});
  • 创建express项目

    • 在开发过程中,可以方便的将请求进行分离:
    • 无论是不同的URL,还是get、post等请求方式;
    • 这样的方式非常方便进行维护、扩展;
  • 请求的路径中如果有一些参数,可以这样表达:

  • /users/:userId;

  • 在request对象中获取可以通过 req.params.userId;

  • 返回数据,可以方便的使用json:

2.4. 认识中间件

  • Express是一个路由和中间件的Web框架,它本身的功能非常少

    • Express应用程序本质上是一系列中间件函数的调用;
  • 中间件是什么呢?

    • 中间件的本质是传递给express的一个回调函数;
    • 这个回调函数接受三个参数:
      • 请求对象(request对象);
      • 响应对象(response对象);
      • next函数(在express中定义的用于执行骗一个中间件的函数);
  • 中间件中可以执行哪些任务呢?

  • 执行任何代码;

  • 更改请求(request)和响应(response)对象;

  • 结束请求-响应周期(返回数据);

  • 调用栈中的下一个中间件;

  • 如果当前中间件功能没有结束请求-响应周期,则必须调用next()将控制权传递给下一个中间件功能,否则,请求将被挂起。

2.5. 应用中间件

  • 那么,如何将一个中间件应用到应用程序中呢?

    • express主要提供了两种方式:app/router.use和app/router.methods;
    • 可以是 app,也可以是router
    • methods指的是常用的请求方式,比如: app.get或app.post等;
  • 先来学习use的用法,因为methods的方式本质是use的特殊情况;

    路由只会匹配第一个可以处理请求的中间件,除非有next调用下一个匹配的中间件

2.5.1. 案例一:最普通的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
res.end("user1");
});


// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
res.end("user1");
});
// 并没有调用next(),不会调用下一个中间件
app.use((req,res,next)=>{
console.log("use2");
res.end("use2");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
res.end("user1");
next();
});
app.use((req,res,next)=>{
console.log("use2");
// 响应数据已经在上一个中间件发送,整个请求-响应周期结束
res.end("use2");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
next();
});
app.use((req,res,next)=>{
console.log("use2");
res.end("use2");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.5.2. 案例二:path匹配中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
res.end("use1");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
next();
});
app.use('/login',(req,res,next)=>{
console.log("login");
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.5.3. 案例三:path和method匹配中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
next();
});
app.get('/login',(req,res,next)=>{
console.log("login");
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.5.4. 案例四:注册多个中间件

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
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
console.log("use1");
next();
});

app.get("/login",(req,res,next)=>{
console.log("login1");
next();
},(req,res,next)=>{
console.log("login2");
next();
},(req,res,next)=>{
console.log("login3");
next();
},(req,res,next)=>{
console.log("login4");
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.6. 应用中间件 – body解析

  • 并非所有的中间件都需要从零去编写:
    • express有内置一些帮助完成对request解析的中间件;
    • registry仓库中也有很多可以辅助我们开发的中间件;
  • 在客户端发送post请求时,会将数据放到body中:
    • 客户端可以通过json的方式传递;
    • 也可以通过form表单的方式传递;

2.6.1. 编写解析request body中间件

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
const express = require("express");

const app = new express();

app.use((req,res,next)=>{
if(req.headers['content-type'] === 'application/json'){
let body = '';
req.on("data",data=>{
body+=data;
})
req.on("end",()=>{
req.body = JSON.parse(body);
next();
})
}
else{
next();
}
});
app.get("/login",(req,res,next)=>{
console.log(req.body);
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.6.2. 解析body - expres内置json()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");

const app = new express();

app.use(express.json());
app.get("/login",(req,res,next)=>{
console.log(req.body);
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.6.3. 解析application/x-www-form-urlencoded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require("express");

const app = new express();
/**
* extend
* true: 对urlencoded 进行解析时,使用的是第三方库 qs
* false: 对urlencoded 进行解析时,使用的是Node内置库 querystring
*
* 原文
* The extended option allows to choose between parsing the URL-encoded data
* with the querystring library (when `false`) or the qs library (when `true`).
*/
app.use(express.urlencoded({extended:true}));

app.get("/login",(req,res,next)=>{
console.log(req.body);
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.7. 应用中间件 – 第三方中间件

2.7.1. morgan记录日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = new express();
const morgan = require("morgan");
const fs = require("fs");

const logWrite = fs.createWriteStream("./out.log",{
flags:'a+'
});

app.use(morgan("combined",{stream:logWrite}));

app.use(express.json());
app.get("/login",(req,res,next)=>{
console.log(req.body);
res.end("login");
});

// 开启监听
app.listen(5858,()=>{
console.log("访问 5858");
});

2.7.2. multer 上传文件

https://github.com/expressjs/multer

https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md

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
const express = require("express");
const multer = require("multer");
const path = require("path");

const app = new express();


// 文件命名
const storage = multer.diskStorage({
// destination 是用来确定上传的文件应该存储在哪个文件夹中
destination: (req,file,cb)=>{
cb(null,"uploads/");
},
// filename 用于确定文件夹中的文件名
filename: (req,file,cb)=>{
cb(null,Date.now()+path.extname(file.originalname));
}
})

const upload = multer({
storage
});

/**
*
* .single(fieldname)
接受一个以 fieldname 命名的文件。
这个文件的信息保存在 req.file。

.array(fieldname[, maxCount])
接受一个以 fieldname 命名的文件数组。
可以配置 maxCount 来限制上传的最大数量。
这些文件的信息保存在 req.files。

.fields(fields)
接受指定 fields 的混合文件。
这些文件的信息保存在 req.files。
fields 应该是一个对象数组,应该具有 name 和可选的 maxCount 属性。
*/
app.post("/uploads",upload.single('file'),(req,res,next)=>{
console.log(req.file);
console.log(req.body);
res.end("上传完成");
});

app.listen(5888,()=>{
console.log("开始监听");
});

2.7.3. multer解析form-data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");
const multer = require("multer");

const app = new express();

// any()
// 接受一切上传的文件。文件数组将保存在 req.files
// 永远不要将 multer 作为全局中间件使用
app.get("/login",multer().any(),(req,res,next)=>{
console.log(req.files);
console.log(req.body);
res.end("OK");
});

app.listen(5888,()=>{
console.log("开始监听");
});

2.8. 客户端发送请求的方式

客户端传递到服务器参数的方法常见的是5种:

  1. 方式一:通过get请求中的URL的params;
  2. 方式二:通过get请求中的URL的query;
  3. 方式三:通过post请求中的body的json格式;
  4. 方式四:通过post请求中的body的x-www-form-urlencoded格式;
  5. 方式五:通过post请求中的form-data格式;

2.8.1. URL的params

req.params

1
2
3
4
5
6
7
8
9
10
11
const express = require("express");
const app = new express();

app.use("/login/:id/:path",(req,res,next)=>{
console.log(req.params);
res.end("params");
});

app.listen(5860,()=>{
console.log("开启 5860");
})

访问 http://localhost:5860/login/1/a

得到的是json

1
{ id: '1', path: 'a' }

2.8.2. URL的query

req.query

1
2
3
4
5
6
7
8
9
10
11
const express = require("express");
const app = new express();

app.use("/login",(req,res,next)=>{
console.log(req.query);
res.end("query");
});

app.listen(5860,()=>{
console.log("开启 5860");
})

访问 http://localhost:5860/login?name=root&pass=123

得到的是json

1
{ name: 'root', pass: '123' }

2.8.3. body的json

req.body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");

const app = new express();

app.use(express.json());
app.use("/login",(req,res,next)=>{
console.log(req.body);
res.end("json");
});

// 开启监听
app.listen(5860,()=>{
console.log("开启 5860");
});

访问 http://localhost:5860/login

1
2
3
4
{
"name":"root",
"pass":123
}

得到的是json

1
{ name: 'root', pass: '123' }

2.8.4. body的x-www-form-urlencoded

req.body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const express = require("express");

const app = new express();

app.use(express.urlencoded({extended:true}));

app.use("/login",(req,res,next)=>{
console.log(req.body);
res.end("x-www-form-urlencoded");
});

// 开启监听
app.listen(5860,()=>{
console.log("开启 5860");
});

2.8.5. body的form-data

req.body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");
const multer = require("multer");

const app = new express();

app.use("/login",multer().any(),(req,res,next)=>{

console.log(req.body);
res.end("form-data");
});

app.listen(5860,()=>{
console.log("开启 5860");
});

2.9. 响应数据

  • end方法
    • 类似于http中的response.end方法,用法是一致的
  • json方法
    • json方法中可以传入很多的类型:object、array、string、boolean、number、null等,它们会被转换成json格式返回;
  • status方法
    • 用于设置状态码;
  • 更多响应的方式:https://www.expressjs.com.cn/4x/api.html#res

2.10. Express的路由

  • 如果将所有的代码逻辑都写在app中,那么app会变得越来越复杂:
    • 一方面完整的Web服务器包含非常多的处理逻辑;
    • 另一方面有些处理逻辑其实是一个整体,应该将它们放在一起:
      • 比如对users相关的处理:
      • 获取用户列表; get /users
      • 获取某一个用户信息; get /users/:id
      • 创建一个新的用户; post /users/add
      • 删除一个用户; delete /users/:id
      • 更新一个用户; patch /users/:id
  • 使用 express.Router来创建一个路由处理程序:
    • 一个Router实例拥有完整的中间件和路由系统;
    • 因此,它也被称为迷你应用程序(mini-app);
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
const express = require("express");
const app = new express();

// application/json
app.use(express.json());
// application/x-www-form-urlencoded
app.use(express.urlencoded({extended:true}));
// form-data
const multer = require("multer");

const useRouter = express.Router();

useRouter.get("/",(req,res,next)=>{
res.json("所有用户数据");
});
useRouter.get("/:id",(req,res,next)=>{
res.json(req.params.id+ " 用户数据");
});
useRouter.post("/add",(req,res,next)=>{
console.log(req.body);
res.end("添加用户数据");
});
app.use("/users",multer().any(),useRouter);

app.listen(8888,()=>{
console.log("开启 8888");
});

2.11. 静态资源服务器

  • 部署静态资源我们可以选择很多方式:
    • Node也可以作为静态资源服务器,并且express提供了方便部署静态资源的方法;
    • 在 uploads 文件夹下放置 index.html 文件
1
2
3
4
5
6
7
8
const express = require("express");
const app = new express();

app.use(express.static("./uploads"));

app.listen(8888,()=>{
console.log("开启 8888");
})

访问 http://localhost:8888/,直接打开index.html

2.12. 服务端的错误处理

将错误通过next传递给中间件函数

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
const express = require("express");
const app = new express();
const useRouter = express.Router();

useRouter.get("/",(req,res,next)=>{
next(new Error("没有权限"));
});

// 路由
app.use("/users",multer().any(),useRouter);

app.use((err,req,res,next)=>{
// console.log(err);
const message = err.message;
const status = 400;
res.status(status);
res.json({
errStatus: status,
errMessage: message
})
})


app.listen(8888,()=>{
console.log("开启 8888");
});

2.13. 源码

  1. 调用express创建的是什么
  2. app.listen启动服务器
    • 如何结合原生http启动服务器
    • express -> http.createServer.listen
  3. app.use(中间件)时,内部发生了什么?
  4. 用户发送请求,中间件是如何被回调的?
  5. next为什么会执行下一个中间件?

2.13.1. 创建app的过程

https://github.com/expressjs/express/blob/master/lib/express.js

express函数的本质其实是createApplication

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
var proto = require('./application');

exports = module.exports = createApplication;

function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};

mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);

// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})

// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})

app.init();
return app;
}

https://github.com/expressjs/express/blob/master/lib/application.js

1
2
3
4
5
6
7
8
9
app.listen = function listen() {
/**
this - function(req, res, next) {
app.handle(req, res, next);
};
*/
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

2.13.2. 注册中间件

  • 比如通过use来注册一个中间件,源码中发生了什么?
    • 无论是app.use还是app.methods都会注册一个主路由;
    • app本质上会将所有的函数,交给这个主路由去处理的;

https://github.com/expressjs/express/blob/master/lib/application.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
57
app.use = function use(fn) {
var offset = 0;
var path = '/';

// default path to '/'
// disambiguate app.use([fn])
if (typeof fn !== 'function') {
var arg = fn;

while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}

// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}

var fns = flatten(slice.call(arguments, offset));

if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}

// setup router
this.lazyrouter();
var router = this._router;

// 遍历所有函数
fns.forEach(function (fn) {
// non-express app
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}

debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;

// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});

// mounted an app
fn.emit('mount', this);
}, this);

return this;
};

app.lazyrouter中创建的 new Router() 传递给 var router = this._router

1
2
3
4
5
6
7
8
9
10
11
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});

this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};

https://github.com/expressjs/express/blob/master/lib/router/index.js

把中间件放在 router.stack = []; 数组中 this.stack.push(layer);

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
var proto = module.exports = function(options) {
var opts = options || {};

function router(req, res, next) {
router.handle(req, res, next);
}

// mixin Router class functions
setPrototypeOf(router, proto)

router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];

return router;
};


proto.use = function use(fn) {
var offset = 0;
var path = '/';

// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') {
var arg = fn;

while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}

// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}

var callbacks = flatten(slice.call(arguments, offset));

if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}

for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];

if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}

// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')

var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);

layer.route = undefined;

this.stack.push(layer);
}

return this;
};

2.13.3. 请求的处理过程

  • 如果有一个请求过来,那么从哪里开始呢?
    • app函数被调用开始的;

https://github.com/expressjs/express/blob/master/lib/express.js

1
2
3
4
5
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
}

https://github.com/expressjs/express/blob/master/lib/application.js

1
2
3
4
app.handle = function handle(req, res, callback) {
var router = this._router;
router.handle(req, res, done);
};

https://github.com/expressjs/express/blob/master/lib/router/index.js

next为什么会执行下一个中间件?

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
proto.handle = function handle(req, res, out) {
var self = this;

var idx = 0;

// middleware and routes
var stack = self.stack;

next();

function next(err) {
var layerError = err === 'route'? null: err;

// get pathname of request
var path = getPathname(req);

// find next matching layer
var layer;
var match;
var route;

while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;

if (match !== true) {
continue;
}
}
}
};

function matchLayer(layer, path) {
try {
return layer.match(path);
} catch (err) {
return err;
}
}

3. Koa框架

3.1. 认识Koa

  • Koa也是非常流行的Node Web服务器框架

  • Koa官方的介绍:

    • koa:next generation web framework for node.js;
    • koa:node.js的下一代web框架;
  • 事实上,koa是express同一个团队开发的一个新的Web框架:

  • 目前团队的核心开发者TJ的主要精力也在维护Koa,express已经交给团队维护了;

  • Koa旨在为Web应用程序和API提供更小、更丰富和更强大的能力;

  • 相对于express具有更强的异步处理能力;

  • Koa的核心代码只有1600+行,是一个更加轻量级的框架,可以根据需要安装和使用中间件;

3.2. Koa初体验

安装模块 npm i koa

  • koa注册的中间件提供了两个参数
    • ctx:上下文(Context)对象;

      • koa并没有像express一样,将req和res分开,而是将它们作为ctx的属性;
      • ctx代表依次请求的上下文对象;
      • ctx.request:获取请求对象;
      • ctx.response:获取响应对象;
    • next:本质上是一个dispatch,类似于之前的next;

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();

app.use((ctx,next)=>{
console.log("Hello Koa1");
next();
});

app.use((ctx,next)=>{
console.log("Hello Koa2");

ctx.response.body = "Hello Koa";
console.log(ctx);
});

app.listen(9999,()=>{
console.log("开启 koa 9999");
})

ctx内容

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
{
request: {
method: 'GET',
url: '/',
header: {
'user-agent': 'PostmanRuntime/7.29.0',
accept: '*/*',
'cache-control': 'no-cache',
'postman-token': 'cd8f5a7f-0d98-4b4d-bd22-7563b4ea1238',
host: 'localhost:9999',
'accept-encoding': 'gzip, deflate, br',
connection: 'keep-alive'
}
},
response: {
status: 200,
message: 'OK',
header: [Object: null prototype] {
'content-type': 'text/plain; charset=utf-8',
'content-length': '9'
}
},
app: { subdomainOffset: 2, proxy: false, env: 'development' },
originalUrl: '/',
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
}

3.3. Koa中间件

  • koa通过创建的app对象,注册中间件只能通过use方法:

  • Koa并没有提供methods的方式来注册中间件;

  • 也没有提供path中间件来匹配路径;

  • 但是真实开发中我们如何将路径和method分离呢?

  1. 方式一:根据request的url来判断;

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

    const app = new koa();

    app.use((ctx,next)=>{
    if(ctx.request.url === '/login'){
    ctx.response.body = "Hello Koa login";
    }else{
    ctx.response.body = "Hello Koa";
    }
    });

    app.listen(9999,()=>{
    console.log("开启 koa 9999");
    })
  2. 方式二:使用第三方路由中间件;

    1
    npm i koa-router

3.4. 路由的使用

  • 先封装一个 user.js 的文件;

  • 在app中将router.routes()注册为中间件;

  • 注意:allowedMethods用于判断某一个method是否支持:

    • 如果请求 get,那么是正常的请求,因为有实现get;
    • 如果请求 put、delete、patch,那么自动报错: Method Not Allowed,状态码:405;
    • 如果请求 link、copy、lock,那么自动报错: Not Implemented,状态码:501;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ==================【router/user.js】 ======================
const Router = require("koa-router");

const userRouter = new Router({prefix: '/users'});

userRouter.get("/",(ctx,next)=>{
ctx.response.body = "所有数据";
});

module.exports = userRouter;

// ===================【index.js】============================
const koa = require("koa");
const userRouter = require("./router/user");

const app = new koa();

app.use(userRouter.routes());

app.listen(9999,()=>{
console.log("开启 koa 9999");
})

在index.js中添加方法判断

1
app.use(userRouter.routes());

3.5. 参数解析

3.5.1. params

ctx.request.params

http://localhost:9999/users/get/1

1
2
3
4
5
6
7
8
9
const Router = require("koa-router");

const userRouter = new Router({prefix: '/users'});

userRouter.get("/get/:id",(ctx,next)=>{
ctx.response.body = `${ctx.request.params.id} 数据`;
});

module.exports = userRouter;

3.5.2. query

ctx.request.query

http://localhost:9999/users/login?name=root

1
2
3
4
5
6
7
8
9
10
const Router = require("koa-router");

const userRouter = new Router({prefix: '/users'});

userRouter.get("/login",(ctx,next)=>{
console.log(ctx.request.query);
ctx.response.body = `${ctx.request.query.name} 欢迎`;
});

module.exports = userRouter;

3.5.3. json

  • 请求地址:http://localhost:9999/login

  • body是json格式:

    1
    2
    3
    4
    {
    "name": "root",
    "pass": "123"
    }

  • 获取json数据:

    • 安装依赖

      1
      npm install koa-bodyparser
    • 使用 koa-bodyparser的中间件;

      const bodyparser = require("koa-bodyparser");

      app.use(bodyparser());

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      const koa = require("koa");

      const bodyparser = require("koa-bodyparser");

      const userRouter = require("./router/user");


      const app = new koa();

      app.use(bodyparser());
      app.use(userRouter.routes());
      app.use(userRouter.allowedMethods());

      app.listen(9999,()=>{
      console.log("开启 koa 9999");
      })

      现在 user.js 中可以通过 ctx.request.body 得到JSON格式数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const Router = require("koa-router");

      const userRouter = new Router({prefix: '/users'});

      userRouter.get("/login",(ctx,next)=>{
      console.log(ctx.request.body);
      ctx.response.body = "欢迎";
      });

      module.exports = userRouter;

3.5.4. x-www-form-urlencoded

  • 请求地址:http://localhost:9999/login

  • body是x-www-form-urlencoded格式:

  • 获取 x-www-form-urlencoded 数据:

    • 安装依赖 - 跟json一样

      1
      npm install koa-bodyparser
    • 使用 koa-bodyparser的中间件;

      const bodyparser = require("koa-bodyparser");

      app.use(bodyparser());

      user.js 中可以通过 ctx.request.body 得到x-www-form-urlencoded格式数据

3.5.5. form-data

  • 请求地址:http://localhost:9999/login

  • body是form-data格式:

  • 获取 form-data 数据:

    • 安装依赖

      1
      npm i @koa/multer
    • 使用 multer 的中间件;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      const Router = require("koa-router");
      const multer = require("@koa/multer");

      const userRouter = new Router({prefix: '/users'});

      const upload = multer({});

      userRouter.get("/login",upload.any(),(ctx,next)=>{
      console.log(ctx.request.body);
      ctx.response.body = "欢迎";
      });

      module.exports = userRouter;

3.6. 上传文件 multer

router/file.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
const path = require("path");

const Router = require("koa-router");
const multer = require("@koa/multer");

const fileRouter = new Router({prefix: "/file"});
const storage = multer.diskStorage({
// 文件存放位置
destination: (req,file,cb)=>{
cb(null,"./uploads");
},
// 文件命名
filename: (req,file,cb)=>{
cb(null,Date.now()+path.extname(file.originalname));
}
});

const upload = multer({
storage
});


fileRouter.post("/",upload.single("avatar"),(ctx,next)=>{
// 获取文件信息
console.log(ctx.request.file);
ctx.response.body = "上传完成";
})

module.exports = fileRouter;

index.js

1
2
3
4
5
6
7
8
9
10
11
const koa = require("koa");

const fileRouter = require("./router/file");

const app = new koa();

app.use(fileRouter.routes());

app.listen(9999,()=>{
console.log("开启 koa 9999");
})

访问 http://localhost:9999/file

得到file信息

1
2
3
4
5
6
7
8
9
10
{
fieldname: 'avatar',
originalname: 'IMG_044531DF1EE9-1.jpeg',
encoding: '7bit',
mimetype: 'image/jpeg',
destination: './uploads',
filename: '1658562598539.jpeg',
path: 'uploads/1658562598539.jpeg',
size: 415527
}

3.7. 数据的响应

  • 输出结果:body将响应主体设置为以下之一

    • string:字符串数据
    • Buffer:Buffer数据
    • Stream:流数据
    • Object|| Array:对象或者数组
    • null:不输出任何内容
    • 如果response.status尚未设置,Koa会自动将状态设置为200或204。
  • 请求状态:status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Router = require("koa-router");

const userRouter = new Router({prefix: '/users'});

userRouter.get("/send",(ctx,next)=>{
// Object
ctx.body = {
name: 'uname',
message: "mmmmmm"
};
// array
ctx.body = ['a','b','c'];
// string
ctx.body = "Hello";
// 状态码
ctx.status = 404;
});

module.exports = userRouter;

ctx.body = ctx.response.body 两者是同一个对象

https://github.com/koajs/koa/blob/master/lib/context.js

delegate 代理 proto对象,实际指向 response对象,故 content.body 实际是调用 content.response.body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Context prototype.
*/

const proto = module.exports;

delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable')

3.8. 静态资源服务器

1
npm install koa-static

部署的过程类似于express

1
2
3
4
5
6
7
8
9
10
11
const koa = require("koa");

const koaStatic = require("koa-static");

const app = new koa();

app.use(koaStatic("./uploads"));

app.listen(9999,()=>{
console.log("开启 koa 9999");
})

访问 http://localhost:9999/1658562598539.jpeg

3.9. 错误处理

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
// ================ 【router/user.js】 =======================
const Router = require("koa-router");

const userRouter = new Router({prefix: '/users'});

userRouter.get("/error",(ctx,next)=>{
ctx.app.emit("error",new Error("未注册"),ctx);
});

module.exports = userRouter;


// ================ 【index.js】 =======================
const koa = require("koa");

const fileRouter = require("./router/file");
const userRouter = require("./router/user");

const app = new koa();
app.use(userRouter.routes());

app.on("error",(err,ctx)=>{
ctx.response.body = err.message;
});

app.listen(9999,()=>{
console.log("开启 koa 9999");
})

3.10. 源码

https://github.com/koajs/koa/blob/master/lib/application.js

require(“koa”) 实际是导入了Application,koa()实际是Application()

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
module.exports = class Application extends Emitter {

constructor (options) {
this.middleware = []
}

listen (...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
}

use (fn) {
this.middleware.push(fn)
return this
}

callback () {
const fn = this.compose(this.middleware)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}

return handleRequest
}

handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
// 处理所有中间件后才会处理响应数据
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}

}

compose (middleware)

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
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

3.11. koa vs express

  • 从架构设计上来说

    • express是完整和强大的,其中帮助我们内置了非常多好用的功能;

    • koa是简洁和自由的,它只包含最核心的功能,并不会对我们使用其他中间件进行任何的限制。

      • 甚至是在app中连最基本的get、post都没有提供;
      • 需要通过自己或者路由来判断请求方式或者其他功能;
  • 因为express和koa框架的核心其实都是中间件

    • 但是中间件的执行机制是不同的,特别是针对某个中间件中包含异步操作时;

3.11.1. 案例实现

  • 通过一个需求来演示所有的过程:

    • 假如有三个中间件会在一次请求中匹配到,并且按照顺序执行;
    • 最终实现的方案是:
      • 在middleware1中,在req.message中添加一个字符串 aaa;
      • 在middleware2中,在req.message中添加一个 字符串bbb;
      • 在middleware3中,在req.message中添加一个 字符串ccc;
      • 当所有内容添加结束后,在middleware1中,通过res返回最终的结果;
  • 实现方案:

  • Express同步数据的实现;

  • Express异步数据的实现;

  • Koa同步数据的实现;

  • Koa异步数据的实现;

3.11.2. Express同步数据实现

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
const express = require("express");
const axios = require("axios");

const app = new express();

/**
*
* 在middleware1中,在req.message中添加一个字符串 aaa;
* 在middleware2中,在req.message中添加一个 字符串bbb;
* 在middleware3中,在req.message中添加一个 字符串ccc;
* 当所有内容添加结束后,在middleware1中,通过res返回最终的结果;
*/

const middleware1 = (req,res,next)=>{
req.message = 'aaa';
next();
res.end(req.message);
}

const middleware2 = (req,res,next)=>{
req.message += 'bbb';
next();
}

const middleware3 = (req,res,next)=>{
req.message+= 'ccc';
console.log(req.message);
}

app.use(middleware1,middleware2,middleware3);

app.listen(9898,()=>{
console.log("开启 9898");
});

原理:res.end(res.message); 是最后执行的

3.11.3. Express异步数据实现

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
const express = require("express");
const axios = require("axios");

const app = new express();

/**
*
* 在middleware1中,在req.message中添加一个字符串 aaa;
* 在middleware2中,在req.message中添加一个 字符串bbb;
* 在middleware3中,在req.message中添加一个 字符串ccc;
* 当所有内容添加结束后,在middleware1中,通过res返回最终的结果;
*/

const middleware1 = async (req,res,next)=>{
req.message = 'aaa';
next();

await middleware3(req);

res.end(req.message);
}

const middleware2 = (req,res,next)=>{
req.message += 'bbb';
next();
}

const middleware3 = async (req,res,next)=>{
const result = await axios.get("http://123.207.32.32:9001/lyric?id=167876");
req.message+=result.data.lrc.lyric;
}

app.use(middleware1,middleware2,middleware3);

app.listen(9899,()=>{
console.log("开启 9899");
});

3.11.4. Koa同步数据实现

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
const koa = require("koa");

const app = new koa();

/**
*
* 在middleware1中,在req.message中添加一个字符串 aaa;
* 在middleware2中,在req.message中添加一个 字符串bbb;
* 在middleware3中,在req.message中添加一个 字符串ccc;
* 当所有内容添加结束后,在middleware1中,通过res返回最终的结果;
*/

const middleware1 = (ctx,next)=>{
ctx.request.message = 'aaa';
next();
ctx.response.body = ctx.request.message;
}

const middleware2 = (ctx,next)=>{
ctx.request.message += 'bbb';
next();
}

const middleware3 = (ctx,next)=>{
ctx.request.message += 'ccc';
console.log(ctx.request.message);
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(9900,()=>{
console.log("开启 9900");
});

原理:ctx.response.body = ctx.request.message; 是最后执行的

3.11.5. Koa异步数据实现

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
const koa = require("koa");

const app = new koa();

const axios = require("axios");

/**
*
* 在middleware1中,在req.message中添加一个字符串 aaa;
* 在middleware2中,在req.message中添加一个 字符串bbb;
* 在middleware3中,在req.message中添加一个 字符串ccc;
* 当所有内容添加结束后,在middleware1中,通过res返回最终的结果;
*/

const middleware1 = async (ctx,next)=>{
ctx.message = 'aaa';
await next();
ctx.body = ctx.message;
}

const middleware2 = async (ctx,next)=>{
ctx.message += 'bbb';
await next();
}

const middleware3 = async (ctx,next)=>{
const result = await axios.get("http://123.207.32.32:9001/lyric?id=167876");
ctx.message += result.data.lrc.lyric;
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(9901,()=>{
console.log("开启 9901");
});

3.12. Koa洋葱模型

  • 两层理解含义:
    • 中间件处理代码的过程;
    • response返回body执行;
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
const koa = require("koa");

const app = new koa();

const middleware1 = (ctx,next)=>{
ctx.request.message = 'aaa';
next();
console.log("middleware1");
ctx.response.body = ctx.request.message;
}

const middleware2 = (ctx,next)=>{
ctx.request.message += 'bbb';
next();
console.log("middleware2");
}

const middleware3 = (ctx,next)=>{
ctx.request.message += 'ccc';
console.log("middleware3");
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(9900,()=>{
console.log("开启 9900");
});

控制台输出

1
2
3
4
开启 9900
middleware3
middleware2
middleware1

就像剥洋葱一样,由外往里,再由内往外

本文结束  感谢您的阅读