记录开发过程
背景
前端预览文件doc、docx、pdf、xlsx,其中pdf,xlsx好处里,doc展示没有好插件,所有借助服务器转为pdf文件流,浏览器预览
环境
node 14+, libreoffice,(下载地址), 根据系统选windows版本、linux版本
代码
#安装依赖
npm i axios express libreoffice-convert
文件夹结构
- 根目录
- app.js
- converter.js
- queue.js
# 入口文件
const express = require("express");
const app = express();
const { convertDocToPdf } = require("./converter");
const Queue = require("./queue");
// 创建任务
const queue = new Queue(2);
app.use(express.json());
app.post("/convert", async (req, res) => {
try {
const { fileUrl } = req.body;
// 将转换任务加入队列
const pdfStream = await queue.add(() => convertDocToPdf(fileUrl));
// 返回 PDF 文件流
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", "attachment; filename=output.pdf");
pdfStream.pipe(res);
} catch (err) {
res.status(500).send("转换失败: " + err.message);
}
});
app.listen(3000, () => console.log("Server running on port http://localhost:3000"));
有并行任务,暂存任务队列,先进先出
class Queue {
constructor(concurrency = 1) {
this.queue = []; // 等待任务队列
this.running = 0; // 当前运行中的任务数
this.concurrency = concurrency; // 最大并发数
}
// 添加任务到队列
add(task) {
return new Promise((resolve, reject) => {
// 包装任务,保留 resolve 和 reject
const wrappedTask = async () => {
try {
const result = await task();
resolve(result);
} catch (err) {
reject(err);
} finally {
this.running--;
this.next(); // 执行下一个任务
}
};
// 将任务加入队列
this.queue.push(wrappedTask);
this.next();
});
}
// 执行下一个任务(自动触发)
next() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
// 取出任务并执行
const task = this.queue.shift();
this.running++;
task();
}
}
module.exports = Queue;
获取文件信息,并转为pdf文件流
const axios = require('axios');
const libre = require('libreoffice-convert');
async function convertDocToPdf(fileUrl) {
// 1. 下载文件
const response = await axios.get(fileUrl, { responseType: 'arraybuffer' });
const docBuffer = Buffer.from(response.data);
// 2. 转换
return new Promise((resolve, reject) => {
libre.convert(docBuffer, '.pdf', undefined, (err, pdfBuffer) => {
if (err) return reject(err);
// 返回 PDF Buffer 的可读流
const stream = require('stream');
const pdfStream = new stream.PassThrough();
pdfStream.end(pdfBuffer);
resolve(pdfStream);
});
});
}
module.exports = { convertDocToPdf };
部署
pm2配置
module.exports = {
apps: [
{
name: "doc-converter",
script: "./app.js",
instances: "max", // number:max 使用所有 CPU 核心
exec_mode: "cluster", // 集群模式
autorestart: true, // 崩溃后自动重启
max_memory_restart: "512M", // 内存超过限制时重启
watch: false,
// 公共配置 环境变量
env: {
COMMON_VAR: "value",
},
// 开发环境
env_development: {
NODE_ENV: "development",
PORT: 3000,
},
// 生产环境
env_production: {
NODE_ENV: "production",
PORT: 3000,
},
log_date_format: "YYYY-MM-DD HH:mm:ss",
error_file: "./logs/error.log", // 错误日志路径
out_file: "./logs/output.log", // 标准输出日志路径
pid_file: "./logs/pm2.pid", // PID 文件路径
},
],
};
启动脚本
#!/bin/bash
# 获取脚本所在目录的绝对路径
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 项目根目录(向上回退一级)
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# 应用配置
APP_NAME="doc-converter"
LOG_DIR="${PROJECT_ROOT}/logs"
# 创建日志目录
mkdir -p "${LOG_DIR}"
# 检查 PM2 是否安装
if ! command -v pm2 &> /dev/null; then
echo "错误: PM2 未安装,请先运行 'npm install pm2 -g'"
exit 1
fi
# 在项目根目录执行 PM2 命令
cd "${PROJECT_ROOT}" || exit 1
pm2 start pm2.config.js
pm2 save
echo "应用已启动!"
echo "日志目录: ${LOG_DIR}"
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
APP_NAME="doc-converter"
cd "${PROJECT_ROOT}" || exit 1
pm2 stop "${APP_NAME}"
pm2 delete "${APP_NAME}"
# 可选:清理旧日志(谨慎使用)
# rm -f "${PROJECT_ROOT}/logs"/*.log
echo "应用已停止!"
未完,后续优化pm2部署