一文搞懂 Express:使用、路由、中间件与源码

0 阅读9分钟

本篇文章将带你深入了解 Express 框架,从安装配置到如何使用中间件、路由、请求响应、错误处理等,再到源码层面的解析,帮助你全方位掌握 Express。

一、Express 认识初体验

1. 什么是 Express?

Express 是一个简洁且灵活的 Web 应用框架,基于 Node.js,帮助开发者快速搭建 Web 应用程序和 RESTful API。其最大的特点是轻量级和高度可扩展,广泛应用于构建小型到中型的 Web 应用。

Express 提供了大量的中间件,方便处理 HTTP 请求、响应,简化了服务器端开发的复杂度。

Express 是 Node.js 最流行的 Web 框架,基于中间件架构设计,提供以下核心功能:

  • 路由管理:灵活处理 HTTP 请求
  • 中间件支持:模块化处理请求生命周期
  • 模板引擎集成:支持 Pug/EJS 等视图渲染
  • 错误处理:统一错误捕获机制

2. 安装 Express

首先,我们需要初始化一个 Node.js 项目并安装 Express。 也可以使用Express 应用程序生成器

npm init -y        # 初始化一个 Node.js 项目
npm install express --save  # 安装 Express 框架

安装完成后,在项目根目录下会生成 node_modules 文件夹和 package.json 文件。

3. 创建一个简单的服务器

通过 Express 创建一个简单的 Web 服务器,监听 HTTP 请求。

const express = require('express');
const app = express();
app.get('/', (req, res) => {
  res.send('Hello, Express!');
});
app.get('/login', (req, res) => {
  //处理loginc操作
  res.send('登录成功,太好了!');
});
app.listen(3000, () => {
  console.log('Server is running at http://localhost:3000');
});

在浏览器中访问 http://localhost:3000,你会看到页面显示 "Hello, Express!"。


二、Express 中间件使用

1. 什么是中间件?

中间件是 Express 中最核心的概念之一,它是指在请求和响应周期之间的处理逻辑。每个中间件可以执行以下任务:

  • 修改请求对象(req)或响应对象(res)
  • 执行一些代码
  • 结束请求响应循环
  • 调用下一个中间件(next函数(在express中定义的用于执行下一个中间件的函数))

执行顺序:按注册顺序依次执行

功能分类

image.png

const express = require('express');
const app = express();

// 给express创建的app传入一个回调函数
// 传入的这个回调函数就称之为是中间件(middleware)
// 如果当前中间件功能没有结束请求-响应周期,则必须调用next()将控制权传递给下一个中间件功能,否则,请求将被挂起
app.use((req, res, next) => {
  console.log('中间件被调用了');
  next();
});

app.post('/login', (req, res, next) => {
  // 中间件中可以执行任意代码,打印,查询数据,判断逻辑
  // 在中间件中修改reqres对象
  // 在中间件中结束响应周期
  console.log('中间件被调用了登录');
  // res.send('登录成功,太好了!');
  res.json({ code: 0, msg: '登录成功' })
  // next();
});

app.listen(3000, () => {
  console.log('Server is running at http://localhost:3000');
});

2. 使用中间件

image.png 使用中间件的基本语法如下:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();  // 调用下一个中间件
});

// 通过use方法注册的中间件是最普通的1简单的中间件
// 通过use注册的中间件,无论是什么请求方式都可以匹配上
app.use((req, res, next) => {
  console.log("中间件1")
  next()
})
app.use((req, res, next) => {
  console.log("中间件2")
   res.json({ message: '测试中间件返回' })
})

总结:

  • 当express接收到客户端发送的网络请求时,在所有中间中开始进行匹配
  • 当匹配到第一个符合要求的中间件时,那么就会执行这个中间件
  • 后续的中间件是否会执行,取决于上一个中间件有没有执行next

3. 常见中间件类型

  • 应用级中间件:作用于所有请求或特定路由,如 app.use()app.get() 注册。
  • 路由级中间件:通过 express.Router() 创建,限定在特定路由上使用。
  • 内置中间件:Express 提供了一些内置中间件,如 express.json()express.static() 等。
  • 错误处理中间件:用于捕捉并处理应用中的错误。

4. 例子:静态文件中间件

Express 提供了 express.static 中间件用于处理静态资源(如 HTML、CSS、JS 文件等):

app.use(express.static('public'));  // 将 public 文件夹作为静态资源目录

中间件开发实战

多种中间件基础使用

//...
// 注册普通的中间件
// app.use((req,res,next)=>{
//   console.log('普通通用,中间件1');
//   res.end('--------');
// })

// 注册路径匹配的中间件
// 路径匹配的中间件是不会对请求方式(method)进行限制
// app.use("/home",(req,res,next)=>{
//   console.log('home接口中间件');
//   res.end('home接口返回');
// })

// 注册中间件:对path/method都有限制
// app.get("/home",(req,res,next)=>{
//   console.log('home接口和请求方式中间件');
//   res.end('home接口和请求方式方式返回');
// })

// 注册多个中间件,可以持续后面接中间件。  
//通常用在复杂接口操作数据中进行分割局部完成事件使用,避免放在同一个中间件里
app.get("/home",(req,res,next)=>{
  console.log('home get中间件1');
  next();
},(req,res,next)=>{
  console.log('home get中间件2');
})
//....

参数解析实例

// 应用中间件解析参数
app.use(express.json());//解析客户端传递过来的json
// 1.解析传递过来urlencoded的时候,,默认使用的node内置querystring模块
//{extended:true}:不再使用内置的querystring,而是使用qs第三方库
app.use(express.urlencoded({ extended: false }));//解析客户端传递过来的urlencoded

app.post("/login", (req, res) => {
  console.log(req.body)
  res.send("登录成功")
})

formData参数解析

const formdata = multer()
app.post("/login",formdata.any(), (req, res) => {
  console.log(req.body)
  res.send("登录成功")
})
// 自定义日志中间件
const logger = (req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${res.statusCode} [${duration}ms]`);
  });
  next();
};

// 应用中间件
app.use(logger);
app.use(express.json()); // 内置JSON解析

上传文件实例

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');

const app = express();
app.use(cors()); // 跨域支持

const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

// 设置文件存储路径和文件名
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, uploadDir); // 保存路径
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    const ext = path.extname(file.originalname);
    cb(null, file.fieldname + '-' + uniqueSuffix + ext);
  }
});

const upload = multer({ storage: storage });

app.post("/avatar", upload.single('file'), (req, res) => {
  console.log(req.file);
  res.send({
    message: '上传成功',
    filename: req.file.filename,
    url: `/uploads/${req.file.filename}`
  });
});

// 静态资源托管(访问图片用)
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

app.listen(3000, () => {
  console.log('Server is running at http://localhost:3000');
});

多文件上传只需用 upload.array() 替换 upload.single(),其余部分变化不大

// 使用express内置中间件
app.post("/avatar", upload.array('file',3), (req, res, next) => {
  console.log(req.files); // 注意是 req.files(数组)
  const fileInfos = req.files.map(file => ({
    filename: file.filename,
    url: `/uploads/${file.filename}`
  }));
  res.send({
    message: '上传成功',
    files: fileInfos
  });
})

三、Express 请求和响应

1. 获取请求数据

Express 提供了丰富的 API 来处理不同类型的请求数据:

  • req.query:获取 URL 查询参数。
  • req.body:获取请求体数据(需配合 express.json()express.urlencoded() 中间件)。
  • req.params:获取 URL 路由参数。
app.use(express.json());  // 解析 JSON 格式的请求体

app.post('/user', (req, res) => {
  console.log(req.body);  // 打印请求体
  res.send('User received');
});

2. 设置响应数据

Express 的 res 对象提供了多种方法来设置响应内容:

  • res.send():发送普通文本或 HTML 内容。
  • res.json():发送 JSON 格式的数据。
  • res.status():设置 HTTP 状态码。
app.get('/user', (req, res) => {
  res.status(200).json({ name: 'Express', age: 5 });
});

3. 设置响应头

有时我们需要设置一些自定义的响应头,Express 也提供了简便的方式:

res.set('X-Custom-Header', 'MyValue');

四、Express 路由的使用

1. 定义基本路由

Express 提供了简单的路由定义方法,如 app.get()app.post()app.put() 等:

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

app.post('/users', (req, res) => {
  res.send('Create user');
});

2. 路由参数

我们可以使用路由参数来捕获动态数据:

app.get('/user/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`User ID: ${userId}`);
});

访问 /user/123 会返回 User ID: 123

解析querystring

app.get('/user', (req, res) => {
  const queryInfo = req.query;
  console.log("🚀 ~ app.get ~ queryInfo:", queryInfo)
  res.send(queryInfo);
});

其他参数解析参考上文中间件的操作使用

3. 使用 Router 模块化路由

为了更好地组织代码,可以使用 express.Router() 来创建模块化的路由。

const userRouter = express.Router();
//路由映射可以抽取为单独文件夹导入
userRouter.get('/', (req, res) => res.send('User Home'));
userRouter.get('/info', (req, res) => res.send('User Info'));

app.use('/user', userRouter);  // 将路由挂载到 /user 路径下

五、Express 的错误处理

1. 错误处理中间件

错误处理中间件是处理应用程序错误的关键。其特殊之处在于,它接受四个参数:err, req, res, next

app.use((err, req, res, next) => {
  console.error(err.stack);  // 打印错误栈
  res.status(500).send('Something broke!');
});

实际使用实例

const express = require('express');
const app = express();

// 支持 JSON 和表单数据
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 登录接口
app.post('/login', (req, res, next) => {
  const { name, password } = req.body;

  // 校验用户名和密码
  if (!name || !password) {
    return next(new Error('请输入用户名或密码'));
  }

  if (name !== "linxi" || password !== "123456") {
    const err = new Error('用户名或密码错误');
    err.status = 400;
    return next(err);
  }

  // 登录成功
  res.status(200).json({
    code: 200,
    msg: '登录成功',
    token: "sadjanb4qwesada5"
  });
});

// 统一错误处理
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({
    code: err.status || 500,
    msg: err.message || '服务器内部错误'
  });
});

// 启动服务
app.listen(3000, () => {
  console.log('Server is running at http://localhost:3000');
});

2. 抛出错误

我们可以通过 next(err) 手动抛出错误,并交由错误处理中间件处理。

app.get('/', (req, res, next) => {
  const error = new Error('Oops! Something went wrong');
  next(error);  // 将错误传递给错误处理中间件
});

3. 捕捉异步错误

对于异步操作的错误,我们可以通过 async/awaittry/catch 来捕获异常。

app.get('/async', async (req, res, next) => {
  try {
    const data = await someAsyncOperation();
    res.json(data);
  } catch (err) {
    next(err);
  }
});

4. 日志记录

使用morgan npm install morgan搭配了fs去写出日志文件,不明白的移步学习fs可写流

const fs = require('fs')
const express = require('express');
const morgan = require('morgan')
const app = express();
// 应用第三方中间件
const writeStream = fs.createWriteStream('./log.txt', { flags: 'a' })
app.use(morgan('combined', { stream: writeStream }))
app.post("/login", (req, res) => {
  res.send("登录成功")
})
app.listen(3000, () => {
  console.log('Server is running at http://localhost:3000');
});

六、Express 源码解析

核心架构剖析

image.png

1. 项目结构

Express 的核心代码位于 lib/ 目录下。最重要的文件是 express.js,它定义了 Express 应用的核心逻辑。其主要功能是返回一个可以处理请求和响应的应用实例。

通过 createApplication() 函数返回一个应用实例。该函数会在内部初始化中间件、路由和错误处理机制。

module.exports = function createApplication() {
  const app = function(req, res, next) {
    app.handle(req, res, next);  // 请求处理
  }
  app.handle = function(req, res, next) {
    // 处理请求并调用路由或中间件
  };
  return app;
}

2. 路由和中间件的管理

在 Express 中,中间件是按顺序执行的。每个中间件执行后,必须调用 next(),否则请求会停留在当前中间件中,无法传递给下一个中间件。

Express 通过 Router 模块来管理路由和中间件。在 app.use()app.get() 方法中注册的中间件都会被添加到 app._router 中。

app.use = function use(fn) {
  this._router.use(fn);  // 注册中间件
  return this;
}

3. 请求与响应对象

Express 会对 reqres 对象进行封装,提供更多有用的方法来处理请求和响应。req 中包含了 URL、请求头、请求体等信息;res 对象则提供了 send()json()status() 等方法来处理响应。

4.由的匹配机制

Express 在内部通过 router.match() 方法进行路由匹配。当请求到来时,框架会遍历所有注册的路由,直到找到第一个匹配的路由为止。