摘要:本文学习了Express框架,包括安装和简单使用,以及如何防盗链和使用模板引擎渲染页面等等。
环境
Windows 10 企业版 LTSC 21H2
Node 18.14.0
NPM 9.3.1
NVM 1.1.12
Express 4.16.1
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
文件作为首页。
默认情况下使用public
目录作为静态文件目录。
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 简介
纯JS语法的模板语言,支持直接在标签内编写代码逻辑。
6.2.2 安装
官网:
使用npm install ejs
命令安装。
6.2.3 使用
创建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
| <!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://unpkg.com/jquery@3.6.0/dist/jquery.min.js"></script> </head> <body> <p>欢迎 <%= user %></p> <p>课程:</p> <% if (classes && classes.length > 0) { %> <ul> <% for (item of classes) { %> <li><%= item %></li> <% } %> </ul> <% } %> </body> </html>
|
在路由中指定EJS作为模板引擎,并使用EJS渲染模板文件:
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 21 22 23 24 25 26
| <!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://unpkg.com/jquery@3.6.0/dist/jquery.min.js"></script> </head> <body> <form action="/info" method="post" enctype="multipart/form-data"> <div> <label for="username">用户名:</label> <input type="text" id="username" name="username"> </div> <div> <label for="address">居住地:</label> <input type="text" id="address" name="address"> </div> <div> <label for="portrait">头像:</label> <input type="file" id="portrait" name="portrait"> </div> <button type="submit">提交</button> </form> </body> </html>
|
8 会话管理
8.1 Cookie
8.1.1 设置
示例:
js1 2 3 4 5 6 7 8 9 10 11 12 13
| app.get('/info/setCookie', (req, res) => { res.cookie('service', 'info', { maxAge: 600000, path: '/info', httpOnly: true, secure: false, sameSite: 'strict' }); res.send('<h1>设置成功</h1>'); });
|
参数:
- maxAge:设置过期时间,单位是毫秒。
- path:设置作用路径。
- httpOnly:是否禁止JS脚本访问,只允许HTTP请求访问,设置为true可以避免XSS攻击。
- secure:是否只在HTTPS连接下发送。
- sameSite:控制发送级别,发送防止CSRF攻击,可选值为Strict/Lax/None。
8.1.2 读取
读取Cookie需要使用cookie-parser
中间件,使用npm install cookie-parser
命令安装。
示例:
js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('/info/getCookie', (req, res) => { let service = req.cookies.service; if (service) { res.send(` <h1>读取成功</h1> <p>服务名:${service}</p> `); } else { res.send('<h1>读取失败</h1>'); } });
|
8.1.3 删除
示例:
js1 2 3 4 5 6 7 8
| app.get('/info/delCookie', (req, res) => { res.clearCookie('service', { path: '/info' }); res.send('<h1>删除成功</h1>'); });
|
8.2 Session
8.2.1 设置
使用Session需要使用express-session
中间件,使用npm install express-session
命令安装。
存储Session到MongoDB数据库需要使用connect-mongo
中间件,使用npm install connect-mongo
命令安装。
示例:
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
| const express = require('express'); const session = require('express-session'); const mongo = require('connect-mongo');
const app = express();
app.use(session({ name: 'sid', secret: 'salt', saveUninitialized: false, resave: true, cookie: { maxAge: 600000, path: '/info', httpOnly: true, secure: false, sameSite: 'strict' }, store: mongo.create({ mongoUrl: 'mongodb://127.0.0.1:27017/test' }), }));
app.get('/info/setSession', (req, res) => { req.session.service = 'info'; res.send('<h1>设置成功</h1>'); });
|
参数:
- name:设置在Cookie中传输SessionId使用的名字,默认是sid。
- secret:用于加密的字符串,也称为加盐。
- saveUninitialized:是否为每次请求存储Session信息,默认为false,只给建立了Session的请求存储,设置为true可以给匿名用户存储Session信息。
- resave:是否每次请求都存储Session信息,设置为true可以在每次请求时存储Session信息,避免超时导致Session失效。
- cookie:设置Cookie的回传信息。
- store:设置存储Session的数据库信息。
8.2.2 读取
示例:
js1 2 3 4 5 6 7 8 9 10 11 12 13 14
| app.get('/info/getSession', (req, res) => { let service = req.session.service; if (service) { res.send(` <h1>读取成功</h1> <p>服务名:${service}</p> `); } else { res.send('<h1>读取失败</h1>'); } });
|
8.2.3 删除
示例:
js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| app.get('/info/delSession', (req, res) => { req.session.destroy(err => { if (err) { res.send(` <h1>删除失败</h1> <p>失败原因:${err}</p> `); } else { res.clearCookie('service', { path: '/info' }); res.send('<h1>删除成功</h1>'); } }); });
|
9 快速构建
使用npm install express-generator -g
命令全局安装构建工具。
安装后可以执行express -h
命令查看帮助。
使用express -e 目录名
设置构建项目的目录。
进入目录后执行npm install
命令重新安装项目所需要的依赖。
查看package.json
文件查看命令,启动项目。
条