# 文件上传与图片处理

URL: https://caijiao.org/express/chapter11/3-file-image
Source: docs/express/chapter11/3-file-image.md
Description: 本章节将介绍如何在 Express 中实现文件上传功能，并处理图片文件。我们将使用 Multer 中间件来处理文件上传，学习如何保存图片文件，并实现基本的图片处理功能，如缩放、裁剪和格式转换。

在本章节中，我们将介绍如何在 Express 应用中实现文件上传和图片处理功能。我们将使用 **Multer** 中间件来处理上传的文件，并使用 **Sharp** 库对图片进行基本的处理操作，如缩放、裁剪和格式转换。通过这个例子，您将学会如何处理用户上传的文件，并对图片进行简单的操作。

## 1. 安装依赖

首先，您需要安装 **Multer** 和 **Sharp** 库：

```bash
npm install multer sharp
````

* **Multer**：用于处理 `multipart/form-data` 类型的表单数据，特别是文件上传。
* **Sharp**：用于处理和转换图片，如缩放、裁剪、格式转换等。

## 2. 设置 Multer 中间件

Multer 中间件用于处理文件上传。我们将配置 Multer 来存储文件，并设置上传文件的大小限制。

### 2.1 配置文件存储

首先，我们需要创建一个存储配置，指定上传的文件存储位置和文件名。

```js
const multer = require('multer');
const path = require('path');

// 设置存储引擎
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');  // 文件存储的目录
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname));  // 文件名使用时间戳，避免重复
  }
});

// 创建 Multer 实例
const upload = multer({
  storage: storage,
  limits: { fileSize: 10 * 1024 * 1024 },  // 限制文件大小为 10MB
});

module.exports = upload;
```

### 2.2 创建上传路由

接下来，我们在 Express 中创建一个路由，用于接收文件上传。

```js
const express = require('express');
const upload = require('../middlewares/upload');
const path = require('path');

const router = express.Router();

// 上传单个文件
router.post('/upload', upload.single('image'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: 'No file uploaded' });
  }
  res.status(200).json({
    message: 'File uploaded successfully',
    file: req.file,
    filePath: `/uploads/${req.file.filename}`,  // 返回文件的相对路径
  });
});

module.exports = router;
```

这里，我们使用了 `upload.single('image')` 来处理上传单个文件的请求。`'image'` 是表单中上传文件的字段名。

## 3. 图片处理

我们将使用 **Sharp** 来处理上传的图片，支持操作如缩放、裁剪和格式转换。

### 3.1 图片缩放

假设我们需要上传的图片在服务器中保存为原始尺寸，但我们也希望生成一张缩小版的图片，用于预览。

```js
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');

// 处理图片，生成缩小版图片
router.post('/upload', upload.single('image'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: 'No file uploaded' });
  }

  const { filename, path: filePath } = req.file;

  // 生成缩小版图片
  const thumbnailPath = path.join('uploads', 'thumbnails', `thumb_${filename}`);
  
  try {
    await sharp(filePath)
      .resize(150, 150)  // 设置宽高为 150x150
      .toFile(thumbnailPath);

    res.status(200).json({
      message: 'File uploaded and thumbnail generated',
      file: req.file,
      filePath: `/uploads/${filename}`,
      thumbnailPath: `/uploads/thumbnails/thumb_${filename}`,
    });
  } catch (err) {
    res.status(500).json({ message: 'Error processing image', error: err });
  }
});
```

### 3.2 图片裁剪

如果我们需要用户上传的图片裁剪为指定尺寸，可以使用 Sharp 来裁剪图片。

```js
router.post('/upload', upload.single('image'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: 'No file uploaded' });
  }

  const { filename, path: filePath } = req.file;

  // 设置裁剪区域
  const cropWidth = 200;
  const cropHeight = 200;

  const croppedImagePath = path.join('uploads', 'cropped', `crop_${filename}`);

  try {
    await sharp(filePath)
      .extract({ width: cropWidth, height: cropHeight, left: 0, top: 0 })  // 从图片左上角开始裁剪
      .toFile(croppedImagePath);

    res.status(200).json({
      message: 'File uploaded and image cropped',
      file: req.file,
      filePath: `/uploads/${filename}`,
      croppedImagePath: `/uploads/cropped/crop_${filename}`,
    });
  } catch (err) {
    res.status(500).json({ message: 'Error cropping image', error: err });
  }
});
```

### 3.3 图片格式转换

除了缩放和裁剪，Sharp 还支持将图片转换为其他格式，如将上传的 JPG 图片转换为 PNG 格式。

```js
router.post('/upload', upload.single('image'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: 'No file uploaded' });
  }

  const { filename, path: filePath } = req.file;

  const convertedImagePath = path.join('uploads', 'converted', `${path.parse(filename).name}.png`);

  try {
    await sharp(filePath)
      .toFormat('png')  // 转换为 PNG 格式
      .toFile(convertedImagePath);

    res.status(200).json({
      message: 'File uploaded and image converted',
      file: req.file,
      filePath: `/uploads/${filename}`,
      convertedImagePath: `/uploads/converted/${path.parse(filename).name}.png`,
    });
  } catch (err) {
    res.status(500).json({ message: 'Error converting image', error: err });
  }
});
```

## 4. 图片压缩

如果文件上传后过大，我们也可以使用 **Sharp** 来压缩图片。

```js
router.post('/upload', upload.single('image'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: 'No file uploaded' });
  }

  const { filename, path: filePath } = req.file;

  const compressedImagePath = path.join('uploads', 'compressed', `compressed_${filename}`);

  try {
    await sharp(filePath)
      .jpeg({ quality: 70 })  // 压缩成质量为 70 的 JPEG 格式
      .toFile(compressedImagePath);

    res.status(200).json({
      message: 'File uploaded and image compressed',
      file: req.file,
      filePath: `/uploads/${filename}`,
      compressedImagePath: `/uploads/compressed/compressed_${filename}`,
    });
  } catch (err) {
    res.status(500).json({ message: 'Error compressing image', error: err });
  }
});
```

## 5. 总结

在本章节中，您学会了如何使用 **Multer** 来处理 Express 中的文件上传，并结合 **Sharp** 实现了基本的图片处理功能，包括缩放、裁剪、格式转换和压缩。您可以根据实际需求，扩展更多功能，例如设置上传文件的类型限制、对多个文件的处理等。

通过这些操作，您可以为 Express 应用添加强大的文件上传和处理功能，支持用户上传图片并对其进行各种操作。
