前言
在 Node.js 原生开发中,我们可以使用内置 http 模块搭建 Web 服务,但原生 API 存在明显短板:需要手动判断请求路径、请求方式、解析请求参数、处理跨域与异常,代码臃肿、耦合度高、维护困难。
Express 是 Node.js 生态中最经典、使用最广泛的轻量级 Web 开发框架,它基于原生 http 模块二次封装,以中间件为核心,简化了路由管理、参数解析、文件上传、静态资源托管、异常处理等全流程操作,生态完善、上手简单,可用于开发接口服务、网站后端、静态服务器、小程序后端等各类场景。
一、Express 简介 & 环境搭建
1.1 为什么选择 Express
- 简化原生操作:无需手动处理 HTTP 请求头、流数据、路由匹配,框架内部封装完成;
- 中间件架构:功能可插拔,灵活扩展能力,第三方中间件生态极其丰富;
- 路由解耦:支持模块化路由,大型项目结构清晰;
- 轻量高效:本身只保留核心能力,按需引入功能,性能开销小;
- 社区成熟:文档完善、问题解决方案多,企业项目使用率极高。
Node 主流 Web 框架对比:Express(老牌稳定)、Koa(基于 Promise,语法更优雅)、NestJS(企业级全框架,TS 优先)。本文主打 Express 核心用法。
1.2 两种项目搭建方式
方式一:使用官方脚手架 express-generator(企业快速建项目)
脚手架会自动生成标准化目录结构、基础配置、路由文件,适合快速启动正式项目。
1)全局安装脚手架
npm install -g express-generator
2)创建 Express 项目
bash
# 创建名为 express-project 的项目
express express-project
3)启动项目
bash
# 进入项目目录
cd express-project
# 安装依赖
npm install
# 启动服务
node bin/www
目录简单说明
bin/www:服务启动入口文件;routes/:路由目录,自动拆分路由模块;public/:默认静态资源目录;views/:模板引擎目录(服务端渲染使用)。
方式二:从零手动搭建(推荐学习、自定义项目)
手动搭建能理解每一行代码作用,学习阶段首选。
1)初始化项目 & 安装 Express
csharp
# 初始化 package.json
npm init -y
# 安装 express 生产依赖
npm install express
二、Express 基础入门(核心语法)
2.1 创建基础 Web 服务
完整可运行代码,包含创建实例、注册接口、监听端口三大核心步骤。
js
// 1. 引入 express 模块
const express = require('express');
// 2. 创建 Express 服务实例 app(整个项目的核心对象)
const app = express();
// 3. 定义监听端口
const PORT = 8000;
// 4. 注册接口:app.请求方式(路由地址, 回调函数)
// GET 请求接口
app.get('/', (req, res) => {
res.end('欢迎访问 Express 首页');
});
// POST 请求接口
app.post('/login', (req, res) => {
res.end('登录接口请求成功');
});
// 5. 监听端口,启动服务器
app.listen(PORT, () => {
console.log(`服务器启动成功,访问地址:http://localhost:${PORT}`);
});
核心对象说明
app:Express 实例,所有路由、中间件、配置都基于该对象;req(Request):请求对象,客户端传给服务端的所有数据(参数、请求头、IP、路径等);res(Response):响应对象,服务端返回给客户端的数据(文本、JSON、状态码、文件等)。
2.2 常用 HTTP 请求方法
Express 支持所有标准 HTTP 请求方式,开发中最常用 5 种:
js
// GET:查询数据
app.get('/user', (req, res) => res.send('查询用户'));
// POST:新增数据、提交表单
app.post('/user', (req, res) => res.send('新增用户'));
// PUT:全量更新数据
app.put('/user/:id', (req, res) => res.send('修改用户'));
// PATCH:局部更新数据
app.patch('/user/:id', (req, res) => res.send('局部修改用户'));
// DELETE:删除数据
app.delete('/user/:id', (req, res) => res.send('删除用户'));
2.3 路由分类 & 动态路由
路由的本质:根据请求地址 + 请求方式,匹配对应的处理函数。
2.3.1 静态路由
地址完全固定,精准匹配:
js
// 仅匹配:http://localhost:8000/about
app.get('/about', (req, res) => {
res.send('关于我们页面');
});
2.3.2 动态路由(路径参数)
路由中使用 :参数名 定义动态参数,用于匹配可变路径,例如用户 ID、文章 ID、商品 ID。
- 取值方式:
req.params.参数名 - 支持多个动态参数
js
// 单个动态参数:匹配 /user/1001、/user/2002
app.get('/user/:userId', (req, res) => {
// 获取路径参数
const userId = req.params.userId;
res.json({
code: 200,
msg: '获取用户ID成功',
userId
});
});
// 多个动态参数:匹配 /order/100/200
app.get('/order/:orderId/:goodsId', (req, res) => {
const { orderId, goodsId } = req.params;
res.json({ orderId, goodsId });
});
2.3.3 通配符路由(模糊匹配)
用于统一处理 404 页面、模糊路径匹配,* 代表通配符。
js
// 匹配所有未定义的接口,统一返回 404
app.get('*', (req, res) => {
res.status(404).send('404 页面不存在');
});
2.4 响应客户端的常用 API
Express 在原生 res.end 基础上封装了大量便捷响应方法,开发高频使用。
1)res.send() 通用响应
支持字符串、HTML 片段、Buffer,自动设置响应头,是最常用的基础方法。
js
app.get('/send-demo', (req, res) => {
// 响应普通文本
res.send('普通文本内容');
// 响应 HTML 片段
// res.send('<h1>这是HTML标题</h1>');
});
2)res.json() 响应 JSON(接口开发首选)
自动将对象/数组转为 JSON 字符串,自动设置 Content-Type: application/json,前后端接口必备。
js
app.get('/json-demo', (req, res) => {
const data = {
code: 200,
message: '请求成功',
data: [1, 2, 3]
};
res.json(data);
});
3)res.status() 设置 HTTP 状态码
HTTP 状态码:2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务端错误)。
res.status(状态码) 支持链式调用。
js
app.get('/status-demo', (req, res) => {
// 400 参数错误、401 未登录、403 无权限、404 地址不存在、500 服务异常
res.status(400).json({ code: 400, msg: '参数错误' });
});
4)res.redirect() 重定向
跳转到指定地址(站内/站外地址均可)。
js
// 访问 /go 自动跳转到 /home
app.get('/go', (req, res) => {
res.redirect('/home');
});
5)res.sendFile() 响应本地文件
直接返回服务器本地文件(HTML、图片、文档等)。
js
const path = require('path');
app.get('/file', (req, res) => {
// __dirname:当前文件所在绝对路径
const filePath = path.join(__dirname, 'index.html');
res.sendFile(filePath);
});
三、Express 核心:中间件(重中之重)
中间件是 Express 的灵魂 ,整个 Express 应用的执行流程就是一串中间件依次执行。所有路由、参数解析、日志、跨域本质都是中间件。
3.1 中间件概念与语法
3.1.1 定义
中间件是一个回调函数,固定接收三个参数:
req:请求对象res:响应对象next:放行函数(核心)
3.1.2 中间件能做什么
- 执行任意业务逻辑(打印日志、身份校验);
- 修改
req/res对象(挂载参数、扩展方法); - 直接结束请求响应(返回数据);
- 通过
next()放行,执行下一个中间件/路由。
3.1.3 核心规则
如果当前中间件没有调用 res 相关方法结束响应 ,必须执行 next(),否则请求会被挂起(浏览器一直转圈)。
3.2 中间件分类(按使用场景划分)
3.2.1 全局中间件(app.use())
使用 app.use(回调函数) 注册,所有客户端请求都会先走全局中间件,挂载顺序从上到下依次执行。
示例1:全局日志中间件
统一打印所有请求的路径、请求方式:
js
const express = require('express');
const app = express();
// 全局中间件:所有请求都会执行
app.use((req, res, next) => {
console.log(`请求方式:${req.method},请求地址:${req.url}`);
// 放行,执行后续代码
next();
});
// 路由接口
app.get('/home', (req, res) => {
res.send('首页');
});
app.listen(8000);
示例2:全局身份校验(模拟登录拦截)
js
// 全局拦截:模拟校验 token
app.use((req, res, next) => {
const token = req.headers.token;
if (!token) {
// 未登录,直接结束响应,不再执行后续路由
return res.status(401).json({ msg: '未登录,请先授权' });
}
next();
});
// 以下所有接口都会被身份拦截
app.get('/user-info', (req, res) => {
res.send('用户信息');
});
3.2.2 路由级中间件(指定路径生效)
app.use(路径, 回调),仅匹配指定路径的请求才会执行。
js
// 仅 /user 开头的请求执行该中间件
app.use('/user', (req, res, next) => {
console.log('进入用户模块中间件');
next();
});
// 会触发中间件
app.get('/user/list', (req, res) => res.send('用户列表'));
// 不会触发中间件
app.get('/order/list', (req, res) => res.send('订单列表'));
3.2.3 路由内中间件(单个接口专属)
写在接口路由内部,仅对当前接口生效,支持注册多个中间件。
js
// 单个接口多个中间件
app.get(
'/api/test',
// 第一个中间件
(req, res, next) => {
console.log('接口前置校验1');
next();
},
// 第二个中间件
(req, res, next) => {
console.log('接口前置校验2');
next();
},
// 最终接口处理函数
(req, res) => {
res.send('接口响应成功');
}
);
3.2.4 错误处理中间件(专属四参数)
专门捕获全局异常,固定 4 个参数 (err, req, res, next),必须放在所有路由、中间件最后。
js
// 模拟异常接口
app.get('/error', (req, res, next) => {
// 抛出错误,交给错误中间件处理
const err = new Error('服务器内部异常');
next(err);
});
// 全局错误处理中间件(四参数)
app.use((err, req, res, next) => {
console.error('错误信息:', err.message);
// 统一格式返回错误
res.status(500).json({
code: 500,
msg: err.message
});
});
3.3 中间件执行顺序(重点考点)
- 代码从上到下依次执行;
- 全局中间件 > 路由级中间件 > 路由内中间件 > 接口处理函数;
- 一旦调用
res.send/res.json等结束响应的方法,后续中间件不再执行; - 错误中间件只有调用
next(错误对象)时才会触发。
四、五大客户端传参方式(全场景覆盖)
客户端向服务端传递参数一共有 5 种主流方式,对应不同解析方案,是接口开发的核心内容。
前置说明
所有 POST 请求的请求体参数,都需要对应解析中间件 才能通过 req.body 获取。
4.1 方式一:URL 查询参数(query 参数)
场景
地址栏拼接 ?键=值&键=值,常用于搜索、分页、筛选。
- 地址示例:
http://localhost:8000/search?keyword=express&page=1&size=10 - 取值:
req.query(Express 原生自动解析,无需额外中间件)
js
app.get('/search', (req, res) => {
// 解构获取参数
const { keyword, page, size } = req.query;
res.json({
关键词: keyword,
页码: page,
条数: size
});
});
4.2 方式二:动态路径参数(params 参数)
前文动态路由已讲解,适用于 ID、唯一标识类参数。
- 地址:
/user/1001 - 取值:
req.params
4.3 方式三:POST JSON 格式参数(接口最常用)
前端使用 axios/fetch 发送 JSON 数据,请求头 Content-Type: application/json。
解析中间件
express.json()(Express 内置),必须在路由前挂载。
js
const express = require('express');
const app = express();
// 解析 JSON 格式请求体(必须放在路由前面)
app.use(express.json());
// 接收 JSON 格式 POST 请求
app.post('/login', (req, res) => {
// req.body 获取 JSON 参数
const { username, password } = req.body;
res.json({
code: 200,
msg: '登录成功',
username,
password
});
});
app.listen(8000);
4.4 方式四:POST 表单格式(x-www-form-urlencoded)
传统网页 form 表单默认格式,Content-Type: application/x-www-form-urlencoded。
解析中间件
express.urlencoded({ extended: true })
js
const express = require('express');
const app = express();
// 解析普通表单参数
app.use(express.urlencoded({ extended: true }));
app.post('/form', (req, res) => {
const { username, password } = req.body;
res.json({ msg: '表单接收成功', data: req.body });
});
参数说明:extended: true 允许解析复杂对象、数组,固定配置即可。
4.5 方式五:POST form-data 格式(文件上传+混合表单)
专门用于文件上传 ,也可传递普通文本参数,请求头 Content-Type: multipart/form-data。
Express 无内置解析能力,使用第三方中间件 multer。
4.5.1 安装 multer
npm install multer
4.5.2 基础单文件上传
js
const express = require('express');
const multer = require('multer');
const app = express();
// 配置:文件保存到 uploads 文件夹
const upload = multer({ dest: 'uploads/' });
// single('file') 对应前端文件字段名
app.post('/upload', upload.single('file'), (req, res) => {
// req.file:文件信息
console.log(req.file);
res.json({ msg: '文件上传成功' });
});
app.listen(8000);
4.5.3 自定义文件名 & 存储路径(解决重名覆盖)
结合 multer.diskStorage 自定义存储规则,使用时间戳防止文件重名:
js
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 自定义存储规则
const storage = multer.diskStorage({
// 定义存储目录
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
// 定义文件名:时间戳 + 原文件后缀
filename: (req, file, cb) => {
const suffix = path.extname(file.originalname);
const fileName = Date.now() + suffix;
cb(null, fileName);
}
});
// 应用配置
const upload = multer({ storage });
app.post('/upload-file', upload.single('file'), (req, res) => {
res.json({ msg: '自定义文件名上传成功' });
});
4.5.4 解析 form-data 普通文本参数
使用 upload.any() 解析表单中的普通字段:
js
const upload = multer();
app.post('/formdata-login', upload.any(), (req, res) => {
// 普通参数依旧在 req.body
res.json({ data: req.body });
});
upload.any()和其他multer的方法的差别如下:
| 方法 | 作用 | 适用场景 |
|---|---|---|
upload.single('file') |
只接收 1 个文件,字段名必须是 file |
单文件上传,固定字段 |
upload.array('img', 5) |
接收最多 5 个文件,统一字段名 img |
多图批量上传 |
upload.fields([{name:'a'},{name:'b'}]) |
接收指定几个固定字段的文件 | 页面多个上传框,字段已知 |
upload.any() |
不限制字段名、不限制文件数量,所有文件全部接收 | 不确定前端上传字段、动态字段 |
💡提示:any方法由于对什么都不进行限制,所以生产环境慎用!
4.6 补充:手动解析 Body(理解底层原理)
不使用内置中间件,基于 Node 流手动解析 JSON 数据,面试常考:
js
app.use((req, res, next) => {
// 判断请求类型为 JSON
if (req.headers['content-type'] === 'application/json') {
let data = '';
// 分段接收流数据
req.on('data', chunk => {
data += chunk.toString();
});
// 数据接收完成
req.on('end', () => {
req.body = JSON.parse(data);
next();
});
} else {
next();
}
});
五、模块化路由 Router(大型项目必备)
当接口数量越来越多,所有代码写在 app.js 中会臃肿难维护。Express 提供 express.Router() 实现路由拆分、模块化管理,每个业务模块独立一个路由文件。
5.1 路由使用流程
- 创建独立路由文件;
express.Router()创建路由实例(迷你应用);- 在路由实例上编写接口;
- 主文件引入路由,通过
app.use(路由前缀, 路由实例)挂载。
5.2 完整实战案例
项目目录结构
bash
├── app.js # 主入口文件
└── routes/ # 路由文件夹
├── user.js # 用户模块路由
└── order.js # 订单模块路由
1)用户路由 routes/user.js
js
const express = require('express');
// 创建路由实例
const userRouter = express.Router();
// 接口:最终地址 /users/
userRouter.get('/', (req, res) => {
res.json({ msg: '获取用户列表' });
});
// 接口:最终地址 /users/:id
userRouter.get('/:id', (req, res) => {
res.json({ userId: req.params.id, msg: '获取单个用户' });
});
// 新增用户
userRouter.post('/', (req, res) => {
res.json({ msg: '新增用户', data: req.body });
});
// 导出路由
module.exports = userRouter;
2)主入口 app.js
js
const express = require('express');
const app = express();
// 解析请求体
app.use(express.json());
// 引入路由模块
const userRouter = require('./routes/user');
const orderRouter = require('./routes/order');
// 挂载路由:统一添加前缀 /users
app.use('/users', userRouter);
app.use('/orders', orderRouter);
app.listen(8000, () => {
console.log('服务启动');
});
5.3 路由嵌套(二级路由)
路由内部也可以继续嵌套子路由,适用于复杂业务。
六、静态资源服务器
Express 内置 express.static() 中间件,一行代码快速搭建静态服务器,托管 HTML、图片、JS、CSS、视频等静态文件。
6.1 基础使用
js
const express = require('express');
const app = express();
// 托管当前目录下的 build 文件夹为静态资源目录
app.use(express.static('./build'));
app.listen(8000);
使用方式:将静态文件放入 build 文件夹,直接通过 http://localhost:8000/文件名 访问。
6.2 配置虚拟前缀
给静态资源添加访问前缀,区分接口和静态资源:
bash
// 访问地址必须加 /static 前缀:http://localhost:8000/static/图片.jpg
app.use('/static', express.static('./build'));
6.3 托管多个静态目录
支持同时托管多个文件夹:
php
app.use(express.static('./build'));
app.use(express.static('./public'));
七、日志中间件 morgan 完整版(生产环境必备)
morgan 是 Express 官方推荐的日志中间件,用于记录所有 HTTP 请求信息,方便线上排查问题。
7.1 安装
npm install morgan
7.2 核心语法
scss
morgan(日志格式, 配置项)
7.3 五种内置日志格式
1)dev(开发环境)
彩色简洁日志,控制台友好,仅本地开发使用
js
const express = require('express');
const morgan = require('morgan');
const app = express();
app.use(morgan('dev'));
app.get('/', (req, res) => res.send('首页'));
app.listen(8000);
输出示例:GET / 200 1.2 ms - 4
2)combined(生产环境标准格式)
信息最全,包含 IP、时间、请求头、UA,线上环境首选:
less
app.use(morgan('combined'));
3)common / short / tiny
极简格式,根据需求选择。
7.4 日志写入本地文件(持久化)
默认输出到控制台,线上需存入文件,使用文件流追加写入:
js
const express = require('express');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const app = express();
// 创建追加写入流
const writeStream = fs.createWriteStream(path.join(__dirname, 'access.log'), {
flags: 'a', // 追加模式,不覆盖旧日志
encoding: 'utf-8'
});
// 日志写入文件
app.use(morgan('combined', { stream: writeStream }));
app.listen(8000);
7.5 日志按日期切割(线上优化)
单日志文件会越来越大,使用 file-stream-rotator 按天切割日志:
安装依赖
npm install file-stream-rotator
按天分割完整代码
js
const express = require('express');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const FileStreamRotator = require('file-stream-rotator');
const app = express();
const logDir = path.join(__dirname, 'logs');
// 自动创建日志目录
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
// 配置切割规则
const rotateStream = FileStreamRotator.getStream({
date_format: 'YYYYMMDD',
filename: path.join(logDir, 'access-%DATE%.log'),
frequency: 'daily', // 按天切割
max_logs: 15 // 保留15天日志
});
app.use(morgan('combined', { stream: rotateStream }));
app.listen(8000);
7.6 日志过滤(skip)
忽略静态资源、只记录错误日志:
javascript
// 只记录 4xx/5xx 错误请求
app.use(morgan('dev', {
skip: (req, res) => res.statusCode < 400
}));
// 忽略静态资源请求
app.use(morgan('combined', {
skip: (req) => /.(css|js|png|jpg|ico)$/.test(req.url)
}));
7.7 自定义日志格式与字段
ini
// 自定义日志模板
const customLog = ':date[iso] :method :url :status :remote-addr';
app.use(morgan(customLog));
// 自定义 Token 字段
morgan.token('token', req => req.headers.token || '无token');
八、扩展常用功能
8.1 跨域处理(CORS)
前后端分离项目必用,使用 cors 中间件快速解决跨域。
安装
npm install cors
全局开启跨域
ini
const cors = require('cors');
const app = express();
// 全局允许所有跨域请求
app.use(cors());
8.2 多环境配置(开发/生产区分)
通过 NODE_ENV 区分环境,不同环境使用不同日志、配置:
php
const env = process.env.NODE_ENV || 'development';
if (env === 'development') {
app.use(morgan('dev'));
} else {
// 生产环境:日志写入文件
app.use(morgan('combined', { stream: rotateStream }));
}
启动命令:
ini
# 开发环境
node app.js
# 生产环境(Mac/Linux)
NODE_ENV=production node app.js
九、全文速查总结
- 服务创建 :
express()创建实例 →app.METHOD()注册接口 →app.listen()启动; - 中间件 :核心机制,
app.use()注册,无响应必须执行next(); - 参数获取
-
- 路径参数:
req.params - 查询参数:
req.query - JSON/表单:
req.body(依赖对应解析中间件) - 文件:
multer+req.file
- 路径参数:
- 路由拆分 :
express.Router()模块化管理接口; - 静态服务 :
express.static(目录路径); - 错误处理 :四参数中间件
(err, req, res, next); - 日志 :开发用
dev,生产用combined,线上必须做日志切割; - 跨域 :引入
cors中间件一键解决。