抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文主要学习了Express框架,包括安装和简单使用,以及如何防盗链和使用模板引擎渲染页面等等。

环境

Windows 10 企业版 LTSC 21H2
Node 18.14.0
NPM 9.3.1
NVM 1.1.12
Express 4.21.2
EJS 3.1.10
Formidable 3.5.2

1 初识

Express是一个基于Node平台的极简且灵活的Web应用框架,它提供了强大且丰富的功能,如路由定义、中间件使用、静态文件服务等,帮助开发者快速构建Web应用和API接口。

官方网址:https://www.expressjs.com.cn/

2 安装

创建项目目录并使用npm init命令初始化。

使用npm install express命令安装Express框架。

在项目根目录创建app.js文件:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入模块
const express = require('express');
// 创建实例
const app = express();
// 创建路由
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
res.end('hello');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

在命令行执行node app.js命令启动服务,在浏览器访问http://127.0.0.1:3000/请求服务。

3 路由

3.1 概念

路由是指根据不同的URL路径和请求方法,将请求映射到相应的处理函数。

在Express中,路由定义是应用的核心部分,它决定了如何响应客户端的请求。

3.2 初体验

语法:

js
1
app.method(path, handler)

参数:

  • app:是Express的一个实例。
  • method:小写的请求方法。
  • path:服务器上的路径。
  • handler:路由匹配时执行的函数。

示例:

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
// 创建get默认路由
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
res.end('index');
});
// 创建get路由
app.get('/home', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
res.end('home');
});
// 创建post路由
app.post('/login', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
res.end('login');
});
// 创建公共路由,允许所有请求方法
app.all('/search', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
res.end('search');
});
// 匹配所有路由规则
app.all('*', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
res.end('404');
});

3.3 路由参数

路由参数是指在定义路由时,在路径中指定的动态部分,用于捕获URL中的特定值。这些参数可以被路由处理函数访问,用于根据动态值生成响应。

路由参数在路径中以:开头,后面跟参数名称,用来获取URL中对应位置的数据。

示例:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const users = [{id:'1', name:'张三', sex:'男'}, {id:'2', name:'李四', sex:'女'}];
// 路由参数
app.get('/search/:id/:name', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset=UTF-8');
// 获取路由参数
let params = req.params;
console.log('路由参数: ' + JSON.stringify(params));
// 根据路由参数查询数据
let user = users.find(user => user.id == params.id && user.name == params.name);
if (user) {
console.log('查询用户: ' + JSON.stringify(user));
res.end(JSON.stringify(user));
} else {
console.log('查无此人');
res.end('查无此人');
}
});

在浏览器访问http://127.0.0.1:3000/search/1/张三请求服务,控制台信息:

bash
1
2
路由参数: {"id":"1","name":"张三"}
查询用户: {"id":"1","name":"张三","sex":"男"}

3.4 优化请求参数

在原生http模块的基础上增加了获取请求信息的方式:

js
1
2
3
4
5
6
7
8
9
10
// 获得请求协议类型
console.log(req.protocol);
// 获取请求URL中的路径
console.log(req.path);
// 获得请求URL中的查询字符串
console.log(req.query);
// 获得请求头的数据
console.log(req.get('host'));
// 获得请求路由参数
console.log(req.params);

3.5 优化响应参数

在原生http模块的基础上增加了设置响应内容的方式:

js
1
2
3
4
5
6
// 设置响应状态码,默认是200
res.status(200);
// 设置响应头
res.set('Content-Test', 'test');
// 设置响应体,无需设置UTF-8中文也不会乱码
res.send('搜索');

也支持支持链式调用:

js
1
2
// 同时设置响应状态码、响应头、响应体
res.status(200).set('Content-Test', 'test').send('搜索');

不能同时使用Node原生方式和Express新增方式返回响应。

除了使用send方法,还支持其他响应设置:

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
// 获取路由参数
let params = req.params;
// 根据路由参数设置响应
switch (params.type) {
case 'redirect':
// 设置重定向
res.redirect('http://www.baidu.com');
break;
case 'download':
// 设置下载
res.download(path.resolve(__dirname, 'index.html'));
break;
case 'json':
// 设置JSON
res.json({id:'1', name:'张三', sex:'男'});
break;
case 'file':
// 设置文件,根据文件后缀自动设置MIME类型
res.sendFile(path.resolve(__dirname, 'index.html'));
break;
default:
// 设置默认方式
res.send('不支持的响应类型');
break;
}

4 中间件

4.1 概念

中间件是一种回调函数,可以访问请求对象、响应对象以及指向下一个中间件。

中间件用于执行各种任务,如日志记录、身份验证、数据解析等。

4.2 分类

按照使用位置,可以分为全局中间件和路由中间件:

  • 全局中间件:定义在应用里,对所有路由生效。
  • 路由中间件:定义在路由上,对当前路由生效。

按照处理类型,可以分为静态中间件和动态中间件:

  • 静态中间件:对静态资源生效,静态中间件一般都是全局中间件。
  • 动态中间件:对非静态的资源生效。

按照创建类型,可以分为自定义中间件和内置中间件:

  • 内置中间件:内置的中间件,比如静态中间件。
  • 自定义中间件:自定义或者第三方创建的中间件。

4.3 使用

4.3.1 全局中间件

示例:

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
// 引入模块
const express = require('express');
// 创建实例
const app = express();
// 创建方法作为中间件,接收参数
function operateLog(req, res, next) {
// 记录日志
let date = new Date().toLocaleString();
let path = req.path.slice(1);
console.log(`操作日志: ${date} ${path}`);
// 继续执行后续的中间件和路由
next();
}
// 调用方法作为中间件
app.use(operateLog);
// 新增操作
app.all('/insert', (req, res) => {
res.send('新增成功');
});
// 查询操作
app.all('/select', (req, res) => {
res.send('查询成功');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

4.3.2 路由中间件

示例:

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
// 引入模块
const express = require('express');
// 创建实例
const app = express();
// 创建方法作为中间件,接收参数
function insertLog(req, res, next) {
// 记录日志
let date = new Date().toLocaleString();
let path = req.path.slice(1);
console.log(`新增日志: ${date} ${path}`);
// 继续执行后续的中间件和路由
next();
}
// 新增操作,调用方法作为路由中间件
app.all('/insert', insertLog, (req, res) => {
res.send('新增成功');
});
// 查询操作
app.all('/select', (req, res) => {
res.send('查询成功');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

4.3.3 静态中间件

示例:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引入模块
const express = require('express');
// 创建实例
const app = express();
// 使用内置的static()方法作为中间件,指定静态文件目录
app.use(express.static(require('path').resolve(__dirname, 'home')));
// 其他请求
app.all('*', (req, res) => {
res.send('处理成功');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

如果访问的是静态文件目录的文件资源,会通过静态中间件找到文件并返回。如果访问的是其他资源,会判断其他路由并返回。

在浏览器访问http://127.0.0.1:3000/默认会在静态资源目录寻找index.html文件,所以可以将index.html文件作为首页。

4.3.4 组合中间件

示例:

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
// 引入模块
const express = require('express');
// 创建实例
const app = express();
// 静态资源使用静态中间件
app.use(express.static(require('path').resolve(__dirname, 'home')));
// 创建自定义中间件
let check = (req, res, next) => {
console.log();
if (req.query.password == 123456) {
next();
} else {
res.send('密码验证失败');
}
}
// 其他请求使用自定义中间件
app.all('*', check, (req, res) => {
res.send('处理成功');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

组合匹配的情况下,按照代码顺序匹配路由。

4.4 解析请求体

支持两种方式解析请求体:

  • 使用express.urlencoded({extended: false})获得解析application/x-www-form-urlencoded格式请求体的中间件。
  • 使用express.json()获得解析application/json格式请求体的中间件。

使用:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 引入模块
const express = require('express');
// 创建实例
const app = express();
// 使用中间件解析请求体
app.use(express.urlencoded({extended: false}));
app.use(express.json());
// 解析后会在req的body中添加请求数据
app.post('/login', (req, res) => {
console.log(req.body);
res.send('处理成功');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

4.5 模块化

将路由分成多个模块,统一进行导入和使用。

示例:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 引入模块
const express = require('express');
const path = require('path');
// 创建实例
const app = express();
// 导入路由器模块
const userRouter = require(path.resolve(__dirname, 'routes/userRouter.js'));
const adminRouter = require(path.resolve(__dirname, 'routes/adminRouter.js'));
// 设置全局中间件,挂载路由器
app.use(userRouter);
app.use(adminRouter);
// 处理业务
app.all('*', function (req, res) {
res.send('处理成功');
})
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

编写userRouter.js文件,示例:

userRouter.js
1
2
3
4
5
6
7
8
9
10
// 引入模块
const express = require('express');
// 创建路由器
const router = express.Router();
// 处理业务
router.get('/login', (req, res) => {
res.send('登录');
});
// 暴露路由
module.exports = router;

编写adminRouter.js文件,示例:

adminRouter.js
1
2
3
4
5
6
7
8
9
10
// 引入模块
const express = require('express');
// 创建路由器
const router = express.Router();
// 处理业务
router.get('/setting', (req, res) => {
res.send('设置');
});
// 暴露路由
module.exports = router;

在挂载路由器模块的时候,如果该模块拥有共同访问前缀,可以在挂载的时候进行设置:

js
1
2
3
// 设置全局中间件,挂载路由器,使用前缀
app.use('/user', userRouter);
app.use('/admin', adminRouter);

使用前缀后,访问的时候需要拼接前缀访问。

4.6 错误处理

在Express中,路由的回调方法中除了可以传入req对象和res对象,还支持传入next方法,使用next方法处理路由错误。

4.6.1 异步代码错误

异步代码中的错误需要使用next方法手动处理,默认会将请求挂起直至超时:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 引入模块
const express = require('express');
const fs = require('fs');
// 创建实例
const app = express();
// 处理请求,传入next处理错误
app.get('/read', (req, res, next) => {
fs.readFile('./info.txt', (err, data) => {
if (err) {
// 使用next将错误返回前端并打印到控制台
next(err);
// 避免执行后续逻辑
return;
}
res.send(data.toString());
});
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

4.6.2 同步代码错误

同步代码中发生的错误不需要手动处理,默认会将错误返回前端并打印到控制台:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引入模块
const express = require('express');
const fs = require('fs');
// 创建实例
const app = express();
// 处理请求
app.get('/read', (req, res) => {
let data = fs.readFileSync('./info.txt');
res.send(data.toString());
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

也可以手动捕获错误并使用next方法处理错误:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入模块
const express = require('express');
const fs = require('fs');
// 创建实例
const app = express();
// 处理请求,传入next处理错误
app.get('/read', (req, res, next) => {
// 使用try-catch捕获错误
try {
let data = fs.readFileSync('./info.txt');
res.send(data.toString());
} catch (err) {
// 使用next将错误返回前端并打印到控制台
next(err);
}
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

4.6.3 错误处理中间件

错误处理中间件需要四个参数,三个参数的中间件将被解释为常规中间件,无法处理错误。

在路由方法中或者在常规中间件中如果使用next方法处理了错误,会忽略之后的常规中间件,只调用错误处理中间件。

示例:

js
1
2
3
4
5
6
7
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: '服务器内部错误',
message: err.message,
});
});

注意要让错误处理中间件最后执行:

  • 对于全局中间件来说,根据从上到下的执行规则,要将错误处理中间件写在最下面,保证经过所有路由后再执行错误处理中间件。
  • 对于局部中间件来说,根据从左到右的执行规则,要将错误处理中间件挂载到路由链的末尾,保证最后执行。

5 防盗链

为了防止外部网站盗用网站资源,可以对网站的资源做防盗链处理,防止直接复制图片地址进行下载。

请求头里的referer参数会携带当前域名和协议及其端口进行请求,根据这个特点就可以进行防盗链处理。

示例:

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
// 引入模块
const express = require('express');
const path = require('path');
// 创建实例
const app = express();
// 拦截列表
const extArr = ['.jpg', '.jpeg', '.png', '.gif', 'bmp'];
const urlArr = ['127.0.0.1'];
// 使用中间件防盗链
app.use((req, res, next) => {
// 判断是否属于资源文件
if (!extArr.includes(path.extname(req.url))) {
next();
return;
}
// 获得referer参数
const referer = req.get('referer');
if (referer) {
// 如果存在referer参数,判断referer的host是否为本机
let url = new URL(referer);
if (urlArr.includes(url.hostname)) {
next();
return;
}
} else {
// 如果不存在referer参数,判断accept参数是否以image起始
const accept = req.get('accept');
if (accept && !accept.startsWith('image')) {
next();
return;
}
}
res.status(404).send('本资源涉嫌盗链,请访问原网站');
});
// 使用中间件处理静态资源
app.use(express.static(path.resolve(__dirname, 'home')));
// 使用路由处理动态数据
app.all('*', (req, res) => {
res.send('处理成功');
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

6 模板引擎

6.1 概念

模板引擎是分离用户界面和业务数据的一种技术,可以将后端的JS文件和前端的HTML文件结合起来,生成最终的网页或视图,发送给客户端。

模板引擎用于将前端和后端分离,但现在已经能够通过其他技术做到分离了,所以模版引擎用得较少了。

Express支持多种常用的模板引擎,以下是几种常见的模板引擎及其特点:

  • EJS(Embedded JavaScript):EJS使用纯JS语法作为模板语言,易于上手和学习,支持逻辑控制和模板继承等功能。
  • Pug(原名Jade):Pug是一种类似于缩进的模板语言,使用简洁的语法来定义HTML结构,减少了标签的书写,适合编写简洁和易读的模板。
  • Handlebars:Handlebars提供了灵活的模板语法,支持条件判断、循环、局部块等功能,适合构建复杂的模板结构。

6.2 EJS

6.2.1 安装

官网:

使用npm install ejs命令安装。

6.2.2 使用

创建views目录存放模板文件,并在views目录中创建welcome.ejs模板文件:

welcome.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>

<body>
<p>欢迎 <%= user %></p>
<p>课程:</p>
<% if (classes && classes.length > 0) { %>
<ul>
<% for (item of classes) { %>
<li><%= item %></p>
<% } %>
</ul>
<% } %>
</body>

</html>

示例:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入模块
const express = require('express');
const path = require('path');
// 创建实例
const app = express();
// 设置模板引擎
app.set('view engine', 'ejs');
// 设置模板文件目录
app.set('views', path.resolve(__dirname, 'views'));
// 处理业务
app.get('/welcome', function (req, res) {
// 读取数据
let data = {user:'张三', classes:['语文', '数学', '英语']};
// 渲染数据
res.render('welcome', data);
})
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

在浏览器访问http://127.0.0.1:3000/welcome会看到渲染后的页面。

7 文件上传

7.1 安装

使用npm install formidable命令安装。

7.2 使用

使用formidableformidable方法创建表单对象并指定文件存储位置,然后使用表单对象的parse方法解析上传文件,得到存储的地址并返回前端。

示例:

app.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
// 引入模块
const express = require('express');
const path = require('path');
const formidable = require('formidable');
// 创建实例
const app = express();
// 设置模板引擎
app.set('view engine', 'ejs');
// 设置模板文件目录
app.set('views', path.resolve(__dirname, 'views'));
// 模板文件路由
app.get('/info', (req, res) => {
res.render('info');
});
// 文件上传路由
app.post('/info', (req, res, next) => {
// 创建表单对象
const form = formidable.formidable({
// 文件保存路径
uploadDir: path.resolve(__dirname, 'uploads'),
// 保留文件扩展名
keepExtensions: true,
// 过滤器
filter: ({name, mimetype}) => {
// 只允许图片类型
return mimetype && mimetype.includes('image');
},
});
// 使用表单对象解析上传文件
form.parse(req, (err, fields, files) => {
// 处理错误
if (err) {
next(err);
return;
}
// 解析上传文件
let urls = Object.fromEntries(
Object.entries(files).map(
([name, value]) => [name, value.map(file => `/uploads/${file.newFilename}`)]
)
);
// 打印并返回
console.log(fields);
console.log(urls);
res.send(JSON.stringify({fields, urls}));
});
});
// 监听端口
app.listen(3000, () => {
console.log('服务已经启动');
console.log('http://127.0.0.1:3000/');
});

views目录中创建info.ejs模板文件:

info.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>

<body>
<form action="/info" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"><br>
居住地:<input type="text" name="address"><br>
头像:<input type="file" name="portrait"><br>
<button>提交</button>
</form>
</body>

</html>

8 快速构建

使用npm install express-generator -g命令全局安装构建工具。

安装后可以执行express -h命令查看帮助。

使用express -e 目录名设置构建项目的目录。

进入目录后执行npm install命令重新安装项目所需要的依赖。

查看package.json文件查看命令,启动项目。

评论