面试官:如何优化批量图片上传?队列机制+分片处理+断点续传三连击!

大家好,我是王嗨皮,一名主业前端,副业全栈的程序员,在这里我会分享关于我工作中涉及的前端/全栈的常用技术 如果我的文章能让您有所收获,欢迎一键三连(评论,点赞,关注),感谢!


"你做过图片批量上传吗?说说你是怎么优化的。"

每次遇到这个面试问题,总不能尬答一句"用了XXX组件库"?大图卡带宽、多图传半天、网络一断从头来、并发一高服务器直接崩------这些坑你踩过几个?

这篇文章分享一下我的 Vue3 + Node.js 图片批量上传优化方案:从并发控制-> 压缩-> 分片-> 断点续传。

方案中使用的技术

· 前端及依赖:vue3viteelement-plusaxios

· 后端及依赖:nodeexpresscorsfs-extranodemonmulter

核心思路:

1.并发控制:队列机制 + 最大并发数,避免同时上传大量文件导致服务端卡顿。

2.图片压缩:通过Web Worker后台压缩,不阻塞主线程。

3.分块上传:大文件切割成 1MB 分片,降低单次请求压力,提升效率。

4.断点续传:存储已上传分块编号,意外中断后从断点继续,避免重复上传。

5.双重验证:本地记录 + 后端校验,防止脏数据(记录存在但文件丢失)。

6.实时进度:每个分块上传后更新进度条。

效果预览:

1.前端结构初始化

首先使用 vite 创建一个 vue3 项目并安装依赖插件和 element-plus 组件库,项目结构如下:

项目结构 复制代码
📦upload_front
 ┣ 📂src
 ┃ ┣ 📂components
 ┃ ┃ ┗ 📜Home.vue
 ┃ ┣ 📜App.vue
 ┃ ┣ 📜main.js
 ┣ 📜index.html
 ┣ 📜package.json
 ┣ 📜vite.config.js

接下来,在 components/Home.vue 中初始化页面结构,同时定义数据和事件处理函数。Home.vue 代码如下:

Home.vue 复制代码
<div>
    <el-upload 
      ref="uploadRef" 
      multiple
      accept="image/png,image/jpeg"
      :before-upload="beforeUpload"
      :http-request="customHttpRequest"
      :on-change="handleFileChange"
      :file-list="fileList"
      :auto-upload="false"
    >
      <el-button type="primary">批量上传图片</el-button>
    </el-upload>
    <div>
      <el-button type="primary" @click="startUpload">开始上传</el-button>
    </div>
    <div v-for="file in uploadStatus" :key="file.uid" style="margin-top: 15px;">
      <div style="margin-bottom: 5px;">
        <span>{{ file.name }}</span>
        <span v-if="file.status === 'success'" style="color: #67c23a; margin-left: 10px;">✓ 上传成功</span>
        <span v-if="file.status === 'error'" style="color: #f56c6c; margin-left: 10px;">✗ 上传失败</span>
      </div>
      <el-progress 
        :percentage="Math.floor(file.progress)" 
        :status="file.status === 'success' ? 'success' : file.status === 'error' ? 'exception' : ''"
      />
    </div>
</div>

<script setup>
    import { ref, onMounted, onUnmounted } from 'vue'
    import { ElMessage } from 'element-plus'
    import request from '@/utils/request'

    ----- 定义数据 -----
    const fileList = ref([])  // 文件列表
    const uploadStatus = ref([])  // 上传状态列表
    const uploadRef = ref(null)  // 上传组件引用

    let uploadQueue = []  // 上传队列
    let currentUploads = 0  // 当前正在上传的数量
    let worker = null  // Worker实例

    // 配置参数
    const maxConcurrentUploads = 3  // 最大并发上传数
    const maxSizeInMB = 10  // 最大文件大小(MB)
    const CHUNK_SIZE = 1 * 1024 * 1024  
    
    // 事件处理函数
    const handleFileChange = () => {}
    const beforeUpload = () => {}
    const customHttpRequest = () => {}
    const startUpload = () => {}

<script>

简单解释一下比较重要的数据定义和事件处理函数:

uploadStatus: 为状态列表,收集要上传的图片数据。

uploadQueue: 上传队列,结合最大并发数控制上传数量。

currentUploads : 监听正在上传的图片数量,如果小于 maxConcurrentUploads 则继续执行上传队列中的图片上传。

CHUNK_SIZE:设置分块大小(MB)。

handleFileChange:用户选择文件时触发。

beforeUpload:用户上传前对文件的校验。

customHttpRequest:上传时自定义的处理事件逻辑。

2.定义辅助事件处理函数

前端初始化结构完成后,继续再编写两个辅助函数

1️⃣ getFileIdentifier: 去重判断,生成唯一的文件标识符,用于断点续传和分块管理。

javasscript 复制代码
/**
 * 生成文件的唯一标识(基于原始文件信息)
 * 规则:文件名_文件大小_最后修改时间
 * 即使压缩后,也使用原始文件信息生成ID,确保不同文件ID不同
 */
const getFileIdentifier = (file) => {
  // 如果文件有原始信息(压缩后的文件),使用原始信息
  const originalFile = file._originalFile || file
  
  const safeName = originalFile.name.replace(/[^a-zA-Z0-9.-]/g, '_')
  return `${safeName}_${originalFile.size}_${originalFile.lastModified}`
}

2️⃣ updateUploadProgress: 更新文件的上传进度和状态,用于显示进度条。

javasscript 复制代码
const updateUploadProgress = (uid, progress, status) => {
  const statusItem = uploadStatus.value.find(item => item.uid === uid)
  if (statusItem) {
    statusItem.progress = progress
    statusItem.status = status
  }
}

3.添加选中的图片至上传列表

handleFileChange 事件函数中将用户上传的图片添加到 uploadStatus 数组中。

javascript 复制代码
const handleFileChange = (file, fileListData) => {
  fileList.value = fileListData
  
  // 检查判断是否已经添加过这个图片,如果id相同代表该图片已存在,直接忽略
  const exists = uploadStatus.value.find(item => item.uid === file.uid)
  if (!exists) {
    uploadStatus.value.push({
      uid: file.uid, // 图片ID
      name: file.name, 图片名称
      progress: 0, // 上传进度,初始化为0
      status: 'pending' // 上传状态,默认值为pending
    })
  }
}

注意:在图片上传到数组之前,添加一个判断,如果该图片已存在则直接忽略,避免相同图片重复上传。

4.创建Web Worker独立线程压缩图片

在项目根目录下新建 utils 文件夹,然后创建一个 imageWorker.js 文件,代码如下:

imageWorkder.js 复制代码
// imageWorker.js - 使用现代 Web Worker API
self.onmessage = async function (e) {
  const { file, quality, targetFormat, taskId } = e.data;

  try {
    const compressedFile = await compressImage(file, quality, targetFormat);
    // 返回时带上 taskId,确保消息对应正确
    self.postMessage({ 
      success: true, 
      file: compressedFile,
      taskId: taskId  // 关键:返回任务ID
    })
  } catch (error) {
    self.postMessage({ 
      success: false, 
      error: error.message,
      taskId: taskId
    })
  }
}

const compressImage = async (file, quality, format) => {
  // 使用 createImageBitmap 替代 new Image()
  const imageBitmap = await createImageBitmap(file)
  
  // 使用 OffscreenCanvas 替代 document.createElement('canvas')
  const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height)
  const ctx = canvas.getContext('2d')
  
  // 绘制图片到离屏画布
  ctx.drawImage(imageBitmap, 0, 0)
  
  // 转换为 Blob,支持格式转换和质量压缩
  const blob = await canvas.convertToBlob({
    type: `image/${format}`,
    quality: quality
  })
  
  // 将 Blob 转换为 File
  const compressedFile = new File([blob], file.name, {
    type: `image/${format}`,
    lastModified: Date.now(),
  });
  
  // 释放 ImageBitmap 资源
  imageBitmap.close();
  return compressedFile;
}

imageWorker.js 利用 Web Worker 独立线程压缩上传图片,避免阻塞主线程,确保图片压缩期间页面流畅不卡顿。

需要注意的是:Web Worker中不要使用 new Image 或者 document.createElement这种API,要更换为 createImageBitmapOffscreenCanvas

更多具体的操作,可以查询 Web Workers 可以使用的函数和类 文档。

完成 imageWorker.js后,将其导入页面中,并在 onMounted 初始化,在 onUnMounted 组件卸载时销毁 Worker 释放内存。

Home.vue 复制代码
......
import { ref, onMounted, onUnmounted } from 'vue'
let worker = null

//组件挂载时初始化Worker
onMounted(() => {
  worker = new Worker(new URL('@/utils/imageWorker.js', import.meta.url))
  console.log('Worker 已初始化')
})

//组件卸载时清理Wokker
onUnmounted(() => {
  if (worker) {
    worker.terminate()
    worker = null
    console.log('Worker 已清理')
  }
})

5.检查文件大小并压缩

接下来在 beforeUpload 事件处理函数中,校验上传图片大小并异步调用 Workder 实现图片压缩。

javascript 复制代码
const beforeUpload = (file) => {
  // 检查文件大小
  const isUnderLimit = file.size / 1024 / 1024 < maxSizeInMB
  if (!isUnderLimit) {
    ElMessage.error(`文件大小不能超过${maxSizeInMB}MB`)
    return false
  }

  // 所有图片都使用 Worker 压缩(无论大小)
  if (worker) {
    return new Promise((resolve, reject) => {
      // 生成唯一任务ID,避免多个文件同时压缩时混乱
      const taskId = `${file.name}_${file.size}_${Date.now()}_${Math.random()}`
      
      const handleMessage = (event) => {
        // 检查是否是当前任务的响应
        if (event.data.taskId === taskId) {
          worker.removeEventListener('message', handleMessage)
          worker.removeEventListener('error', handleError)
          
          if (event.data.success) {
            // 压缩后的文件
            const compressedFile = event.data.file
            
            // 关键:给压缩后的文件添加原始文件信息
            // 这样可以确保文件ID的唯一性和一致性
            compressedFile._originalFile = {
              name: file.name,
              size: file.size,
              lastModified: file.lastModified
            }
            
            console.log(`[${file.name}] 压缩完成: ${file.size} -> ${compressedFile.size} 字节`)
            resolve(compressedFile)
          } else {
            console.error(`[${file.name}] 压缩失败:`, event.data.error)
            reject(new Error(event.data.error))
          }
        }
      }
      
      const handleError = (error) => {
        worker.removeEventListener('message', handleMessage)
        worker.removeEventListener('error', handleError)
        console.error(`[${file.name}] Worker 错误:`, error)
        reject(error)
      }
      
      worker.addEventListener('message', handleMessage)
      worker.addEventListener('error', handleError)
      
      console.log(`[${file.name}] 开始压缩... (任务ID: ${taskId})`)
      worker.postMessage({ 
        file, 
        quality: 0.8,           // 压缩质量 0.8
        targetFormat: 'jpeg',   // 目标格式 JPEG
        taskId                  // 传递任务ID
      })
   })

   // 如果 Worker 未初始化,直接通过
  console.log(`[${file.name}] Worker 未初始化,不压缩`)
  return true
}

简述一下几个关键点:

1️⃣ Promise + taskId 异步压缩,通过 taskId 匹配,并发压缩时区分文件,防止返回结果混乱。

2️⃣ 在压缩后的文件上附加 _originalFile,确保后续生成的 fileId 基于原始文件,保证后续实现断点续传时的一致性。

6.队列控制同时上传并发数

继续定义一个处理上传队列的事件函数,并在上传自定义事件 customHttpRequest 进行调用。

javascript 复制代码
const customHttpRequest = (options) => {
  const { file, onProgress, onError, onSuccess } = options
  
  // 将文件和回调函数包装到队列中
  uploadQueue.push({
    file,
    onProgress,
    onError,
    onSuccess,
    uid: file.uid
  })
  
  // 开始处理队列
  processQueue()
}

const processQueue = () => {
  while (currentUploads < maxConcurrentUploads && uploadQueue.length > 0) {
    const nextFile = uploadQueue.shift()
    if (nextFile) {
      ......
    }
  }
}

processQueue 的作用控制最大上传并发数,如果当前上传数量小于最大并发数,则从待上传队列中取出下一个文件,填满并发数。在保证上传效率的同时尽量避免服务器压力过大。

7.分块上传并支持断点续传

我们再来定义一个事件处理函数,将上传的文件拆分成多个分块。

从而实现分块上传 + 断点续传 + 失败重试 + 文件合并的完整流程。

javascript 复制代码
const uploadFileWithChunks = async (fileData) => {
  currentUploads++
  const file = fileData.file
  
  // 获取文件名(可能是压缩后的文件,需要用原始文件名)
  const fileName = file._originalFile ? file._originalFile.name : file.name
  
  // 生成文件ID(使用原始文件信息)
  const fileId = getFileIdentifier(file)
  
  try {
    // 计算总分块数
    const totalChunks = Math.ceil(file.size / CHUNK_SIZE)
    console.log(`[${fileName}] 总共 ${totalChunks} 块,每块 1MB`)
    
    // 从 localStorage 读取已上传的分块(断点续传)
    const progressKey = `upload_${fileId}`
    let uploadedChunks = []
    try {
      const saved = localStorage.getItem(progressKey)
      if (saved) {
        uploadedChunks = JSON.parse(saved).chunks || []
      }
    } catch (e) {
      console.log(`[${fileName}] 无断点记录,从头开始`)
    }
    
    // 检查是否所有分块都已上传(文件已完成)
    if (uploadedChunks.length === totalChunks) {
     
      try {
        // 第二重验证:调用后端接口确认文件是否真实存在
        await request.post('/merge-chunks', {
          filename: fileName,
          fileId: fileId,
          totalChunks
        })  
      
        // 文件确实存在,跳过重复上传
        localStorage.removeItem(progressKey)
        currentUploads--
        processQueue()
        fileData.onSuccess({ success: true })
        updateUploadProgress(fileData.uid, 100, 'success')
        ElMessage.success(`${fileName} 已上传(跳过重复上传)`)
        return
        
      } catch (error) {
        // 合并失败,可能文件已被删除,清理记录重新上传
        localStorage.removeItem(progressKey)
        uploadedChunks = []  // 清空已上传记录,重新开始
      }
    }
    
    // 逐个上传分块
    for (let i = 0; i < totalChunks; i++) {
      // 跳过已上传的分块
      if (uploadedChunks.includes(i)) {
        const progress = ((i + 1) / totalChunks) * 100
        updateUploadProgress(fileData.uid, progress, 'uploading')
        continue
      }
      
      // 切割分块
      const start = i * CHUNK_SIZE
      const end = Math.min(start + CHUNK_SIZE, file.size)
      const chunk = file.slice(start, end)
      
      // 准备上传数据
      const formData = new FormData()
      formData.append('chunk', chunk)
      formData.append('filename', fileName)  // 使用原始文件名
      formData.append('fileId', fileId)
      formData.append('chunkNumber', i)
      formData.append('totalChunks', totalChunks)
      
      // 上传分块(重试3次)
      let success = false
      for (let retry = 0; retry < 3 && !success; retry++) {
        try {
          await request.post('/upload-chunk', formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
          })
          success = true
          // 记录到 localStorage(用于断点续传)
          uploadedChunks.push(i)
          localStorage.setItem(progressKey, JSON.stringify({ chunks: uploadedChunks }))
          
          // 更新进度条
          const progress = ((i + 1) / totalChunks) * 100
          updateUploadProgress(fileData.uid, progress, 'uploading')
          fileData.onProgress({ percent: progress })
          
        } catch (error) {
          if (retry < 2) {
            await new Promise(resolve => setTimeout(resolve, 1000))
          } else {
            throw new Error(`分块 ${i} 上传失败`)
          }
        }
      }
    }
    
    // 🎯 所有分块上传完成后,立即清理 localStorage
    // 这样可以避免:上传完成 → 刷新页面 → localStorage残留 → 重复上传
    localStorage.removeItem(progressKey)
    
    // 合并文件
    console.log(`[${fileName}] 开始合并...`)
    try {
      const mergeResult = await request.post('/merge-chunks', {
        filename: fileName,  // 使用原始文件名
        fileId: fileId,
        totalChunks
      })
      
      // 检查是否是跳过重复上传
      if (mergeResult.message && mergeResult.message.includes('跳过重复')) {
        console.log(`[${fileName}] ⚠️ 后端检测到重复上传,已跳过`)
      } else {
        console.log(`[${fileName}] ✓ 合并完成`)
      }
      
      currentUploads--
      processQueue()
      fileData.onSuccess({ success: true })
      updateUploadProgress(fileData.uid, 100, 'success')
      ElMessage.success(`${fileName} 上传成功`)
      
    } catch (mergeError) {
      // 合并失败的特殊处理
      const errorMsg = mergeError.message || ''
      
      // 如果是"分块目录不存在",可能是临时文件被清理了
      // 但所有分块已上传,视为成功(容错处理)
      if (errorMsg.includes('分块目录不存在') || errorMsg.includes('不存在')) {
        currentUploads--
        processQueue()
        fileData.onSuccess({ success: true })
        updateUploadProgress(fileData.uid, 100, 'success')
        ElMessage.success(`${fileName} 上传成功`)
      } else {
        // 其他合并错误,正常抛出
        throw mergeError
      }
    }
    
  } catch (error) {
    console.error(`[${fileName}] ✗ 上传失败:`, error.message)
    currentUploads--
    processQueue()
    fileData.onError(error)
    updateUploadProgress(fileData.uid, 0, 'error')
    ElMessage.error(`${fileName} 上传失败`)
  }
}

关键技术节点:

1️⃣ 双重验证:localstroage 本地记录 + 后端 merge-chunks 接口验证,防止"脏数据"的产生。

2️⃣ 断点续传:localStorage.setItem(progressKey, JSON.stringify({ chunks: uploadedChunks })),每上传一个分块保存编号,等下次断点续传时方便精准对应编号,跳过重复模块以节省资源。

3️⃣ 失败重试:for 循环重试3次,提高上传成功率。

最后,给 "开始上传" 按钮绑定 startUpload点击事件,触发 <el-upload> 上传程序。

javascript 复制代码
const startUpload = () => {
  if (uploadRef.value) {
    uploadRef.value.submit()
  }
}

8.Node.js后端代码

受限于篇幅过长的原因,后端代码不再做详细阐述,主要逻辑已在源码中标记注释。

也可点击查看 前后端完整源码

Node.js 复制代码
import express from 'express'
import cors from 'cors'
import path from 'path'
import fs from 'fs-extra'
import multer from 'multer'
import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const app = express()
app.use(cors())

const upload_dir = path.resolve(__dirname, 'uploads')
fs.ensureDirSync(upload_dir)

// 配置静态文件访问,使 /uploads 路径可以访问到 uploads 文件夹
app.use('/uploads', express.static(upload_dir))

// 配置 multer 存储
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, upload_dir)
  },
  filename: function (req, file, cb) {
    const ext = path.extname(file.originalname)
    const basename = path.basename(file.originalname, ext)
    cb(null, `${basename}-${Date.now()}${ext}`)
  }
})

const upload = multer({ storage: storage })

app.use(express.json())

// 分块上传临时目录
const temp_dir = path.resolve(__dirname, 'temp_chunks')
fs.ensureDirSync(temp_dir)

// 已完成上传的标记目录(避免重复上传)
// 注意:放在 temp_chunks 外部,避免被清理
const completed_dir = path.resolve(__dirname, 'upload_completed')
fs.ensureDirSync(completed_dir)
console.log(`完成标记目录: ${completed_dir}`)

// 上传单个分块(简化版)
app.post('/upload-chunk', upload.single('chunk'), (req, res) => {
  try {
    const { filename, fileId, chunkNumber, totalChunks } = req.body
    
    if (!req.file) {
      return res.status(400).json({
        success: false,
        message: '未接收到分块文件'
      })
    }
    
    // 使用 fileId 创建分块目录
    const chunkDir = path.resolve(temp_dir, fileId)
    fs.ensureDirSync(chunkDir)
    
    // 保存分块文件
    const chunkPath = path.resolve(chunkDir, `chunk-${chunkNumber}`)
    fs.moveSync(req.file.path, chunkPath, { overwrite: true })
    
    console.log(`已接收: ${filename} 分块 ${chunkNumber}/${totalChunks}`)
    
    res.json({
      success: true,
      message: `分块 ${chunkNumber} 上传成功`
    })
  } catch (error) {
    console.error(`分块上传失败: ${error.message}`)
    res.status(500).json({
      success: false,
      message: '分块上传失败',
      error: error.message
    })
  }
})

// 合并分块(简化版)
app.post('/merge-chunks', async (req, res) => {
  try {
    const { filename, fileId, totalChunks } = req.body
    const chunkDir = path.resolve(temp_dir, fileId)
    const completedFlag = path.resolve(completed_dir, fileId)
    
    console.log(`开始合并: ${filename} (共 ${totalChunks} 块)`)
    console.log(`分块目录: ${chunkDir}`)
    console.log(`完成标记: ${completedFlag}`)
    
    // 🎯 优先检查:该文件是否已经完成过(避免重复上传)
    if (fs.existsSync(completedFlag)) {
      try {
        const savedFilename = await fs.readFile(completedFlag, 'utf-8')
        const actualFilePath = path.resolve(upload_dir, savedFilename)
        
        // 🔍 关键检查:验证实际文件是否真实存在
        if (fs.existsSync(actualFilePath)) {
          console.log(`✓ 文件 ${filename} 已存在,跳过重复上传`)
          console.log(`  实际文件: ${savedFilename}`)
          return res.json({
            success: true,
            message: '文件已上传(跳过重复)',
            filename: savedFilename
          })
        } else {
          // ⚠️ 标记文件存在,但实际文件不存在(可能被删除)
          console.log(`⚠️ 标记存在但文件不存在: ${savedFilename}`)
          console.log(`  删除无效标记,重新上传`)
          await fs.remove(completedFlag)
          // 继续执行后续的合并流程
        }
      } catch (readError) {
        console.error(`读取完成标记失败: ${readError.message}`)
        // 如果读取失败,删除损坏的标记文件,继续合并流程
        await fs.remove(completedFlag)
      }
    }
    
    // 检查分块目录是否存在
    if (!fs.existsSync(chunkDir)) {
      console.log(`❌ 分块目录不存在: ${fileId}`)
      return res.status(400).json({
        success: false,
        message: '分块目录不存在,请重新上传'
      })
    }
    
    // 生成最终文件名
    const ext = path.extname(filename)
    const basename = path.basename(filename, ext)
    const finalFileName = `${basename}-${Date.now()}${ext}`
    const finalFilePath = path.resolve(upload_dir, finalFileName)
    
    // 创建写入流,按顺序合并分块
    const writeStream = fs.createWriteStream(finalFilePath)
    
    for (let i = 0; i < totalChunks; i++) {
      const chunkPath = path.resolve(chunkDir, `chunk-${i}`)
      
      // 检查分块是否存在
      if (!fs.existsSync(chunkPath)) {
        writeStream.close()
        return res.status(400).json({
          success: false,
          message: `分块 ${i} 不存在,请重新上传`
        })
      }
      
      // 读取并写入分块
      const chunkBuffer = await fs.readFile(chunkPath)
      writeStream.write(chunkBuffer)
    }
    
    writeStream.end()
    
    // 等待写入完成
    await new Promise((resolve, reject) => {
      writeStream.on('finish', resolve)
      writeStream.on('error', reject)
    })
    
    // 删除临时分块目录
    await fs.remove(chunkDir)
    
    // 🎯 创建完成标记(保存最终文件名,避免重复上传)
    try {
      // 确保 .completed 目录存在
      await fs.ensureDir(completed_dir)
      await fs.writeFile(completedFlag, finalFileName, 'utf-8')
      console.log(`✓ 创建完成标记: ${fileId}`)
    } catch (flagError) {
      // 标记创建失败不影响主流程,仅记录日志
      console.error(`⚠️ 创建完成标记失败: ${flagError.message}`)
    }
    
    console.log(`合并成功: ${finalFileName}`)
    
    res.json({
      success: true,
      message: '文件上传成功',
      filename: finalFileName
    })
  } catch (error) {
    console.error(`文件合并失败: ${error.message}`)
    res.status(500).json({
      success: false,
      message: '文件合并失败',
      error: error.message
    })
  }
})

// 上传图片文件接口(保留原有接口)
app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({
      success: false,
      message: 'No file uploaded'
    })
  }
  res.json({
    success: true,
    message: 'File uploaded successfully',
    filePath: req.file.path,
    filename: req.file.filename
  })
})

app.listen(3000, () => {
  console.log('Server is running on port 3000')
})

如果我的文章能让您有所收获,欢迎一键三连(评论,点赞,关注),感谢!

相关推荐
借个火er2 小时前
Qiankun vs Wujie:微前端框架深度对比
前端
freeWayWalker2 小时前
【前端工程化】前端代码规范与静态检查
前端·代码规范
C2X2 小时前
关于Git Graph展示图的理解
前端·git
昊茜Claire2 小时前
鸿蒙开发之:性能优化与调试技巧
前端
雲墨款哥2 小时前
从一行好奇的代码说起:Vue怎么没有React的<StrictMode/>
前端
小肥宅仙女2 小时前
告别繁琐!React 19 新特性对比:代码量减少 50%,异步状态从此自动管理
前端·react.js
ohyeah2 小时前
柯理化(Currying):让函数参数一个一个传递
前端·javascript
岁月宁静2 小时前
AI 多模态全栈项目实战:Vue3 + Node 打造 TTS+ASR 全家桶!
vue.js·人工智能·node.js
9坐会得自创3 小时前
使用marked将markdown渲染成HTML的基本操作
java·前端·html