如何在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
相关产品推荐
相关产品推荐

