摘要:本文主要学习了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.js1 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 初体验
语法:
js1
| app.method(path, handler)
|
参数:
- app:是Express的一个实例。
- method:小写的请求方法。
- path:服务器上的路径。
- handler:路由匹配时执行的函数。
示例:
js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| app.get('/', (req, res) => { res.setHeader('Content-Type', 'text/html;charset=UTF-8'); res.end('index'); });
app.get('/home', (req, res) => { res.setHeader('Content-Type', 'text/html;charset=UTF-8'); res.end('home'); });
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中对应位置的数据。
示例:
js1 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/张三
请求服务,控制台信息:
bash1 2
| 路由参数: {"id":"1","name":"张三"} 查询用户: {"id":"1","name":"张三","sex":"男"}
|
3.4 优化请求参数
在原生http
模块的基础上增加了获取请求信息的方式:
js1 2 3 4 5 6 7 8 9 10
| console.log(req.protocol);
console.log(req.path);
console.log(req.query);
console.log(req.get('host'));
console.log(req.params);
|
3.5 优化响应参数
在原生http
模块的基础上增加了设置响应内容的方式:
js1 2 3 4 5 6
| res.status(200);
res.set('Content-Test', 'test');
res.send('搜索');
|
也支持支持链式调用:
js1 2
| res.status(200).set('Content-Test', 'test').send('搜索');
|
不能同时使用Node原生方式和Express新增方式返回响应。
除了使用send
方法,还支持其他响应设置:
js1 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': res.json({id:'1', name:'张三', sex:'男'}); break; case 'file': res.sendFile(path.resolve(__dirname, 'index.html')); break; default: res.send('不支持的响应类型'); break; }
|
4 中间件
4.1 概念
中间件是一种回调函数,可以访问请求对象、响应对象以及指向下一个中间件。
中间件用于执行各种任务,如日志记录、身份验证、数据解析等。
4.2 分类
按照使用位置,可以分为全局中间件和路由中间件:
- 全局中间件:定义在应用里,对所有路由生效。
- 路由中间件:定义在路由上,对当前路由生效。
按照处理类型,可以分为静态中间件和动态中间件:
- 静态中间件:对静态资源生效,静态中间件一般都是全局中间件。
- 动态中间件:对非静态的资源生效。
按照创建类型,可以分为自定义中间件和内置中间件:
- 内置中间件:内置的中间件,比如静态中间件。
- 自定义中间件:自定义或者第三方创建的中间件。
4.3 使用
4.3.1 全局中间件
示例:
js1 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 路由中间件
示例:
js1 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 静态中间件
示例:
js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const express = require('express');
const app = express();
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 组合中间件
示例:
js1 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
格式请求体的中间件。
使用:
js1 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());
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.js1 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.js1 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.js1 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;
|
在挂载路由器模块的时候,如果该模块拥有共同访问前缀,可以在挂载的时候进行设置:
js1 2 3
| app.use('/user', userRouter); app.use('/admin', adminRouter);
|
使用前缀后,访问的时候需要拼接前缀访问。
4.6 错误处理
在Express中,路由的回调方法中除了可以传入req
对象和res
对象,还支持传入next
方法,使用next
方法处理路由错误。
4.6.1 异步代码错误
异步代码中的错误需要使用next
方法手动处理,默认会将请求挂起直至超时:
js1 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();
app.get('/read', (req, res, next) => { fs.readFile('./info.txt', (err, data) => { if (err) { next(err); return; } res.send(data.toString()); }); });
app.listen(3000, () => { console.log('服务已经启动'); console.log('http://127.0.0.1:3000/'); });
|
4.6.2 同步代码错误
同步代码中发生的错误不需要手动处理,默认会将错误返回前端并打印到控制台:
js1 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
方法处理错误:
js1 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();
app.get('/read', (req, res, next) => { try { let data = fs.readFileSync('./info.txt'); res.send(data.toString()); } catch (err) { next(err); } });
app.listen(3000, () => { console.log('服务已经启动'); console.log('http://127.0.0.1:3000/'); });
|
4.6.3 错误处理中间件
错误处理中间件需要四个参数,三个参数的中间件将被解释为常规中间件,无法处理错误。
在路由方法中或者在常规中间件中如果使用next
方法处理了错误,会忽略之后的常规中间件,只调用错误处理中间件。
示例:
js1 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
参数会携带当前域名和协议及其端口进行请求,根据这个特点就可以进行防盗链处理。
示例:
js1 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; } const referer = req.get('referer'); if (referer) { let url = new URL(referer); if (urlArr.includes(url.hostname)) { next(); return; } } else { 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.ejs1 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>
|
示例:
js1 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 使用
使用formidable
的formidable
方法创建表单对象并指定文件存储位置,然后使用表单对象的parse
方法解析上传文件,得到存储的地址并返回前端。
示例:
app.js1 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.ejs1 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
文件查看命令,启动项目。
条