Skip to content

doc

概要

在本周的后端课程中,我们将学习如何实现一个文件上传的API,特别是针对用户头像的上传和存储。我们将使用 Express 框架处理文件上传,通过 Multer 中间件来接收和存储文件,同时将相关的信息(例如用户名、头像文件路径)存储在 MongoDB 中,以便前端能够访问。通过逐步讲解、操作与代码分析,我们将掌握如何构建这样的上传功能。

圣诞节到了,公司内充满了快活的空气,大家约好晚上一起去逛街。就在这时,社长上网的时候发现其他公司的内网页面都能显示员工头像,而自家公司还是只能显示用户名。于是他要求程序组和海子实现一下需求再下班。海子虽然内心很不爽,但还是借助中间件熟练地完成了社长的任务。最终和并大家一起欣赏了圣诞夜的街景。

image

基础知识

1. 文件上传的原理

文件上传的基本过程可以分为以下几个步骤:

  1. 客户端发起请求:客户端(如前端页面)通过表单或AJAX请求将文件和相关数据发送到服务器。
  2. 服务器接收文件:服务器端使用文件上传中间件(如Multer)来处理上传的文件。
  3. 存储文件:上传的文件可以被存储在服务器的文件系统中,或上传到云存储等服务。
  4. 保存信息到数据库:将文件的路径、文件名和用户相关信息一起存储到数据库中,以便后续使用。

2. Multer中间件

Multer 是一个用于处理 multipart/form-data​ 的中间件,通常用于处理文件上传。它会自动解析上传的文件,将文件保存到服务器的某个目录,或者将文件信息(如文件名)传递到服务器其他部分处理。

3. MongoDB存储文件信息

我们将使用 MongoDB 存储用户的头像文件路径和与之相关的用户名。存储信息的基本步骤包括:

  • 创建一个 MongoDB 模型,用来存储用户信息。
  • 将头像的文件路径和用户名保存到数据库。

4. 文件名防重处理

为了避免上传的头像文件名冲突,我们可以在文件名中加入随机字符串(或UUID)。这样可以确保每个文件都有一个唯一的文件名。

情景式案例解读:实现头像上传API

1. 需求分析

我们的目标是实现一个用户头像上传功能:

  • 用户提交头像文件和用户名。
  • 服务器接收文件并进行保存。
  • 将文件保存路径以及用户名存储到 MongoDB 中。
  • 用户可以通过用户名获取对应的头像。

2. 如何实现?

我们将逐步实现这个功能。首先,我们需要准备好开发环境,安装所需的依赖包。

3. 开发环境设置

首先,我们需要初始化一个 Node.js 项目,并安装必备的依赖。

bash
# 创建一个新的项目目录
mkdir avatar-upload-api
cd avatar-upload-api

# 使用pnpm初始化一个新的项目
pnpm init

# 安装需要的依赖
pnpm add express multer mongoose dotenv
  • express​:用于创建后端服务器。
  • multer​:用于处理文件上传。
  • mongoose​:用于与 MongoDB 交互。
  • dotenv​:用于管理环境变量。

4. 项目文件结构

确保你的项目目录结构如下:

bash
/avatar-upload-api
  ├── node_modules/          # 项目依赖
  ├── uploads/               # 存储上传文件的目录
  ├── .env                   # 存储环境变量配置的文件
  ├── server.js              # Express 服务器代码
  ├── package.json           # 项目描述文件
  └── package-lock.json      # 锁定的依赖版本

5. 创建 .env​ 文件

在项目根目录下创建一个 .env​ 文件,用于存储 MongoDB 连接字符串和服务器端口等环境变量。其内容如下:

bash
MONGODB_URI=mongodb://localhost:27017/avatar-upload-api  # 你的 MongoDB 连接 URI
PORT=3000  # 服务器监听的端口

这里的 MONGODB_URI​ 是 MongoDB 的连接字符串,PORT​ 是你的服务器端口,可以根据需要修改。

6. 创建服务器和配置 Multer

接下来,我们将创建一个 Express 服务器,并配置 Multer 来处理文件上传。我们还将使用 Mongoose 来与 MongoDB 进行交互,并将上传的文件路径与用户名一并存储。

server.js​:

javascript
// 引入依赖包
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. 代码解读

  1. Express 服务器:我们使用 express​ 创建了一个简单的服务器,并设置了 /upload-avatar​ 和 /get-avatar/:username​ 两个API端点。
  2. Multer 配置:通过 multer.diskStorage​ 设置文件上传的存储目录和文件名格式。文件名中包含了当前时间和随机数,以避免文件名冲突。
  3. MongoDB 存储:我们使用 mongoose​ 创建了一个 User​ 模型,用来保存用户的头像路径和用户名。在 /upload-avatar​ 路由中,我们通过上传的头像路径将文件信息存入数据库。
  4. 上传文件:在 /upload-avatar​ 路由中,使用 upload.single('avatar')​ 来处理文件上传,文件存储后,我们将文件路径与用户名一起存入数据库。
  5. 获取头像:在 /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. 启动服务器

在项目目录下,使用以下命令启动服务器:

bash
node server.js

服务器会监听在 .env​ 文件中配置的端口。可以通过浏览器或 API 客户端(如 Postman)进行测试,我们在lab中进行尝试。

Lab: 测试头像上传和展示功能

任务目标

本实验的目标是通过前端 HTML 页面来测试已搭建的头像上传和展示功能。学生将通过创建两个简单的 HTML 文件来与后端服务器进行交互,测试头像上传和头像展示的功能。

1. 前提条件

确保你按照doc内容操作,后端服务器 server.js​ 已经在你的电脑上启动,并且能够通过 http://localhost:3000​ 访问。后端服务会处理头像上传、存储以及根据用户名查询头像。

2. 创建前端页面

在你的电脑上创建以下两个 HTML 文件,并双击打开它们来进行测试。

upload.html​:上传头像页面

这个页面允许用户选择头像并提交给后端。表单提交后,前端会向后端发送 POST 请求以上传头像。

html
<!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 请求从后端获取头像并展示。

html
<!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. 打开上传头像页面

  1. 在你的本地电脑上创建并保存 upload.html​ 文件。
  2. 双击 upload.html​ 文件,用浏览器打开。
  3. 在页面中输入一个用户名,并选择一个头像文件,然后点击 "上传" 按钮。
  4. 如果上传成功,浏览器会显示 "上传成功!" 的提示,头像文件会被上传到服务器并保存。

2. 打开查看头像页面

  1. 在你的本地电脑上创建并保存 show.html​ 文件。
  2. 双击 show.html​ 文件,用浏览器打开。
  3. 输入之前上传头像时使用的用户名,并点击 "查询头像" 按钮。
  4. 如果用户名对应的头像存在,头像会显示在页面上;否则,页面会显示 "用户未找到或头像加载失败"。

4. 注意

  • 确保服务器在运行中:只有在服务器正常运行时,前端页面才能与后端进行交互。
  • 文件上传大小限制:根据后端配置,Multer 可能会限制上传的文件大小。请确保上传的文件不超过限制。
  • 浏览器跨域问题:如果你遇到跨域问题(CORS),你可能需要在后端配置 CORS 处理。我们已经学习过跨域问题。

Homework删除旧头像

1. 背景

大家逛街时,新人宁宁提出了一个疑惑:“海子姐,如果用户上传了新头像,我们是不是要删除旧的头像文件呢?不然服务器会堆积很多无用的图片。”海子意识到了自己的疏忽,她接下来的任务就是实现这个功能。

2. 开发要求

本次作业要求学生在现有的头像上传功能基础上,加入删除旧头像的功能。具体要求如下:

  • 删除旧头像功能:

    • 当用户上传新的头像时,检查该用户是否已有旧头像。
    • 如果有旧头像,删除旧头像文件。
    • 只保留最新上传的头像。
  • 提示:

    • 在删除文件时,请确保文件路径正确,且文件存在。
    • 上传新头像时,不需要再次保存用户名,只需要更新文件路径。
    • 或许可以使用 fs.unlink​ 方法来删除旧头像文件。
    • 当用户上传新头像时,更新数据库记录,确保文件路径是最新的。

4. 思考题

防止用户绕过前端的文件类型限制:虽然前端可以限制文件类型,但用户可以通过开发者工具绕过这些限制。你认为后端如何才能有效防止恶意文件上传?