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

如何在Express.js中让耗时文件转换后台执行以支持多请求

解决Express路由中耗时文件转换操作阻塞请求的问题

我是Node和Express的新手,正在开发一个接收用户上传文件并转换为其他格式的路由。当前遇到的问题是:用户发送第二次请求时,必须等待第一次的文件处理完成才能上传新文件。

问题核心在于convertFile是耗时操作,会占用服务器资源导致无法接收新请求。我本以为在newProject.save()保存到MongoDB后返回响应,服务器就能接收新请求,但实际服务器仍会被之前的convertFile处理占用,无法响应新请求。

路由代码

router.post('/', upload.fields([{ name: 'zip' }]), async (req, res, next) => {
  let data = {
    title: req.body.title,
    description: req.body.description,
  }

  if (req.files && req.files.zip) {
    const newProject = new MongoProject(data);
    newProject.save()
      .then(async (project) => {
        res.send(project);
        console.log("Project created");


        const uploadedFilePath = path.normalize(req.files.zip[0].path);

        // 下面的convertFile是耗时方法
        const extractZipinfo = await convertFile(uploadedFilePath , data.masterFile).then((zipInfo) => {
          console.log({ zipInfo })

          data.zipInfo = {
            sizes: zipInfo.sizes
          }
        })
      })
      .catch(err => {
        console.log("Error creating project");
        return res.send(err);
      })

  }
})

convertFile函数简化版

async function convertFile(inputFilePath, outputInfo) {
    const outputFilePath = "output.abc";
    const jsonFilePath = "output.json";
    const doc = new Document(); // 存储最终输出文件数据的类

    const _FileAPI = new fileAPI();
    const outputFinalData = await _FileAPI.Init() // 异步方法
        .then(() => {
            const dataClass = initiateClass(); // 存储JSON格式数据的类
            const paragraphs = _FileAPI.GetallParagraphs(inputFilePath);
            for (let i = 0, len = paragraphs.size(); i < len; i++) {
                for (let j = 0, lenj = paragraphs.size(); j < lenj; j++) {
                    const para = paragraphs.get(j);
                    // 读取每个段落并将每个单词首字母大写
                    dataClass.paragraphs.push(para);
                }
            }
            fs.writeFileSync(jsonFilePath, JSON.stringify(dataClass, null, 2), 'utf-8');
            console.log("then")
        }).then(() => {
            const io = new NodeIO(); // 帮助写入目标格式文件的类
            const outData = io.write(outputFilePath, doc).then(() => {
                outputInfo.sizes.push(fs.statSync(outputFilePath).size);
                return outputInfo;
            });
            return outData;
        });
    return outputFinalData;
}

解决方案:让耗时操作脱离主事件循环

1. 移除await,让convertFile后台执行

当前代码中await convertFile(...)会阻塞后续逻辑,即使已经返回响应,Node.js的事件循环仍会被占用。去掉await直接调用,让转换任务在后台异步执行:

router.post('/', upload.fields([{ name: 'zip' }]), async (req, res, next) => {
  let data = {
    title: req.body.title,
    description: req.body.description,
    sizes: [] // 确保outputInfo.sizes存在,避免报错
  }

  if (req.files && req.files.zip) {
    const newProject = new MongoProject(data);
    newProject.save()
      .then((project) => {
        res.send(project);
        console.log("项目创建成功");

        const uploadedFilePath = path.normalize(req.files.zip[0].path);

        // 直接调用convertFile,不等待结果,后台运行
        convertFile(uploadedFilePath, data)
          .then((zipInfo) => {
            console.log({ zipInfo });
            // 转换完成后更新MongoDB中的项目信息
            MongoProject.findByIdAndUpdate(project._id, { zipInfo: { sizes: zipInfo.sizes } })
              .catch(err => console.log("更新项目失败:", err));
          })
          .catch(err => console.log("文件转换失败:", err));
      })
      .catch(err => {
        console.log("创建项目失败");
        return res.send(err);
      });
  }
})

2. 替换同步文件操作为异步版本

convertFile中的fs.writeFileSync和fs.statSync是同步阻塞操作,会占用事件循环。换成异步的fs.promises.writeFile和fs.promises.stat,避免阻塞:

async function convertFile(inputFilePath, outputInfo) {
    const outputFilePath = "output.abc";
    const jsonFilePath = "output.json";
    const doc = new Document();

    const _FileAPI = new fileAPI();
    await _FileAPI.Init();

    const dataClass = initiateClass();
    const paragraphs = _FileAPI.GetallParagraphs(inputFilePath);
    for (let i = 0, len = paragraphs.size(); i < len; i++) {
        for (let j = 0, lenj = paragraphs.size(); j < lenj; j++) {
            const para = paragraphs.get(j);
            dataClass.paragraphs.push(para);
        }
    }

    // 异步写入JSON文件
    await fs.promises.writeFile(jsonFilePath, JSON.stringify(dataClass, null, 2), 'utf-8');
    console.log("JSON文件写入完成");

    const io = new NodeIO();
    await io.write(outputFilePath, doc);
    
    // 异步获取文件大小
    const stats = await fs.promises.stat(outputFilePath);
    outputInfo.sizes.push(stats.size);
    
    return outputInfo;
}

3. 进阶:用Worker Threads处理CPU密集型任务(可选)

如果文件转换是CPU密集型操作,单线程Node.js仍会受限。可以用worker_threads模块在单独线程中处理,完全不阻塞主事件循环:

步骤1:创建单独的转换线程文件file-converter.js

const { parentPort, workerData } = require('worker_threads');
const fs = require('fs').promises;
// 导入你的相关类和方法
const fileAPI = require('./path-to-fileAPI');
const NodeIO = require('./path-to-NodeIO');
const Document = require('./path-to-Document');
const initiateClass = require('./path-to-initiateClass');

async function convertFile(inputFilePath, outputInfo) {
    const outputFilePath = "output.abc";
    const jsonFilePath = "output.json";
    const doc = new Document();

    const _FileAPI = new fileAPI();
    await _FileAPI.Init();

    const dataClass = initiateClass();
    const paragraphs = _FileAPI.GetallParagraphs(inputFilePath);
    for (let i = 0, len = paragraphs.size(); i < len; i++) {
        for (let j = 0, lenj = paragraphs.size(); j < lenj; j++) {
            const para = paragraphs.get(j);
            dataClass.paragraphs.push(para);
        }
    }

    await fs.writeFile(jsonFilePath, JSON.stringify(dataClass, null, 2), 'utf-8');
    console.log("JSON文件写入完成");

    const io = new NodeIO();
    await io.write(outputFilePath, doc);
    
    const stats = await fs.stat(outputFilePath);
    outputInfo.sizes.push(stats.size);
    
    return outputInfo;
}

// 执行转换并向主进程发送结果
convertFile(workerData.inputFilePath, workerData.outputInfo)
    .then(result => parentPort.postMessage({ success: true, result }))
    .catch(err => parentPort.postMessage({ success: false, error: err.message }));

步骤2:在路由中调用Worker

const { Worker } = require('worker_threads');

router.post('/', upload.fields([{ name: 'zip' }]), async (req, res, next) => {
  let data = {
    title: req.body.title,
    description: req.body.description,
    sizes: []
  }

  if (req.files && req.files.zip) {
    const newProject = new MongoProject(data);
    newProject.save()
      .then((project) => {
        res.send(project);
        console.log("项目创建成功");

        const uploadedFilePath = path.normalize(req.files.zip[0].path);

        // 创建Worker处理转换任务
        const worker = new Worker('./file-converter.js', {
          workerData: {
            inputFilePath: uploadedFilePath,
            outputInfo: data
          }
        });

        worker.on('message', (message) => {
          if (message.success) {
            console.log({ zipInfo: message.result });
            MongoProject.findByIdAndUpdate(project._id, { zipInfo: { sizes: message.result.sizes } })
              .catch(err => console.log("更新项目失败:", err));
          } else {
            console.log("文件转换失败:", message.error);
          }
        });

        worker.on('error', (err) => {
          console.log("Worker错误:", err);
        });

        worker.on('exit', (code) => {
          if (code !== 0) {
            console.log(`Worker退出,代码:${code}`);
          }
        });
      })
      .catch(err => {
        console.log("创建项目失败");
        return res.send(err);
      });
  }
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 03:10:17