doc
概要
在本周的后端课程中,我们将学习如何实现一个文件上传的API,特别是针对用户头像的上传和存储。我们将使用 Express 框架处理文件上传,通过 Multer 中间件来接收和存储文件,同时将相关的信息(例如用户名、头像文件路径)存储在 MongoDB 中,以便前端能够访问。通过逐步讲解、操作与代码分析,我们将掌握如何构建这样的上传功能。
圣诞节到了,公司内充满了快活的空气,大家约好晚上一起去逛街。就在这时,社长上网的时候发现其他公司的内网页面都能显示员工头像,而自家公司还是只能显示用户名。于是他要求程序组和海子实现一下需求再下班。海子虽然内心很不爽,但还是借助中间件熟练地完成了社长的任务。最终和并大家一起欣赏了圣诞夜的街景。
基础知识
1. 文件上传的原理
文件上传的基本过程可以分为以下几个步骤:
- 客户端发起请求:客户端(如前端页面)通过表单或AJAX请求将文件和相关数据发送到服务器。
- 服务器接收文件:服务器端使用文件上传中间件(如Multer)来处理上传的文件。
- 存储文件:上传的文件可以被存储在服务器的文件系统中,或上传到云存储等服务。
- 保存信息到数据库:将文件的路径、文件名和用户相关信息一起存储到数据库中,以便后续使用。
2. Multer中间件
Multer 是一个用于处理 multipart/form-data
的中间件,通常用于处理文件上传。它会自动解析上传的文件,将文件保存到服务器的某个目录,或者将文件信息(如文件名)传递到服务器其他部分处理。
3. MongoDB存储文件信息
我们将使用 MongoDB 存储用户的头像文件路径和与之相关的用户名。存储信息的基本步骤包括:
- 创建一个 MongoDB 模型,用来存储用户信息。
- 将头像的文件路径和用户名保存到数据库。
4. 文件名防重处理
为了避免上传的头像文件名冲突,我们可以在文件名中加入随机字符串(或UUID)。这样可以确保每个文件都有一个唯一的文件名。
情景式案例解读:实现头像上传API
1. 需求分析
我们的目标是实现一个用户头像上传功能:
- 用户提交头像文件和用户名。
- 服务器接收文件并进行保存。
- 将文件保存路径以及用户名存储到 MongoDB 中。
- 用户可以通过用户名获取对应的头像。
2. 如何实现?
我们将逐步实现这个功能。首先,我们需要准备好开发环境,安装所需的依赖包。
3. 开发环境设置
首先,我们需要初始化一个 Node.js 项目,并安装必备的依赖。
# 创建一个新的项目目录
mkdir avatar-upload-api
cd avatar-upload-api
# 使用pnpm初始化一个新的项目
pnpm init
# 安装需要的依赖
pnpm add express multer mongoose dotenv
-
express
:用于创建后端服务器。 -
multer
:用于处理文件上传。 -
mongoose
:用于与 MongoDB 交互。 -
dotenv
:用于管理环境变量。
4. 项目文件结构
确保你的项目目录结构如下:
/avatar-upload-api
├── node_modules/ # 项目依赖
├── uploads/ # 存储上传文件的目录
├── .env # 存储环境变量配置的文件
├── server.js # Express 服务器代码
├── package.json # 项目描述文件
└── package-lock.json # 锁定的依赖版本
5. 创建 .env
文件
在项目根目录下创建一个 .env
文件,用于存储 MongoDB 连接字符串和服务器端口等环境变量。其内容如下:
MONGODB_URI=mongodb://localhost:27017/avatar-upload-api # 你的 MongoDB 连接 URI
PORT=3000 # 服务器监听的端口
这里的 MONGODB_URI
是 MongoDB 的连接字符串,PORT
是你的服务器端口,可以根据需要修改。
6. 创建服务器和配置 Multer
接下来,我们将创建一个 Express 服务器,并配置 Multer 来处理文件上传。我们还将使用 Mongoose 来与 MongoDB 进行交互,并将上传的文件路径与用户名一并存储。
server.js
:
// 引入依赖包
const express = require('express');
const multer = require('multer');
const mongoose = require('mongoose');
const path = require('path');
const dotenv = require('dotenv');
// 加载环境变量
dotenv.config();
// 初始化Express应用
const app = express();
const port = process.env.PORT || 5000;
// 连接MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch((err) => console.log('MongoDB connection error:', err));
// 配置Multer存储引擎
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // 上传文件保存的目录
},
filename: (req, file, cb) => {
// 设置上传文件的文件名,避免文件名冲突
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const fileExtension = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + fileExtension); // 保存为“fieldname-时间戳-随机数.扩展名”
}
});
// 创建Multer上传实例
const upload = multer({ storage: storage });
// 创建用户信息模型
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
avatarPath: { type: String, required: true }
});
const User = mongoose.model('User', UserSchema);
// API端点:上传头像
app.post('/upload-avatar', upload.single('avatar'), async (req, res) => {
const { username } = req.body;
const avatarPath = req.file.path; // 获取文件上传后的路径
// 保存到数据库
try {
let user = await User.findOne({ username });
if (!user) {
// 如果用户不存在,创建新的用户
user = new User({ username, avatarPath });
await user.save();
} else {
// 如果用户已存在,更新头像路径
user.avatarPath = avatarPath;
await user.save();
}
res.status(200).json({
message: '头像上传成功',
avatarPath
});
} catch (err) {
res.status(500).json({ message: '服务器错误', error: err.message });
}
});
// API端点:获取用户头像
app.get('/get-avatar/:username', async (req, res) => {
const { username } = req.params;
try {
const user = await User.findOne({ username });
if (user) {
res.sendFile(path.join(__dirname, user.avatarPath)); // 返回头像图片文件
} else {
res.status(404).json({ message: '用户未找到' });
}
} catch (err) {
res.status(500).json({ message: '服务器错误', error: err.message });
}
});
// 启动服务器
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
7. 代码解读
- Express 服务器:我们使用
express
创建了一个简单的服务器,并设置了/upload-avatar
和/get-avatar/:username
两个API端点。 - Multer 配置:通过
multer.diskStorage
设置文件上传的存储目录和文件名格式。文件名中包含了当前时间和随机数,以避免文件名冲突。 - MongoDB 存储:我们使用
mongoose
创建了一个User
模型,用来保存用户的头像路径和用户名。在/upload-avatar
路由中,我们通过上传的头像路径将文件信息存入数据库。 - 上传文件:在
/upload-avatar
路由中,使用upload.single('avatar')
来处理文件上传,文件存储后,我们将文件路径与用户名一起存入数据库。 - 获取头像:在
/get-avatar/:username
路由中,通过用户名查询用户的头像路径,并返回头像图片。
8. 外部请求API的格式
POST 请求:上传头像
URL:
http://localhost:5000/upload-avatar
请求体:
form-data
(设置字段为avatar
和username
)-
avatar
: 上传的文件 -
username
: 用户名
-
GET 请求:获取头像
- URL:
http://localhost:5000/get-avatar/{username}
9. 启动服务器
在项目目录下,使用以下命令启动服务器:
node server.js
服务器会监听在 .env
文件中配置的端口。可以通过浏览器或 API 客户端(如 Postman)进行测试,我们在lab中进行尝试。
Lab: 测试头像上传和展示功能
任务目标
本实验的目标是通过前端 HTML 页面来测试已搭建的头像上传和展示功能。学生将通过创建两个简单的 HTML 文件来与后端服务器进行交互,测试头像上传和头像展示的功能。
1. 前提条件
确保你按照doc内容操作,后端服务器 server.js
已经在你的电脑上启动,并且能够通过 http://localhost:3000
访问。后端服务会处理头像上传、存储以及根据用户名查询头像。
2. 创建前端页面
在你的电脑上创建以下两个 HTML 文件,并双击打开它们来进行测试。
upload.html
:上传头像页面
这个页面允许用户选择头像并提交给后端。表单提交后,前端会向后端发送 POST 请求以上传头像。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传头像</title>
</head>
<body>
<h1>上传头像</h1>
<form id="uploadForm" enctype="multipart/form-data">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br><br>
<label for="avatar">选择头像:</label>
<input type="file" id="avatar" name="avatar" required><br><br>
<button type="submit">上传</button>
</form>
<script>
document.getElementById('uploadForm').addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData();
const username = document.getElementById('username').value;
const avatar = document.getElementById('avatar').files[0];
formData.append('username', username);
formData.append('avatar', avatar);
fetch('http://localhost:3000/upload-avatar', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
alert('上传成功!');
})
.catch(error => {
alert('上传失败!');
});
});
</script>
</body>
</html>
show.html
:展示头像页面
这个页面允许用户输入用户名,点击按钮后,页面会通过 GET 请求从后端获取头像并展示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看头像</title>
</head>
<body>
<h1>查看头像</h1>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br><br>
<button onclick="showAvatar()">查询头像</button>
<div id="avatarContainer">
<!-- 头像将显示在这里 -->
</div>
<script>
function showAvatar() {
const username = document.getElementById('username').value;
fetch(`http://localhost:3000/get-avatar/${username}`)
.then(response => response.blob())
.then(blob => {
const imageUrl = URL.createObjectURL(blob);
document.getElementById('avatarContainer').innerHTML = `<img src="${imageUrl}" alt="头像">`;
})
.catch(error => {
alert('用户未找到或头像加载失败');
});
}
</script>
</body>
</html>
3. 测试步骤
1. 打开上传头像页面
- 在你的本地电脑上创建并保存
upload.html
文件。 - 双击
upload.html
文件,用浏览器打开。 - 在页面中输入一个用户名,并选择一个头像文件,然后点击 "上传" 按钮。
- 如果上传成功,浏览器会显示 "上传成功!" 的提示,头像文件会被上传到服务器并保存。
2. 打开查看头像页面
- 在你的本地电脑上创建并保存
show.html
文件。 - 双击
show.html
文件,用浏览器打开。 - 输入之前上传头像时使用的用户名,并点击 "查询头像" 按钮。
- 如果用户名对应的头像存在,头像会显示在页面上;否则,页面会显示 "用户未找到或头像加载失败"。
4. 注意
- 确保服务器在运行中:只有在服务器正常运行时,前端页面才能与后端进行交互。
- 文件上传大小限制:根据后端配置,Multer 可能会限制上传的文件大小。请确保上传的文件不超过限制。
- 浏览器跨域问题:如果你遇到跨域问题(CORS),你可能需要在后端配置 CORS 处理。我们已经学习过跨域问题。
Homework删除旧头像
1. 背景
大家逛街时,新人宁宁提出了一个疑惑:“海子姐,如果用户上传了新头像,我们是不是要删除旧的头像文件呢?不然服务器会堆积很多无用的图片。”海子意识到了自己的疏忽,她接下来的任务就是实现这个功能。
2. 开发要求
本次作业要求学生在现有的头像上传功能基础上,加入删除旧头像的功能。具体要求如下:
删除旧头像功能:
- 当用户上传新的头像时,检查该用户是否已有旧头像。
- 如果有旧头像,删除旧头像文件。
- 只保留最新上传的头像。
提示:
- 在删除文件时,请确保文件路径正确,且文件存在。
- 上传新头像时,不需要再次保存用户名,只需要更新文件路径。
- 或许可以使用
fs.unlink
方法来删除旧头像文件。 - 当用户上传新头像时,更新数据库记录,确保文件路径是最新的。
4. 思考题
防止用户绕过前端的文件类型限制:虽然前端可以限制文件类型,但用户可以通过开发者工具绕过这些限制。你认为后端如何才能有效防止恶意文件上传?