以新闻管理系统为例:
1、初始化项目
(1)初始化包管理配置文件:npm init
(2)安装express框架:npm install express
(3)创建app.js文件
// 导入express框架
const express = require('express')
// 创建express服务区实例对象
const app = express()
// 设置端口号并启动服务器
app.listen(80,()=>{
console.log('服务器启动成功,请访问http://127.0.0.1')
})
上述代码中只创建了基本服务器,当前并未接收到任何客户端请求。当前项目架构:

2、配置常用中间件
(1)配置跨域中间件
cors中间件:解决前端向后端的跨域请求,什么是跨域呢?只要请求的 协议(https/http)、域名、端口 中有任何一个不一致,浏览器就会认为这是跨域请求。
在项目中,可以运行npm install cors来安装cors中间件。
在app.js文件中导入cors中间件,并进行配置。
// 导入中间件
const cors = require('cors')
// 使用app.use注册成为全局中间件
app.use(cors())
(2)配置解析表单数据的中间件
Express框架中内置:urlencoded。在Koa框架中,通常用第三方中间件:koa-bodyparser。
在 Express 中,app.use(express.urlencoded({ extended: false })) 这行代码的作用是解析 POST 请求体中 application/x-www-form-urlencoded 格式的数据(也就是传统的表单提交格式)。
extended 参数决定了使用哪个库来解析这些数据,它的核心区别在于数据的复杂程度。
extended: false -> 使用 Node.js 内置的 querystring 库
extended: true -> 使用第三方的 qs 库
通常,根据前端传入的不同数据格式,需要使用不同的解析库,甚至是自定义中间件。
3、创建路由模块
根目录下新建router和router_handler文件夹,用来存放路由模块。其中,router文件夹用来存放客户端请求地址和事件处理函数之间的映射关系;router_handler文件夹用来存放路由事件处理函数。
(1)初始化路由模块
在 router 文件夹中创建 user.js 文件,用于保存所有和用户相关的路由对应关系,初始化代码如下:
const express = require('express')
// 创建路由对象
const router = express.Router()
// 注册 API 接口
router.post('/register', (req, res) => {
res.send({ status: 0, message: 'register OK' })
})
// 共享路由对象
module.exports = router
路由模块定义成功之后,就可以在 app.js 入口文件中导入路由模块了。示例代码如下:
// 导入路由自定义模块,./router为当前目录
const user_router = require('./router/user')
// 将路由模块注册成全局中间件
app.use('/api', user_router)
app.use('/api', user_router) 代码段应位于跨域中间件和解析表单数据中间件之后。
(2)抽离路由模块事件处理函数
上述写法非常不利于后期维护,有时一个事件处理函数就需要几十行代码,页面会显得很杂乱。规范的写法是将事件处理函数单独封装成一个模块,实现思路如下:
在 router_handler 目录下,新建 user.js 文件,存放和用户相关的事件处理函数。
在 user.js 文件中,使用 exports 往外共享事件处理函数,供其他模块使用。
首先将注册的事件处理函数抽离到 user.js 文件中,代码如下:
exports.register = (req, res) => {
res.send({ status: 0, message: 'register OK' })
}
事件处理函数模块定义完成之后,在 user.js 文件中导入新模块,并调用 register 方法。最终抽离完成的代码如下:
const express = require('express')
// 导入事件处理函数模块
const userFn = require('../router_handler/user')
const router = express.Router()
// 使用 userFn 模块下的 register 方法
router.post('/register', userFn.register)
module.exports = router
4、配置数据库信息
npm install mysql:安装mysql第三方模块
新建db文件夹,在db文件夹下新建index.js,用来创建数据库连接对象,示例代码如下:
// 导入 mysql 模块
const mysql = require('mysql')
// 创建数据库连接对象
const db = mysql.createPool({
// 数据库 IP 地址
host: '5.252.164.181',
// 数据库账号
user: 'webedu',
// 数据库密码
password: '5b2NxdXGBmKN3H8c',
// 数据库名称
database: 'webedu'
})
// 共享数据库连接对象
module.exports = db
在开发初期,SQL 语句通常硬编码在 Controller(事件处理函数)中。但为了提高数据复用性并实现关注点分离,我们会将数据持久化逻辑抽离到 Model 层(或 DAO 层)。同理,为了沉淀核心业务逻辑并减少冗余,我们会将跨模块复用的复杂逻辑下沉到 Service 服务层,从而实现高内聚、低耦合的代码结构。
5、MVC架构
(1)Model(模型):负责数据和业务逻辑。它直接与数据库打交道,定义数据的增删改查。它只关心数据长什么样,不关心数据怎么显示。
(2)View:在前后端分离的架构中,View 被移到了前端(如 Vue 或 React)。后端不再负责渲染页面,而是通过 res.send() 返回 JSON 数据。前端拿到 JSON 后,再由前端框架负责“视图”的展示。
(3)Controller(控制器):负责转发请求和业务处理。
6、主流的用户认证方式
| 认证方式 | 存储位置 | 状态感 | 典型场景 |
| Session-Cookie | 服务端内存/Redis | 有状态 | 传统的管理后台、单体架构 |
| JWT | 客户端(Local/Cookie) | 无状态 | 前后端分离、App、微服务 |
| OAuth 2.0 | 第三方服务器 | 授权机制 | 社交登录、开放平台 |
| Passkeys | 设备硬件 | 安全性极高 | 银行、高安全级别账户 |
在JWT认证方式中,客户端手动将 Token 放在 HTTP 头部(Authorization: Bearer )传给服务器。
7、请求方法(Methods)
| 特性 | GET (获取) | POST (提交) | PUT (更新) | DELETE (删除) |
| 主要作用 | 从服务器获取资源 | 向服务器发送数据(创建资源) | 更新服务器上的资源 | 删除服务器上的资源 |
| 数据位置 | 放在 URL 中(查询字符串) | 放在 Request Body(请求体)中 | 放在 Request Body 中 | 通常不带 Body,ID 在 URL 中 |
| 数据大小限制 | 受限于 URL 长度(通常约 2KB) | 无限制(适合传文件/大数据) | 无限制 | 无限制 |
| 安全性 | 较低(参数暴露在地址栏,会被缓存) | 较高(数据不显示在 URL 中) | 较高 | 较高 |
| 幂等性* | 是(多次请求结果相同) | 否(多次提交可能创建多个数据) | 是(多次覆盖结果相同) | 是(删一次和删多次结果相同) |
| 可缓存性 | 可以被浏览器缓存 | 不可被浏览器缓存 | 不可缓存 | 不可缓存 |
| 后退/刷新 | 无害 | 数据会被重新提交(浏览器会弹出警告) | 无害 | 无害 |
1.、GET (获取数据)
GET 请求通常将参数拼接在 URL 后面。
axios.get('/api/users', { params: { id: 123 } // 最终变为 /api/users?id=123 }).then(res => console.log(res.data));
2、POST (创建数据)
POST 会将敏感或大量的数据封装在“请求体”(Body)中。
// 注册新用户
axios.post('/api/register', {
username: 'Geographic_Explorer',
password: 'securepassword123'
}).then(res => console.log("注册成功"));
3、PUT (更新数据)
PUT 通常用于完整替换现有的资源。
// 修改 ID 为 101 的用户资料(全量更新)
axios.put('/api/users/101', {
nickname: 'ZhidiYun_Dev',
bio: 'Full-stack developer'
});
4、DELETE (删除数据)
DELETE 请求通常只需要在 URL 中指定要删除的资源 ID。
// 删除 ID 为 99 的文章
axios.delete('/api/posts/99')
.then(() => console.log("删除成功"));
8、Express和Koa框架对比
(1)表格对比
| 特性 | Express | Koa |
| 异步方案 | 回调 (Callback) | async/await (Promise) |
| 中间件模型 | 线性流,不等待异步 | 洋葱模型,支持 await next() |
| 内置功能 | 丰富(路由、静态资源等) | 极简(几乎所有功能需安装插件) |
| 错误处理 | 容易漏掉异步错误 | 统一 try-catch 非常方便 |
| 学习曲线 | 较低,适合快速上手 | 较高,需要理解 async/await 和模型 |
(表 1)
| 维度 | Express (像多米诺骨牌) | Koa (像剥洋葱) |
next() 的本质 | “去吧,后面不关我事了” | “我先等后面的做完,我再收尾” |
| 异步捕获 | 很难在前面的中间件获取后面异步操作的结果。 | 极其简单,直接 await next() 即可。 |
| 错误捕获 | 如果后面异步报错,前面很难 try-catch 到。 | 可以在最外层中间件 try { await next() } 捕获全链路错误。 |
(表 2)
| 框架 | 捕获异步错误的能力 | 大白话解释 |
| Express | 差 | 像单向跑步,后面的人摔了,前面的人看不见。必须手动派人(next(err))去报信。 |
| Koa | 极强 | 像拉橡皮筋,不管你拉多远(异步多久),断了(报错)的时候力都会传回到手心里。 |
(表 3)
(2)举例对比
F-PolyImage(AI 绘图平台)作为场景。
想象一下,用户点击“生成图片”后,后台需要经历三个步骤:
- 身份验证:检查用户点数够不够。
- AI 生成:调用模型算图(这是个耗时的异步过程)。
- 日志记录:图片画完了,记录一下消耗的时间和结果。
A. Express:像“单向接力赛”
在 Express 里,中间件就像一排站好的运动员。
- 流程如下:
- 第一个运动员(验证):检查完点数,大喊一声
next(),把接力棒传给下一个人。 - 第二个运动员(日志):他拿到棒子,看一眼表,记录“开始时间”,然后也大喊
next()传下去。 - 第三个运动员(AI 算图):他接到棒子开始画画。
- 第一个运动员(验证):检查完点数,大喊一声
- 尴尬的地方: 第二个“日志运动员”喊完
next()后,他不会等第三个人画完。他会立刻在自己的小本本上写:“计时结束,耗时 0.001 秒”。 等到 5 秒钟后 AI 图片真的画出来了,第二个运动员早就跑去喝水了,他根本没法回头去更新那个 5 秒的真实耗时。
大白话总结: Express 就像泼出去的水,中间件一旦把请求传给下一个,就很难再管回头的事了。
B. Koa:像“剥洋葱 / 闯关模式”
在 Koa 里,中间件像是一层层的洋葱皮,请求必须“钻进去”再“钻出来”。
- 流程如下:
- 进入外层(日志层):记录开始时间。
- 进入内层(AI 算图层):这一层说“等一下,我要画画”,于是整个流程就在这里等(await)。
- 画完了:图片生成成功。
- 钻回外层(回到日志层):请求原路返回,再次经过日志层。这时候日志层一看表:“嘿,画画用了 5 秒,记下来!”
- 优雅的地方: 你只需要在最外层写一个简单的逻辑:
开始 -> 等待(next) -> 结束。无论中间 AI 画图画了多久,最外层都能稳稳地等到它结束,然后准确记录时间。
大白话总结: Koa 就像回力镖,请求发出去处理完,最后一定会回到你手里,让你做最后的收尾工作。
牛逼