前端预览doc、docx功能

90 阅读2分钟

记录开发过程

背景

前端预览文件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部署