前言

这篇会介绍使用express的基本使用

为什么要使用express

因为http模块来开发,有一些问题

  1. 在实际开发中,我们要根据不同请求路径,不同请求方法做不同的事情,处理起来比较麻烦
  2. 读取请求体和写入响应体是通过流的方式,使用起来比较麻烦

所以我们一般会使用一些框架,常用的有

  • express
  • koa

这里只介绍express

官方文档:https://expressjs.com/

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

express的基本使用

首先,你要安装一下express

1
npm install express --save

添加下面的代码

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
// index.js
const express = require("express");
// 创建一个express应用
const app = express();
const port = 5555;

// express会建立一个请求映射表,如果请求方法和请求路径均满足匹配,交给处理函数进行处理
// 配置请求映射的方式:app.请求方法("请求路径", 处理函数)
// req和res都是被express封装过后的对象
app.get("/data/:id", (req, res) => {

// 获取请求信息
console.log("请求头", req.headers);
console.log("请求路径", req.path);
console.log("query", req.query);
console.log("params", req.params);

// 响应
// send会在内部调用end方法
res.setHeader("a", "123");
res.setHeader("b", "456");
res.send({
name : "SakuraSnow"
});
// 重定向, 这里的end和http模块里的end是一样的意思,标志消息体的结束
// res.status(302).header("location", "https://baidu.com").end();
// res.status(302).location("https://baidu.com").end();
// res.redirect(302, "https://baidu.com");

});

// 匹配任何get请求
app.get("*", (req, res) => {
console.log("abc");
res.send({
name : "Snow"
})
});

// 开启服务器
app.listen(port, () => {
console.log(`server listen on ${port}`);
});

要创建一个简单的服务器,你只需要做下面几件事

  • 创建一个express应用
  • 配置请求映射
  • 开启服务器

是不是很简单呢

express中间件

如果你用过redux,那你一定对中间件不陌生(虽然我已经把react忘得差不多了),中间件本质上就是一个函数,一般来说用于给系统添加某些功能(比如添加CORS)或者处理某些数据

中间件有下面的特点

  • 按顺序执行;
  • 可执行任何脚本;
  • 可以对request对象和response对象进行overwrite;
  • 可以响应请求以结束本次请求生命周期;
  • 通过next方法执行下一个中间件;

我们一般使用use表示使用中间件,和get等不同的是,它设置的字段,路径只要以它开头就行,比get和post等更广

比如使用app.use(“/data”),可以匹配到的地址包括”/data”,”/data/a”, “/data/b”

image-20210126200356926

下面是简单的中间件使用示例

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
// index.js
const express = require("express");
// 创建一个express应用
const app = express();
const port = 5555;

// express会建立一个请求映射表,如果请求方法和请求路径均满足匹配,交给处理函数进行处理
// 配置请求映射的方式:app.请求方法("请求路径", 处理函数)
// req和res都是被express封装过后的对象
// 中间件的写法很简单,写三个参数就行
app.get("/data/:id", (req, res, next) => {
console.log("handler0")
// 获取请求信息
console.log("请求头", req.headers);
console.log("请求路径", req.path);
console.log("query", req.query);
console.log("params", req.params);

// 请求移交给下一个中间件
next()
});

app.get("/data/:id", (req, res, next) => {
console.log("handler1")
res.setHeader("a", "123");
next()
})
app.get("/data/:id", (req, res, next) => {
console.log("handler2")
res.setHeader("b", "456");
next()
});

app.get("/data/:id", (req, res, next) => {
console.log("handler3")
res.send({
name : "SakuraSnow"
});
// 如果在最后一个中间件中都没有处理res,express会响应404并结束请求
// 如果已经处理(调用了res.end), 后续中间件依旧会执行,但是后续不能再处理,否则会报错
})

// 匹配任何get请求
app.get("*", (req, res) => {
console.log("abc");
res.send({
name : "Snow"
})
});


app.listen(port, () => {
console.log(`server listen on ${port}`);
});

用postman访问,服务器会打印下面的日志

image-20210126201801740

可以看出,中间件会按绑定的顺序执行

另外,你可以在中间件中抛出错误(或者真的有预料之外的错误)

express就会寻找后续的错误处理中间件,把控制权转交给错误处理中间件

如果没有,服务器就会响应500

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
// index.js
const express = require("express");
// 创建一个express应用
const app = express();
const port = 5555;

// express会建立一个请求映射表,如果请求方法和请求路径均满足匹配,交给处理函数进行处理
// 配置请求映射的方式:app.请求方法("请求路径", 处理函数)
// req和res都是被express封装过后的对象
// 你可以通过next把控制权交给下一个中间件
// 如果在最后一个中间件中都没有处理res,express会响应404并结束请求
// 如果已经处理(调用了res.end), 后续中间件依旧会执行,但是后续不能再处理,否则会报错
app.get("/data/:id", (req, res, next) => {
console.log("handler0")
// 获取请求信息
console.log("请求头", req.headers);
console.log("请求路径", req.path);
console.log("query", req.query);
console.log("params", req.params);
res.send({
name : "SakuraSnow"
});
});

app.get("/error", (req, res, next) => {
// 另外你可以使用下面的方式抛出错误
// express会寻找后续的错误处理中间件,然后把错误传入
// 如果没有,后续的中间件都会全部被跳过
// throw new Error("rua");
// next(new Error("rua"));
throw new Error("something wrong")
})
// 处理404
app.use('*', (req, res) => {
res.status(404).send({
code: 404,
success: false,
data: '',
msg: 'the directory you request is not exist in the server'
})
})
// 处理error, 只要接收四个参数就代表这是一个错误处理中间件
app.use("*", (err, req, res, next) => {
res.status(500).send({
code: 500,
success: false,
data: '',
msg: err.toString()
})
})

app.listen(port, () => {
console.log(`server listen on ${port}`);
});

express常用中间件

静态资源中间件express.static

使用方法很简单,加上下面的代码即可

1
2
3
4
5
6
7
8
9
const path = require("path");
// 静态文件目录
const staticRoot = path.resolve(__dirname, "./public");

// 当请求时,会根据请求路径(req.path(如果使用了use绑定中间件,req.baseUrl就是绑定时的路径, 也叫基路径,req.path就是全路径去掉基路径后的路径)),
// 从指定的目录中寻找是否存在该文件,如果存在,直接响应文件内容,而不再移交给后续的中间件
// 如果不存在文件,则直接移交给后续的中间件处理
// 默认情况下,如果映射的结果是一个目录,则会自动使用index.html文件
app.use("/static", express.static(staticRoot));

访问http://localhost:5555/static/index.html,就可以看到页面展示出来了

image-20210126212205686

解析application/x-www-form-urlencoded格式的数据

express.urlencoded可以解析格式为application/x-www-form-urlencoded的数据并放入到req.body中

假如有下面的代码

1
2
3
4
5
6
7
8
app.post("/data/:id", (req, res, next) => {
console.log("query", req.query);
console.log("params", req.params);
console.log("body", req.body);
res.send({
name : "Snow"
})
})

在postman中,我们对这个接口进行测试

image-20210126213109726

在服务器控制台中打印数据,可以看到下面的结果

1
2
3
query {}
params { id: '1' }
body undefined

这里的body是undefined,是不是意味着请求失败了呢,其实不是,因为我们有时候服务器的请求体里会带上一个文件,所以我们要用流的方式来解析请求体,这里express没有给我们进行处理,但是大多数情况下,我们都不会在post请求里发文件,所以,我们有时更希望能直接解析消息体,这时候就要用到我们的express.urlencoded中间件了

使用方法也很简单(这个extended选项用于标志内部使用qs库来解析消息体)

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

加入后,再次请求

1
2
3
query {}
params { id: '1' }
body { data: '123' }

这样就可以直接解析了

事实上,它的工作原理大概就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const qs = require("querystring");
module.exports = (req, res, next) => {
if (req.headers["content-type"] === "application/x-www-form-urlencoded") {
//自行解析消息体
let result = "";
req.on("data", (chunk) => {
result += chunk.toString("utf-8");
});
req.on("end", () => {
// 解析完后把解析结果放到body里
const query = qs.parse(result);
req.body = query;
next();
});
} else {
next();
}
};

解析application/json格式的数据

express.json可以解析application/json的数据并放入到req.body中

但有时我们传递的消息体是这样的

image-20210126215258014

这时我们就要使用另外一个中间件express.json了

使用方法还是很简单

1
app.use(express.json());

最后放个完整代码

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
// index.js
const express = require("express");
const path = require("path");
// 创建一个express应用
const app = express();
const port = 5555;
const staticRoot = path.resolve(__dirname, "./public");

// 把这些中间件放在前面
// 映射public目录中的静态资源
app.use("/static", express.static(staticRoot));
// 解析 application/x-www-form-urlencoded 格式的请求体
app.use(express.urlencoded({ extended: true }));
// 解析 application/json 格式的请求体
app.use(express.json());

app.get("/data/:id", (req, res, next) => {
// 获取请求信息
console.log("请求头", req.headers);
console.log("请求路径", req.path);
console.log("query", req.query);
console.log("params", req.params);
res.send({
name : "SakuraSnow"
});
});

app.post("/data/:id", (req, res, next) => {
console.log("head", req.headers)
console.log("query", req.query);
console.log("params", req.params);
console.log("body", req.body);
res.send({
name : "Snow"
})
})

app.get("/error", (req, res, next) => {
throw new Error("something wrong")
})

app.use('*', (req, res) => {
res.status(404).send({
code: 404,
success: false,
data: '',
msg: 'the directory you request is not exist in the server'
})
})

app.use("*", (err, req, res, next) => {
res.status(500).send({
code: 500,
success: false,
data: '',
msg: err.toString()
})
})

app.listen(port, () => {
console.log(`server listen on ${port}`);
});

解析multipart/form-data格式的数据(文件上传)

Multer 用于解析multipart/form-data

文档看这里:https://github.com/expressjs/multer

Multer 会添加一个 body 对象 以及 filefiles 对象 到 express 的 request 对象中。 body 对象包含表单的文本域信息,filefiles 对象包含对象表单上传的文件信息。

安装

1
cnpm install multer --save

使用

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
// /api/upload/index.js
const express = require("express");
const router = express.Router();
const multer = require("multer");
const path = require("path");
const storage = multer.diskStorage({
// 用于存储文件,设置存储路径
destination: function (req, file, cb) {
cb(null, path.resolve(__dirname, "../../public/upload"));
},
// 生成要存储的文件的文件名
filename: function (req, file, cb) {
// 时间戳-6位随机字符.文件后缀
const timeStamp = Date.now();
const randomStr = Math.random().toString(36).slice(-6);
const ext = path.extname(file.originalname);
const filename = `${timeStamp}-${randomStr}${ext}`;
cb(null, filename);
},
});

const upload = multer({
storage,
limits: {
// 限制文件上传的大小
fileSize: 1024 * 1024,
},
// 验证扩展名
fileFilter(req, file, cb) {
//验证文件后缀名
const extname = path.extname(file.originalname);
const whitelist = [".jpg", ".gif", ".png"];
if (whitelist.includes(extname)) {
cb(null, true);
} else {
cb(new Error(`your ext name of ${extname} is not support`));
}
},
});

router.post("/uploadFile", upload.single("img"), (req, res) => {
console.log("body", req.body);
console.log("file", req.file);
// req.file.filename是生成的文件名
// req.file.fieldname是在form-data中的文件名,但是框架会自动处理form-data中的文件,所以很少用
const url = `/static/upload/${req.file.filename}`;
res.send({
code: 0,
msg: "",
src: url,
});
});

module.exports = router;

在index.js中导入

1
app.use("/", require("./api/upload"));

写个页面测试下

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Document</title>
<link rel="stylesheet" href="./css/index.css"/>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.js" defer></script>
</head>
<body>
<input type="file" id="fileInput"/>
<button class="button">上传</button>
<img src=""/>
<script>
let fileInput = document.querySelector("#fileInput");
let button = document.querySelector(".button");
let img = document.querySelector("img");
button.addEventListener("click", async () => {
let formData = new FormData();
formData.append("img", fileInput.files[0]);
let data = await axios.post("http://localhost:5555/uploadFile", formData);
img.src = data.data.src;
})
</script>
</body>
</html>

测试成功

image-20210127214853537

express路由

我们可以使用路由对接口进行管理,而不是用代码堆在一起

就像下面的代码

1
2
3
4
5
// 处理 api 的请求
app.use("/api/student", require("./api/student"));
// app.use("/api/book", require("./api/book"));
// app.use("/api/class", require("./api/class"));
// app.use("/api/admin", require("./api/admin"));

然后我们可以把学生相关的api都提取起来

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
// student/index
const express = require("express");
const router = express.Router();

router.get(
"/:id",
(req, res) => {
// 获取学生...
}
);

router.delete(
"/:id",
(req, res) => {
// 添加学生...
}
);

router.delete(
"/:id",
(req, res) => {
// 删除学生...
}
);

module.exports = router;

这样收集起来,就可以很方便管理

文件下载

其实非常简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// download.js
const express = require("express");
const path = require("path");
const router = express.Router();

router.get("/:filename", (req, res) => {
const absolutePath = path.resolve(
__dirname,
"../../resources",
req.params.filename
);
// 第一个参数是下载路径,第二个参数是默认文件名
// 另外,如果请求头中有range,就只会读取对应部分的内容(流传输嘛), 这样就可以断点续传了
res.download(absolutePath, req.params.filename);
});

module.exports = router;
1
2
// index.js
app.use("/download", require("./api/download"));

然后访问浏览器:http://localhost:5555/download/head.jpg

浏览器就可以进行下载了

image-20210127221512226

CORS

啊哈,可以康康我这篇文章,好久前写过了,这里就不复制粘贴了

Cookie是身份认证的重要一环,在这里我们使用一个中间件cookie-parse来设置cookie,

安装

1
npm install cookie-parser --save

安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 加入cookie-parser 中间件
// 加入之后,会在req对象中注入cookies属性,用于获取所有请求传递过来的cookie
// 加入之后,会在res对象中注入cookie方法,用于设置cookie
const cookieParser = require("cookie-parser");
app.use(cookieParser());
app.get("/data/:id", (req, res, next) => {
// 添加的cookie对象,包含了客户端传来的cookie
console.log("cookie", req.cookies);
res.cookie("token", "abc123", {
path: "/", // 路径
domain: "localhost", // 域名
maxAge: 7 * 24 * 3600 * 1000, //毫秒数
});
res.send({
name : "SakuraSnow"
});
});

如果你想了解cookie的更多信息,可以参见另一篇文章:

Session

Session一般也用来身份认证,不过它和Cookie有略微的区别

这里简单介绍下Session的技术原理和优缺点

先介绍下Session的大致原理,先放点祖传图

image

image-20210127181831248

大致流程是这样的

  1. 服务器创建一个表,记录sessionId和数据对象的映射关系
  2. 客户端登录后,服务器生成一个新的sessionId(一般会进行加密处理)并返回给客户端
  3. 客户端保存这个sessionId(一般是存在Cookie里)
  4. 客户端之后的每次请求,都带上这个sessionId
  5. 服务器接收到sessionId后进行解析,拿到sessionId对应的数据对象

然后我们介绍下Session的优缺点

先看看Cookie有什么特点吧

  • 存储在客户端
  • 优点
    • 存储在客户端,不占用服务器资源
  • 缺点
    • 只能是字符串格式
    • 存储量有限
    • 数据不安全,容易被获取,篡改
    • 数据容易丢失
    • 每个请求都会带在HTTP请求头中,影响性能
    • 要是用户端禁用Cookie,就会失效

然后我们再介绍下Session的特点

  • 真正的数据存储在服务端,对客户端不可见

  • 优点

    • 可以是任何格式
    • 存储量理论上是无限的(只要内存或者硬盘够的话)
    • 数据比较安全,难以获取和篡改
    • 数据不容易丢失
    • 请求时不会带上很长的信息
    • 在用户端禁用Cookie的情况下还是可以使用(使用URL重写)

JWT

JWT嘛,和前面两个不大一样,它的全程叫Json Web Token,非要翻译的话,应该是JSON格式的互联网令牌。嘛,翻译起来怪怪的,感觉少了点内味

暂且不提这个,JWT就是一个安全可靠的令牌格式

以此,你可以把它存储到任何地方,无论是cookie还是localstorage,如果客户端不是浏览器,而是桌面端应用,也可以把它存在文本中

对于传输,你可以使用任何传输方式来传输JWT,一般来说,我们会使用消息头来传输它

大致过程如下

  1. 服务器给客户端响应一个JWT令牌
  2. 客户端自行存储JWT令牌
  3. 在网络请求时,按照约定的方式带上JWT令牌
  4. 服务器获取令牌并解析

JWT令牌由三个部分组成,分别是:

  1. header:令牌头部,记录了整个令牌的类型和签名算法
  2. payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
  3. signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改

它们组合而成的完整格式是:header.payload.signature

比如,一个完整的jwt令牌如下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

它各个部分的值分别是:

  • header:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • payload:eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9
  • signature: BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

另外,服务器可以验证令牌是否被篡改

验证方式非常简单,就是对header+payload用同样的秘钥和加密算法进行重新加密

然后把加密的结果和传入JWT的signature进行对比,如果完全相同,则表示前面两部分没有动过,就是自己颁发的,如果不同,肯定是被篡改过了。

所以,总结一下JWT的特点

  • JWT本质上是一种令牌格式。它和终端设备无关,同样和服务器无关,甚至与如何传输无关,它只是规范了令牌的格式而已
  • JWT由三部分组成:header、payload、signature。主体信息在payload
  • JWT难以被篡改和伪造。这是因为有第三部分的签名存在。

后记

不知道写啥,祝您身体健康)