第7章 Node框架实战篇 - Express 中间件与RESTful API 接口规范

Express 不同中间件类别的使用方式
应用级中间件

通过 app.use([path], middleware)app.METHOD([path], middleware)注册。path是可选的,省略时对所有路径生效。

javascript 复制代码
const express = require('express');
const app = express();

// 全局中间件:对所有请求生效
app.use((req, res, next) => {
  console.log('Request URL:', req.originalUrl);
  next(); // 必须调用 next() 将控制权交给下一个中间件
});

// 局部中间件:只对 /user 路径的 GET 请求生效
const specificMiddleware = (req, res, next) => {
  console.log('Accessing /user endpoint');
  next();
};
app.get('/user', specificMiddleware, (req, res) => {
  res.send('User Info');
});
路由级中间件

使用 express.Router()创建一个路由实例,然后像使用 app一样使用这个 router实例。

javascript 复制代码
const express = require('express');
const router = express.Router(); // 创建路由实例

// 在路由实例上使用中间件
router.use((req, res, next) => {
  console.log('Time: ', Date.now());
  next();
});

router.get('/list', (req, res) => {
  res.send('User List');
});

// 最后将路由实例挂载到应用的某个路径下(如 /api)
const app = express();
app.use('/api', router); // 现在访问 /api/user/list 会触发上述中间件和路由
错误处理中间件

必须放在所有路由和其他中间件之后

javascript 复制代码
// 在路由中抛出错误(同步)
app.get('/problematic', (req, res) => {
  throw new Error('Something went wrong!'); // 同步错误直接 throw
});

// 在路由中传递错误(异步)
app.get('/async-problematic', (req, res, next) => {
  someAsyncFunction()
    .then(data => res.send(data))
    .catch(error => next(error)); // 异步错误通过 next(error) 传递
});

// 定义在最后的错误处理中间件
app.use((err, req, res, next) => { // 注意4个参数
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
内置中间件

Express 提供了几个开箱即用的中间件,最常见的是解析请求体和托管静态文件。

javascript 复制代码
const express = require('express');
const app = express();

// 解析 application/json 格式的请求体数据
app.use(express.json());
// 解析 application/x-www-form-urlencoded 格式的请求体数据
app.use(express.urlencoded({ extended: false }));
// 托管静态文件,例如 public 目录下的图片、CSS、HTML
app.use(express.static('public'));

// 使用解析后的数据
app.post('/profile', (req, res) => {
  // 配置了中间件后,req.body 才有值
  console.log(req.body);
  res.json({ message: 'Data received', data: req.body });
});
第三方中间件

首先通过 npm 安装,然后引入并使用。

javascript 复制代码
// 1. 终端运行:npm install cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser'); // 2. 引入
const app = express();

app.use(cookieParser()); // 3. 注册

app.get('/', (req, res) => {
  // 现在可以读取请求中的 cookie 了
  console.log('Cookies: ', req.cookies);
  res.send('Hello World');
});
关键注意事项
执行顺序至关重要

中间件的执行顺序取决于它们在代码中注册(app.use)的顺序。请求会依次经过匹配的中间件。除了错误处理中间件,其他所有中间件(如解析中间件)必须在路由之前配置

调用 next()

如果中间件函数不结束请求-响应周期(比如通过 res.send()),**必须调用 next()**​ 将控制权传递给下一个中间件,否则请求会被挂起。

错误处理中间件的位置

这是一个特例,必须放在所有其他 app.use()和路由调用之后,以确保能捕获到前面所有环节抛出的错误

Express路由与响应方法
类别 方法 主要用途 关键特点
路由定义 app.METHOD(path, handler) 处理特定HTTP方法(GET、POST等)的请求 精准匹配HTTP动词和路径
app.all(path, handler) 处理所有HTTP方法对某路径的请求 适用于全局中间件或路径预处理
app.route(path) 为同一路径创建可链式调用的路由处理程序 避免路径重复书写,代码更清晰
express.Router() 创建模块化的路由处理程序 实现路由的模块化和管理
响应方法 res.send() 发送多种类型数据(Buffer、String、Object、Array) 自动设置Content-Type等响应头
res.json() 发送JSON响应 自动设置Content-Type为application/json
res.end() 快速结束响应,不发送数据 适用于无响应体的场景
res.status() 设置HTTP状态码 通常与其他响应方法链式调用
res.redirect() 重定向请求 可设置状态码(默认302)
路径匹配示例
javascript 复制代码
// 字符串路径:精确匹配
app.get('/about', (req, res) => {
  res.send('About us');
});

// 字符串模式:使用特殊字符
app.get('/ab?cd', (req, res) => { // 匹配 acd 或 abcd
  res.send('Pattern matched');
});
app.get('/ab+cd', (req, res) => { // 匹配 abcd, abbcd, abbbcd 等
  res.send('Pattern matched with plus');
});

// 正则表达式:更灵活的匹配
app.get(/.*fly$/, (req, res) => { // 匹配 butterfly, dragonfly 等
  res.send('Regex matched');
});

// 路由参数:捕获路径中的值
app.get('/users/:userId/books/:bookId', (req, res) => {
  // 通过 req.params 访问捕获的值
  res.send(`User ID: ${req.params.userId}, Book ID: ${req.params.bookId}`);
});
高级路由技巧
链式路由

使用 app.route()为同一路径定义多个方法

javascript 复制代码
app.route('/book')
  .get((req, res) => { res.send('Get a random book'); })
  .post((req, res) => { res.send('Add a book'); })
  .put((req, res) => { res.send('Update the book'); });
模块化路由

使用 express.Router()创建可挂载的模块化路由处理程序

javascript 复制代码
// birds.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => { res.send('Birds home page'); });
router.get('/about', (req, res) => { res.send('About birds'); });

module.exports = router;

// 主文件 app.js
const birdsRouter = require('./birds');
app.use('/birds', birdsRouter); // 现在可以处理 /birds 和 /birds/about 的请求
响应方法详解
核心响应方法使用场景

res.send([body]):最通用的响应方法

javascript 复制代码
// 发送字符串(Content-Type 自动设为 "text/html")
res.send('<p>Some HTML</p>');

// 发送对象或数组(Content-Type 自动设为 "application/json")
res.send({ user: 'tobi' });
res.send([1, 2, 3]);

// 发送 Buffer(Content-Type 自动设为 "application/octet-stream")
res.send(new Buffer('whoop'));

// 结合状态码
res.status(404).send('Sorry, we cannot find that!');

res.json([body]):专门发送JSON响应,会自动将非对象(如null、undefined)转换为JSON

javascript 复制代码
res.json({ user: 'tobi' });
res.status(500).json({ error: 'message' });
res.json(null); // 有效,尽管null不是有效JSON

res.end([data][, encoding]) :快速结束响应,不发送数据。**除非需要无响应体结束,否则优先用 res.send()res.json()**​

javascript 复制代码
res.status(404).end(); // 结束响应,不发送数据

res.redirect([status,] path):重定向

javascript 复制代码
res.redirect('/new-page'); // 默认302
res.redirect(301, '/permanent-new-page'); // 永久重定向
其他实用响应方法
  • res.status(code):设置HTTP状态码,常链式调用
  • res.set(field [, value])res.header(field [, value]):设置响应头
  • res.type(type):设置 Content-Type
  • res.cookie(name, value [, options]):设置 Cookie
进阶应用与性能考量
中间件与路由结合
javascript 复制代码
const logTime = (req, res, next) => {
  console.log('Time:', Date.now());
  next(); // 必须调用 next() 将控制权传递给下一个中间件或路由
};

app.use(logTime); // 应用级中间件,对所有路由生效
app.get('/specific', logTime, (req, res) => { // 路由级中间件
  res.send('Specific route with logging');
});
错误处理

定义错误处理中间件时,函数参数为四个 (err, req, res, next)

javascript 复制代码
app.get('/problematic', (req, res, next) => {
  try {
    // 可能出错的操作
  } catch (error) {
    next(error); // 将错误传递给错误处理中间件
  }
});

// 错误处理中间件(定义在路由之后)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
项目基础设计搭建优化

安装 express

复制代码
pnpm i express -S

router\user.js

定义用户路由

javascript 复制代码
const express = require('express');
const router = express.Router();
const userConroller = require('../controller/user');

router.get('/list', userConroller.list);
router.get('/:id', userConroller.getUserById);

module.exports = router;

controller\user.js

定义用户 controller

javascript 复制代码
// 获取用户列表
const list = (req, res) => {
  res.send('get user list');
};

// 获取指定id的用户
const getUserById = (req, res) => {
  res.send(`get user ${req.params.id}`);
};

module.exports = {
  list,
  getUserById,
};

router\index.js

路由主入口,一次性导出所有路由

javascript 复制代码
const express = require('express');
const router = express.Router();

router.use('/user', require('./user'));
router.use('/video', require('./video'));

module.exports = router;

app.js

应用主入口,进行一些全局配置,例如路由、日志、中间件等

javascript 复制代码
const express = require('express');
const app = express();

// 设置相应数据类型
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 设置路由
app.use(require('./router'));

// 设置静态文件目录
app.use(express.static('public'));

// 监听3000端口
app.listen(3000, () => {
  console.log('服务器已启动,监听端口3000');
});
数据处理模块-mongoose

安装 mongoose

复制代码
pnpm i mongoose@6.3.1 -S

基本使用

model\userModel.js

javascript 复制代码
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
  },
  password: {
    type: String,
    required: true,
  },
  phone: {
    type: String,
    required: true,
  },
  image: {
    type: String,
    default: null,
  },
  createdAt: {
    type: Date,
    default: Date.now(),
  },
  updateAt: {
    type: Date,
    default: Date.now(),
  },
});

module.exports = userSchema;

model\index.js

使用 mongoose 连接数据库,导出所有数据库 Model

javascript 复制代码
const mongoose = require('mongoose');
async function mian() {
  await mongoose.connect('mongodb://localhost:27017/express-video');
}

mian()
  .then((res) => {
    console.log('mongo链接成功');
  })
  .catch((err) => {
    console.log(err);
    console.log('mongo链接失败');
  });

module.exports = {
  User: mongoose.model('User', require('./userModel')),
};
用户注册数据入库

router\user.js

用户注册路由

javascript 复制代码
const express = require('express');
const router = express.Router();
const userConroller = require('../controller/user');

router.post('/register', userConroller.register); 

module.exports = router;

controller\user.js

处理与用户路由相关的业务逻辑

javascript 复制代码
const { User } = require('../model');
// 用户注册
const register = async (req, res) => {
  // 创建用户 model 并存储
  const user = new User(req.body);
  try {
    await user.save();
    res.status(200).send('注册成功');
  } catch (error) {
    res.status(400).send(error.message);
  }
};

module.exports = {
  register,
};

使用 postman 测试用户注册功能

用户注册的密码加密问题

使用node内置的crypto库封装 md5 加密

util\md5.js

javascript 复制代码
const crypto = require('crypto')
module.exports = str => {
  return crypto.createHash('md5')
    .update('by' + str)
    .digest('hex')
}

model\userModel.js

使用封装好的 md5 加密

javascript 复制代码
const md5 = require('../util/md5');
password: {
    type: String,
    required: true,
    set: (val) => {
      return md5(val);
    },
  }
客户端提交数据安全校验

安装 express-validator 库,用于提交参数校验

javascript 复制代码
pnpm i express-validator@6.14 -S

middleware\validator\errorBack.js

封装统一错误请求处理函数

javascript 复制代码
const { validationResult } = require('express-validator');

// 错误处理中间件
module.exports = (validator) => {
  return async (req, res, next) => {
    // 执行验证器
    await Promise.all(validator.map((valid) => valid.run(req)));
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).send(errors.array()[0].msg);
    }
    next();
  };
};

middleware\validator\userValidator.js

javascript 复制代码
const { body } = require('express-validator');
const validate = require('./errorBack');

// 用户注册验证规则
module.exports.register = validate([
  body('username')
    .notEmpty()
    .withMessage('用户名不能为空')
    .bail()
    .isLength({ min: 3 })
    .withMessage('用户名长度最小为3')
    .bail(),
  body('password')
    .notEmpty()
    .withMessage('密码不能为空')
    .bail()
    .isLength({ min: 6, max: 16 })
    .withMessage('密码长度最小为6,最大为16')
    .bail(),
  body('email')
    .notEmpty()
    .withMessage('邮箱不能为空')
    .bail()
    .isEmail()
    .withMessage('邮箱格式不正确')
    .bail(),
]);

router\user.js

在用户路由使用上述封装好的用户参数校验工具

javascript 复制代码
const express = require('express');
const router = express.Router();
const userConroller = require('../controller/user');
const validator = require('../middleware/validator/userValidator');

router.post('/register', validator.register, userConroller.register);

module.exports = router;
数据唯一性验证

middleware\validator\userValidator.js

通过自定义校验规则验证邮箱和手机号是否已注册

javascript 复制代码
body('email')
    .notEmpty()
    .withMessage('邮箱不能为空')
    .bail()
    .isEmail()
    .withMessage('邮箱格式不正确')
    .custom((value) => {
      return User.findOne({ email: value }).then((user) => {
        if (user) {
          return Promise.reject('邮箱已被注册');
        }
      });
    })
    .bail(),
  body('phone')
    .notEmpty()
    .withMessage('手机不能为空')
    .bail()
    .custom((value) => {
      return User.findOne({ phone: value }).then((user) => {
        if (user) {
          return Promise.reject('手机号已被注册');
        }
      });
    })
    .bail()
用户登录信息对比

验证登录用户信息:邮箱是否已注册

middleware\validator\userValidator.js

javascript 复制代码
// 用户登录验证规则
module.exports.login = validate([
  body('email')
    .notEmpty()
    .withMessage('邮箱不能为空')
    .bail()
    .isEmail()
    .withMessage('邮箱格式不正确')
    .custom((value) => {
      return User.findOne({ email: value }).then((user) => {
        if (!user) {
          return Promise.reject('邮箱未注册');
        }
      });
    })
    .bail(),
  body('password')
    .notEmpty()
    .withMessage('密码不能为空')
    .bail()
    .isLength({ min: 6, max: 16 })
    .withMessage('密码长度最小为6,最大为16')
    .bail(),
]);
JWT用户身份认证

安装 jsonwebtoken

javascript 复制代码
pnpm i jsonwebtoken - S

util\jwt.js

封装生成token以及校验token工具函数

javascript 复制代码
const jwt = require('jsonwebtoken');
const { promisify } = require('util');
const { uuid } = require('../config/config.default');
// jwt.sign 转换为promise
const toJwt = promisify(jwt.sign);
// jwt.verify 转换为promise
const verifyJwt = promisify(jwt.verify);

// 生成 token
module.exports.createToken = async (user) => {
  const token = await toJwt({ user }, uuid, {
    expiresIn: 60 * 60,
  });
  return token;
};

// jwt 验证
module.exports.verifyToken = async (req, res, next) => {
  //从 header 读取 token, 并解析 token
  const authorization = req.headers.authorization;
  const token = authorization ? authorization.split('Bearer ')[1] : null;
  if (!token) {
    return res.status(401).send('token 不存在');
  }
  try {
    const user = await verifyJwt(token, uuid);
    next();
  } catch (error) {
    res.status(401).send('token 验证失败');
  }
};

controller\user.js

用户登录成功后生成token并返回

javascript 复制代码
const { createToken } = require('../util/jwt');
const login = async (req, res) => {
  try {
    const dbBack = await User.findOne({
      email: req.body.email,
      password: req.body.password,
    });
    if (dbBack) {
      // 将数据库返回的数据转换为json格式
      const user = dbBack.toJSON();
      // 生成 token
      const token = await createToken(user);
      user.token = token;
      res.status(200).json(user);
    } else {
      res.status(400).json('账号密码错误');
    }
  } catch (error) {
    res.status(400).json(error.message);
  }
};

router\user.js

在其他需要登录状态下访问的接口添加校验token 中间件

javascript 复制代码
const { verifyToken } = require('../util/jwt');
// 用户列表
router.get('/list', verifyToken, userConroller.list);
相关推荐
百***464518 小时前
SocketTool、串口调试助手、MQTT中间件基础
单片机·嵌入式硬件·中间件
zhangbaolin1 天前
深度智能体的中间件
中间件·langchain·大模型·深度智能体
whltaoin1 天前
【微服务中间件】RabbitMQ 多平台安装搭建实践指南(Windows_macOS_Ubuntu_Docker 全场景)
微服务·中间件·消息队列·rabbitmq·多平台
q***71851 天前
开源数据同步中间件(Dbsyncer)简单玩一下 mysql to mysql 的增量,全量配置
mysql·中间件·开源
Alex艾力的IT数字空间2 天前
完整事务性能瓶颈分析案例:支付系统事务雪崩优化
开发语言·数据结构·数据库·分布式·算法·中间件·php
q***72193 天前
国产化中间件东方通TongWeb环境安装部署(图文详解)
中间件
无心水3 天前
【中间件:Redis】5、Redis分布式锁实战:从基础实现到Redisson高级版(避坑指南)
redis·分布式·中间件·redisson·后端面试·redis分布式锁·分布式系统
q***47433 天前
【服务治理中间件】consul介绍和基本原理
中间件·consul
无心水3 天前
【中间件:Redis】3、Redis数据安全机制:持久化(RDB+AOF)+事务+原子性(面试3大考点)
redis·中间件·面试·后端面试·redis事务·redis持久化·redis原子性