You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Apollo-server-express与MongoDB上传单张/多张图片?

用 Apollo Server Express + MongoDB 实现单/多张图片上传

1. 安装依赖

先安装所需的核心包:

npm install apollo-server-express express mongoose graphql-upload graphql uuid

2. 服务器基础配置

初始化 Express 并集成 Apollo Server,同时配置文件上传中间件:

const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { graphqlUploadExpress } = require('graphql-upload');
const mongoose = require('mongoose');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');

const app = express();

// 配置文件上传限制(示例:单文件最大10MB)
app.use(graphqlUploadExpress({ maxFileSize: 10 * 1024 * 1024 }));

// 连接 MongoDB
mongoose.connect('mongodb://localhost:27017/image-upload-db')
  .then(() => console.log('MongoDB 连接成功'))
  .catch(err => console.error('MongoDB 连接失败:', err));

// 创建 Apollo Server 实例
const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: false,
  cache: 'bounded',
});

// 启动服务
async function startServer() {
  await server.start();
  server.applyMiddleware({ app });
  
  app.listen({ port: 4000 }, () => {
    console.log(`🚀 服务器运行在 http://localhost:4000${server.graphqlPath}`);
  });
}

startServer();

3. MongoDB 模型设计

创建两个模型:一个用于单张图片存储,一个用于关联多张图片的帖子(也可合并为单个模型,用数组存多张图片)。

单张图片模型 (models/SingleImage.js)

const mongoose = require('mongoose');

const singleImageSchema = new mongoose.Schema({
  filename: { type: String, required: true, unique: true },
  originalName: { type: String, required: true },
  mimeType: { type: String, required: true },
  path: { type: String, required: true }, // 本地存储路径或 GridFS ID
  size: { type: Number, required: true },
  createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('SingleImage', singleImageSchema);

多图关联帖子模型 (models/MultiImagePost.js)

const mongoose = require('mongoose');

const multiImagePostSchema = new mongoose.Schema({
  title: { type: String, required: true },
  images: [{
    filename: { type: String, required: true },
    originalName: { type: String, required: true },
    mimeType: { type: String, required: true },
    path: { type: String, required: true },
    size: { type: Number, required: true }
  }],
  createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('MultiImagePost', multiImagePostSchema);

4. GraphQL Schema 定义

创建 schema.js,定义上传标量和操作类型:

const { gql } = require('apollo-server-express');

const typeDefs = gql`
  scalar Upload

  type SingleImage {
    id: ID!
    filename: String!
    originalName: String!
    mimeType: String!
    path: String!
    size: Int!
    createdAt: String!
  }

  type MultiImagePost {
    id: ID!
    title: String!
    images: [SingleImage!]!
    createdAt: String!
  }

  type Mutation {
    uploadSingleImage(file: Upload!): SingleImage!
    uploadMultipleImages(title: String!, files: [Upload!]!): MultiImagePost!
  }
`;

module.exports = typeDefs;

5. Resolver 核心逻辑

创建 resolvers.js,处理文件存储和数据库写入:

const { createWriteStream, existsSync, mkdirSync, statSync } = require('fs');
const { join } = require('path');
const { v4: uuidv4 } = require('uuid');
const SingleImage = require('./models/SingleImage');
const MultiImagePost = require('./models/MultiImagePost');

// 确保上传目录存在
const uploadDir = join(__dirname, 'uploads');
if (!existsSync(uploadDir)) mkdirSync(uploadDir);

const resolvers = {
  Upload: require('graphql-upload').GraphQLUpload,

  Mutation: {
    // 单张图片上传
    uploadSingleImage: async (_, { file }) => {
      const { createReadStream, filename, mimetype } = await file;
      const uniqueFilename = `${uuidv4()}-${filename}`;
      const filePath = join(uploadDir, uniqueFilename);

      // 写入本地文件
      await new Promise((resolve, reject) => {
        createReadStream()
          .pipe(createWriteStream(filePath))
          .on('finish', resolve)
          .on('error', reject);
      });

      // 保存到 MongoDB
      const image = new SingleImage({
        filename: uniqueFilename,
        originalName: filename,
        mimeType: mimetype,
        path: filePath,
        size: statSync(filePath).size
      });
      await image.save();

      return image;
    },

    // 多张图片上传
    uploadMultipleImages: async (_, { title, files }) => {
      const images = [];

      for (const file of files) {
        const { createReadStream, filename, mimetype } = await file;
        const uniqueFilename = `${uuidv4()}-${filename}`;
        const filePath = join(uploadDir, uniqueFilename);

        await new Promise((resolve, reject) => {
          createReadStream()
            .pipe(createWriteStream(filePath))
            .on('finish', resolve)
            .on('error', reject);
        });

        images.push({
          filename: uniqueFilename,
          originalName: filename,
          mimeType: mimetype,
          path: filePath,
          size: statSync(filePath).size
        });
      }

      // 创建帖子并关联图片
      const post = new MultiImagePost({ title, images });
      await post.save();

      return post;
    }
  }
};

module.exports = resolvers;

6. 优化建议

  • 文件类型校验:在 resolver 中检查 mimetype,仅允许 image/jpeg、image/png 等合法图片类型。
  • GridFS 适配:若需存储超过16MB的大图片,改用 MongoDB GridFS 替代本地存储,规避单文档大小限制。
  • 静态文件路由:添加 Express 静态路由,让前端可直接访问图片:app.use('/uploads', express.static(uploadDir));
  • 异常处理:在 resolver 中添加 try-catch 块,处理文件写入失败、数据库存储异常等情况。

7. 测试上传

启动服务器后,打开 GraphQL Playground(http://localhost:4000/graphql),用以下 mutation 测试:

单张上传

mutation {
  uploadSingleImage(file: $file) {
    id
    originalName
    mimeType
    size
  }
}

在 Playground 底部的「QUERY VARIABLES」区域选择文件,变量名填 file。

多张上传

mutation {
  uploadMultipleImages(title: "旅行相册", files: $files) {
    id
    title
    images {
      originalName
      size
    }
  }
}

同样在变量区域选择多个文件,变量名填 files。

内容的提问来源于stack exchange,提问作者dmx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 01:55:38