摘要 :使用原生 http 模块构建复杂应用太过繁琐,路由、静态资源、中间件等都需要手动处理。Express 是 Node.js 最流行的 Web 框架,以其极简、灵活著称。本文将带你一步一步 掌握 Express 的项目搭建、路由、中间件机制、模板引擎和错误处理,最后搭建一个功能完备的 MVC 示例博客系统。
一、为什么需要 Express?
在上一篇原生 Node.js 教程中,我们看到原生方式写路由和静态文件非常啰嗦。例如,原生处理不同 URL 需要大量 if-else 判断,设置 MIME 类型、处理静态文件也很麻烦。
Express 解决的核心问题:
| 原生 Node.js 痛点 | Express 解决方案 |
|---|---|
| 手动解析请求 URL 和方法 | 提供 app.get()、app.post() 等直观路由 |
| 难以管理多个中间件(日志、解析 body、权限) | 中间件链式调用,use 任意扩展 |
| 静态文件服务需自己读取文件并设置响应头 | express.static() 一行搞定 |
| 模板引擎集成繁琐 | app.set('view engine', 'ejs') 一行配置 |
| 错误处理不统一 | 统一错误处理中间件 |
简言之:Express 让你专注于业务逻辑,而不是 HTTP 协议的细节。
二、创建第一个 Express 应用
2.1 环境准备
确保已安装 Node.js(建议 v14 以上)。
2.2 新建项目
打开终端,依次执行:
javascript
mkdir express-demo
cd express-demo
npm init -y # 快速生成 package.json
npm install express

2.3 编写入口文件 app.js
javascript
// 1. 引入 express
const express = require('express');
// 2. 创建应用实例
const app = express();
// 3. 定义端口
const port = 3000;
// 4. 定义路由:当用户 GET 访问根路径 '/' 时
app.get('/', (req, res) => {
// res.send 自动处理 Content-Type 和状态码
res.send('<h1>Hello Express!</h1>');
});
// 5. 启动服务器
app.listen(port, () => {
console.log(`Express 服务器启动在 http://localhost:${port}`);
});
2.4 运行与验证
javascript
node app.js
打开浏览器访问 http://localhost:3000,看到大标题即成功。

核心理解 :
app对象是一个由中间件 和路由构成的请求处理管道。每个请求按顺序流经这些函数,直到有人发送响应。
三、路由详解
路由是指根据 HTTP 方法和 URL 路径,映射到对应的处理函数。
3.1 基本路由方法
javascript
// GET 请求 ------ 获取资源
app.get('/users', (req, res) => {
res.json([{ name: 'Alice' }, { name: 'Bob' }]);
});
// POST 请求 ------ 创建资源
app.post('/users', (req, res) => {
// 201 Created 是创建成功的标准状态码
res.status(201).send('用户已创建');
});
// PUT 请求 ------ 完全替换资源
app.put('/users/:id', (req, res) => {
// :id 是路由参数,下面会解释
res.send(`更新用户 ${req.params.id}`);
});
// DELETE 请求 ------ 删除资源
app.delete('/users/:id', (req, res) => {
res.send(`删除用户 ${req.params.id}`);
});
3.2 路由路径语法
Express 支持字符串模式、正则表达式、以及参数占位符。
javascript
// 路由参数 (params) ------ 从 URL 中提取
app.get('/articles/:category/:id', (req, res) => {
console.log(req.params); // 例如:{ category: 'tech', id: '42' }
// 查询参数 (query) ------ 从 ? 后面提取
console.log(req.query); // 例如:访问 /articles/tech/42?page=2&sort=desc
// 输出:{ page: '2', sort: 'desc' }
res.send('文章信息');
});
路径匹配示例:
| 路径模式 | 匹配示例 | 说明 |
|---|---|---|
/about |
/about |
完全匹配 |
/ab?cd |
/acd, /abcd |
? 表示前面字符可选 |
/ab*cd |
/ab123cd, /abxyzcd |
* 表示任意字符 |
/user/:uid |
/user/5 |
:uid 作为参数保存在 req.params.uid |
3.3 模块化路由 ------ Express.Router
当应用越来越大,把所有路由写在 app.js 会非常混乱。使用 Router 可以把相关路由组织成独立模块。
步骤1:创建 routes/users.js
javascript
const express = require('express');
const router = express.Router();
// 所有路径都会自动加上前缀 /users
router.get('/', (req, res) => {
res.send('用户列表');
});
router.get('/:id', (req, res) => {
res.send(`用户ID: ${req.params.id}`);
});
router.post('/', (req, res) => {
res.status(201).send('创建用户');
});
module.exports = router;
步骤2:在 app.js 中挂载
javascript
const usersRouter = require('./routes/users');
// 第一个参数 '/users' 是挂载点,即路由前缀
app.use('/users', usersRouter);
重新运行app,然后访问 /users 会触发列表路由,访问 /users/123 会触发详情路由。


四、中间件:Express 的精髓
中间件函数 的签名是 function(req, res, next)。它可以:
-
执行任意代码
-
修改
req或res对象 -
结束请求-响应周期(调用
res.send()等) -
调用
next()将控制权传递给下一个中间件
如果不调用 next() 也不发送响应,请求会挂起!
4.1 应用级中间件
通过 app.use() 或 app.METHOD() 挂载。
4.1.1 内置中间件 express.static
javascript
// 假设项目根目录下有一个 public 文件夹,里面放 style.css, logo.png 等
app.use(express.static('public'));
// 现在可以通过 http://localhost:3000/style.css 直接访问 public/style.css

多个静态目录可以多次调用
app.use(express.static(...)),按顺序查找。
4.1.2 自定义日志中间件
javascript
// 这个中间件会记录每个请求的方法、URL 和时间
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next(); // 必须调用 next(),否则后续路由不会执行
});
app.get('/', (req, res) => {
res.send('主页');
});
每访问一次主页,终端会输出类似 GET / - 2026-01-15T10:30:00.000Z。

4.1.3 身份验证中间件(示例)
javascript
// 一个简单的 token 验证中间件
function auth(req, res, next) {
// 假设要求请求头包含 Authorization: Bearer mysecrettoken
if (req.headers.authorization === 'Bearer mysecrettoken') {
next(); // 验证通过,继续
} else {
res.status(401).send('未授权');
}
}
// 只对 /admin 路径应用此中间件
app.get('/admin', auth, (req, res) => {
res.send('管理页面');
});

4.2 中间件执行顺序
中间件注册的顺序决定了执行顺序。例如:
javascript
app.use((req, res, next) => {
console.log('A');
next();
console.log('A 结束');
});
app.use((req, res, next) => {
console.log('B');
next();
console.log('B 结束');
});
app.get('/', (req, res) => {
console.log('路由处理');
res.send('ok');
});
访问 / 时,控制台输出顺序:
说明 next() 之后的代码会在后续中间件执行完后才执行(类似"洋葱模型")。
4.3 常用第三方中间件
安装:
javascript
npm install morgan cors helmet
在 app.js 中引入并使用:
javascript
const morgan = require('morgan'); // 日志
const cors = require('cors'); // 跨域
const helmet = require('helmet'); // 安全头
// 使用 morgan 的 dev 格式(简洁彩色日志)
app.use(morgan('dev'));
// 允许所有跨域请求(开发环境常用)
app.use(cors());
// 自动设置 X-Content-Type-Options, X-Frame-Options 等安全头
app.use(helmet());
4.4 解析请求体中间件
Express 内置了以下两个中间件来解析 POST/PUT 等请求中的数据:
javascript
// 解析 application/x-www-form-urlencoded (传统表单提交)
app.use(express.urlencoded({ extended: true }));
// 解析 application/json (fetch/Axios 提交的 JSON)
app.use(express.json());
使用示例:
javascript
app.post('/submit', (req, res) => {
console.log(req.body); // 表单字段或 JSON 对象
res.send('收到数据');
});
extended: true允许解析嵌套对象;false则只支持简单键值对。
五、模板引擎与 MVC 结构
5.1 为什么需要模板引擎?
当需要动态生成 HTML 页面(比如显示数据库里的文章列表)时,直接拼接字符串很痛苦。模板引擎允许你在 HTML 中嵌入代码,并传入变量。
Express 支持 Pug、EJS、Handlebars 等。本文选择 EJS,因为它语法接近原生 HTML,学习成本低。
5.2 安装和配置 EJS
javascript
npm install ejs
在 app.js 中添加:
javascript
const path = require('path');
// 设置模板存放目录(默认是 views 文件夹)
app.set('views', path.join(__dirname, 'views'));
// 设置模板引擎为 ejs
app.set('view engine', 'ejs');
5.3 创建第一个模板
新建文件夹 views,在里面创建 index.ejs:
javascript
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1><%= heading %></h1>
<ul>
<% items.forEach(item => { %>
<li><%= item %></li>
<% }); %>
</ul>
</body>
</html>
<%= variable %>会输出转义后的变量值<% code %>可以执行 JavaScript 代码,但不输出到页面
5.4 在路由中渲染模板
javascript
app.get('/', (req, res) => {
res.render('index', { // 对应 views/index.ejs
title: '我的网站',
heading: '欢迎光临',
items: ['Node.js', 'Express', 'EJS']
});
});
访问 http://localhost:3000 即可看到动态生成的 HTML。

5.5 MVC 架构组织
MVC 将应用分为三层:
-
Model:数据和数据操作(如读取数据库、内存存储)
-
View:用户界面(EJS 模板)
-
Controller:业务逻辑,连接 Model 和 View
Express 本身不强约束,但我们可以按以下结构组织文件夹:
javascript
blog-project/
├── models/ # 数据模型
├── views/ # EJS 模板
├── controllers/ # 控制器
├── routes/ # 路由定义
├── public/ # 静态资源 (CSS, JS, 图片)
├── app.js # 应用入口
└── package.json
六、错误处理中间件
6.1 同步错误的捕获
Express 会自动捕获同步代码中的 throw,并传递给错误处理中间件。
javascript
app.get('/error', (req, res) => {
throw new Error('出错了!');
});
6.2 定义错误处理中间件
错误处理中间件有 4 个参数 ,必须声明 err 作为第一个参数。
javascript
// 放在所有路由之后
// 处理 404:没有匹配任何路由
app.use((req, res, next) => {
res.status(404).send('抱歉,页面未找到');
});
// 全局错误处理(捕获所有未被处理的错误)
app.use((err, req, res, next) => {
console.error(err.stack); // 打印错误栈
res.status(500).send('服务器内部错误');
});
6.3 异步错误的处理
Express 4.x 不能 自动捕获异步函数(如 setTimeout、Promise、async/await)中抛出的错误。你必须手动调用 next(err)。
错误示例(错误不会被捕获):
javascript
app.get('/async-bad', async (req, res) => {
const data = await someAsyncFunction(); // 如果这里 reject,Express 无法捕获
res.json(data);
});
正确方式1:使用 next
javascript
app.get('/async-ok', async (req, res, next) => {
try {
const data = await someAsyncFunction();
res.json(data);
} catch (err) {
next(err); // 传递给错误处理中间件
}
});
正确方式2:使用 express-async-errors 包
javascript
npm install express-async-errors
在 app.js 最顶部(所有路由之前)引入:
javascript
require('express-async-errors');
之后你就可以直接写 async 路由,任何 reject 或 throw 会自动进入错误处理中间件。
正确方式3:手动包装函数
javascript
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/data', asyncHandler(async (req, res) => {
const data = await someAsyncFunction();
res.json(data);
}));
七、完整实战:简易博客系统(MVC)
我们将构建一个功能完备的博客系统,包括:
-
文章列表页
-
文章详情页
-
创建新文章(含表单)
-
删除文章
暂不涉及真实数据库,使用内存数组存储。
7.1 初始化项目结构
javascript
mkdir express-blog
cd express-blog
npm init -y
npm install express ejs

创建以下文件夹和文件(手动或使用命令):
javascript
express-blog/
├── models/
│ └── post.js
├── views/
│ ├── posts.ejs
│ ├── post.ejs
│ └── create.ejs
├── controllers/
│ └── postController.js
├── routes/
│ └── post.js
├── public/ (空文件夹,留待放置 CSS)
├── app.js
└── package.json
7.2 编写模型 (Model) ------ models/post.js
模型负责数据存储和操作。这里用数组模拟。
javascript
// 内存存储
let posts = [];
let idCounter = 1;
// 辅助函数:确保 id 为数字(用于比较)
function toNumber(id) {
return typeof id === 'number' ? id : parseInt(id, 10);
}
module.exports = {
// 获取所有文章(按创建时间倒序)
getAll: () => [...posts].reverse(),
// 根据 id 获取单篇文章
getById: (id) => posts.find(p => p.id === toNumber(id)),
// 创建新文章
create: (title, content) => {
const post = {
id: idCounter++,
title: title.trim(),
content: content.trim(),
createdAt: new Date()
};
posts.push(post);
return post;
},
// 删除文章
delete: (id) => {
const numericId = toNumber(id);
const index = posts.findIndex(p => p.id === numericId);
if (index !== -1) {
posts.splice(index, 1);
return true;
}
return false;
}
};
7.3 编写控制器 (Controller) ------ controllers/postController.js
控制器负责处理请求、调用模型、决定渲染哪个视图。
javascript
const Post = require('../models/post');
// 显示所有文章
exports.list = (req, res) => {
const posts = Post.getAll();
res.render('posts', { posts });
};
// 显示单篇文章详情
exports.show = (req, res) => {
const id = parseInt(req.params.id, 10);
const post = Post.getById(id);
if (!post) {
return res.status(404).send('文章不存在');
}
res.render('post', { post });
};
// 显示创建新文章的表单
exports.createForm = (req, res) => {
res.render('create');
};
// 处理创建文章的 POST 请求
exports.create = (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).send('标题和内容不能为空');
}
Post.create(title, content);
res.redirect('/posts'); // 重定向到列表页
};
// 处理删除文章(通过 POST 请求,避免 GET 被爬虫误删)
exports.delete = (req, res) => {
const id = parseInt(req.params.id, 10);
Post.delete(id);
res.redirect('/posts');
};
7.4 编写视图 (View) ------ EJS 模板
7.4.1 基础布局(可选)------ views/partials/header.ejs和footer.ejs
为了不重复写 HTML 结构,可以使用 EJS 的 <%- include() %>。我们创建两个布局文件:
html
<!-- views/partials/header.ejs -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>简易博客 - <%= title %></title>
<style>
body { font-family: Arial; margin: 40px; line-height: 1.5; }
.post { border-bottom: 1px solid #ccc; margin-bottom: 20px; }
.post h2 { margin-bottom: 5px; }
.actions { margin-top: 10px; }
button { background: #c00; color: white; border: none; padding: 5px 10px; cursor: pointer; }
form { display: inline; }
</style>
</head>
<body>
<h1><a href="/posts" style="text-decoration: none;">📝 我的博客</a></h1>
html
<!-- views/partials/footer.ejs -->
</body>
</html>
7.4.2 文章列表页 ------ views/posts.ejs
javascript
<% const title = '文章列表'; %>
<%- include('partials/header', { title: title }) %>
<a href="/posts/new">➕ 写新文章</a>
<% if (posts.length === 0) { %>
<p>还没有任何文章,点击上方按钮创建第一篇!</p>
<% } else { %>
<% posts.forEach(post => { %>
<div class="post">
<h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2>
<small>发布于: <%= post.createdAt.toLocaleString() %></small>
<div class="actions">
<form method="POST" action="/posts/<%= post.id %>/delete" onsubmit="return confirm('确定删除?')">
<button type="submit">删除</button>
</form>
</div>
</div>
<% }); %>
<% } %>
<%- include('partials/footer') %>
7.4.3 文章详情页 ------ views/post.ejs
javascript
<% const title = post.title; %>
<%- include('partials/header', { title: title }) %>
<div>
<h2><%= post.title %></h2>
<small><%= post.createdAt.toLocaleString() %></small>
<div style="margin-top: 20px;"><%= post.content.replace(/\n/g, '<br>') %></div>
<p><a href="/posts">← 返回列表</a></p>
</div>
<%- include('partials/footer') %>
7.4.4 创建文章表单页 ------ views/create.ejs
javascript
<% const title = '写新文章'; %>
<%- include('partials/header', { title: title }) %>
<form method="POST" action="/posts">
<div>
<label>标题</label><br>
<input type="text" name="title" style="width: 100%; padding: 8px;" required>
</div>
<div style="margin-top: 10px;">
<label>内容</label><br>
<textarea name="content" rows="10" style="width: 100%;" required></textarea>
</div>
<div style="margin-top: 10px;">
<button type="submit">发布文章</button>
<a href="/posts">取消</a>
</div>
</form>
<%- include('partials/footer') %>
7.5 编写路由 ------ routes/post.js
javascript
const router = require('express').Router();
const controller = require('../controllers/postController');
// 注意顺序:具体的路径 (如 /new) 要放在动态路径 (/:id) 前面,否则 /new 会被当作 id 处理
router.get('/', controller.list); // GET /posts
router.get('/new', controller.createForm); // GET /posts/new
router.get('/:id', controller.show); // GET /posts/123
router.post('/', controller.create); // POST /posts
router.post('/:id/delete', controller.delete); // POST /posts/123/delete
module.exports = router;
7.6 入口文件 app.js
javascript
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;
// 1. 模板引擎配置
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// 2. 中间件配置
app.use(express.static('public')); // 静态资源
app.use(express.urlencoded({ extended: true })); // 解析表单
app.use(express.json()); // 解析 JSON(虽未用到但保留)
// 3. 挂载博客路由
const postRoutes = require('./routes/post');
app.use('/posts', postRoutes);
// 4. 根路径重定向到 /posts
app.get('/', (req, res) => {
res.redirect('/posts');
});
// 5. 404 处理(放在所有路由之后)
app.use((req, res) => {
res.status(404).send('404 - 页面未找到');
});
// 6. 全局错误处理(放在最后)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('500 - 服务器内部错误');
});
// 7. 启动服务器
app.listen(port, () => {
console.log(`博客系统运行在 http://localhost:${port}`);
});
7.7 运行并测试
javascript
node app.js
- 访问
http://localhost:3000自动跳转到文章列表。

- 点击「写新文章」,填写标题和内容后提交,会看到新文章出现在列表。


- 点击标题进入详情页,点击删除按钮可删除。



恭喜!你已经拥有一个完全基于 MVC 模式、具备增删查功能的博客系统原型。
7.8 进一步练习
-
添加编辑文章功能(提示:使用
PUT或POST路由,再创建一个edit.ejs表单) -
将内存存储换成真实的数据库(如 SQLite、MongoDB)
-
添加用户认证(使用
express-session和bcrypt)
八、总结
通过这篇超详细教程,你已经掌握了:
Express 的安装与第一个应用
路由系统:各种 HTTP 方法、参数、查询、Router 模块化
中间件机制:内置、自定义、第三方、执行顺序、请求体解析
模板引擎 EJS:动态渲染 HTML,并结合 MVC 架构
错误处理:同步/异步错误、404 和 500 中间件
完整项目:从零构建一个可运行的博客系统(增、查、删)
Express 让开发者专注于业务逻辑而非 HTTP 细节。你现在已经具备开发中型 Web 应用的能力。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。