Node.js 导出 Excel 完整指南:表头样式、合并单元格、图片插入与自动扩展封装

2026-07-13 172 浏览 0 评论

在实际业务开发中,Excel 导出几乎是后台系统的标配能力,尤其在电商、报表、数据分析等场景中,对格式和功能的要求往往非常高,例如:

  • 自定义表头样式(颜色、字体、对齐)
  • 合并单元格(多级表头、说明行)
  • 插入图片(商品图、头像等)
  • 支持多种图片来源(本地 / URL / Base64)
  • 自动适配布局

本文将从基础能力入手,逐步实现一个工程化的 Excel 导出方案,并最终抽象出一个通用的 addImageAuto 方法。可能你并不需要复杂的功能,那你可以抽取出其中的关键代码自我实现功能。​


一、基础能力:使用 ExcelJS 导出 Excel

安装依赖:

npm install exceljs

基础示例:

const ExcelJS = require('exceljs');

async function exportExcel() {
  const workbook = new ExcelJS.Workbook();
  const worksheet = workbook.addWorksheet('Sheet1');

  // 定义列
  worksheet.columns = [
    { header: '姓名', key: 'name', width: 20 },
    { header: '年龄', key: 'age', width: 10 },
    { header: '地址', key: 'address', width: 30 },
  ];

  // 表头样式
  const headerRow = worksheet.getRow(1);
  headerRow.eachCell((cell) => {
    cell.font = {
      bold: true,
      color: { argb: 'FFFFFFFF' },
    };

    cell.alignment = {
      horizontal: 'center',
      vertical: 'middle',
    };

    cell.fill = {
      type: 'pattern',
      pattern: 'solid',
      fgColor: { argb: 'FF4472C4' },
    };
  });

  // 数据
  worksheet.addRow({ name: '张三', age: 18, address: '北京' });
  worksheet.addRow({ name: '李四', age: 22, address: '上海' });

  // 合并单元格
  worksheet.mergeCells('A4:C4');
  worksheet.getCell('A4').value = '合并单元格示例';

  await workbook.xlsx.writeFile('output.xlsx');
}

exportExcel();

二、插入图片:基础实现

const imageId = workbook.addImage({
  filename: './logo.png',
  extension: 'png',
});

worksheet.addImage(imageId, {
  tl: { col: 0, row: 5 },
  ext: { width: 200, height: 100 },
});

问题在于: extension 需要手动指定,这在动态场景下不可靠。


三、自动识别图片扩展名

1. 基于文件路径

const path = require('path');

function getImageExtension(filePath) {
  const ext = path.extname(filePath).toLowerCase().replace('.', '');

  const map = {
    jpg: 'jpeg',
    jpeg: 'jpeg',
    png: 'png',
    gif: 'gif',
  };

  if (!map[ext]) {
    throw new Error(`不支持的图片格式: ${ext}`);
  }

  return map[ext];
}

2. 基于 Buffer(文件头判断)

function getImageExtensionFromBuffer(buffer) {
  if (buffer.slice(0, 4).toString('hex') === '89504e47') {
    return 'png';
  }
  if (buffer.slice(0, 3).toString('hex') === 'ffd8ff') {
    return 'jpeg';
  }
  if (buffer.slice(0, 3).toString() === 'GIF') {
    return 'gif';
  }

  throw new Error('无法识别图片格式');
}

四、工程化封装:addImageAuto

为了支持多种输入来源(路径 / URL / Buffer / Base64),可以封装统一入口。

核心实现

const fs = require('fs');
const path = require('path');
const axios = require('axios');

// 自动识别扩展名
function detectExtension(input, buffer) {
  if (typeof input === 'string') {
    if (input.startsWith('data:image')) {
      const match = input.match(/^data:image\/(\w+);base64,/);
      return match ? match[1] : 'png';
    }

    const ext = path.extname(input).toLowerCase().replace('.', '');
    if (ext) {
      return ext === 'jpg' ? 'jpeg' : ext;
    }
  }

  if (buffer) {
    const hex = buffer.slice(0, 4).toString('hex');
    if (hex.startsWith('89504e47')) return 'png';
    if (hex.startsWith('ffd8ff')) return 'jpeg';
    if (buffer.slice(0, 3).toString() === 'GIF') return 'gif';
  }

  return 'png';
}

// 加载图片
async function loadImage(input) {
  if (Buffer.isBuffer(input)) {
    return { buffer: input };
  }

  if (typeof input === 'string' && input.startsWith('data:image')) {
    const base64 = input.split(',')[1];
    return { buffer: Buffer.from(base64, 'base64') };
  }

  if (typeof input === 'string' && /^https?:\/\//.test(input)) {
    const res = await axios.get(input, { responseType: 'arraybuffer' });
    return { buffer: Buffer.from(res.data) };
  }

  if (typeof input === 'string') {
    return { filename: input };
  }

  throw new Error('不支持的图片类型');
}

// 主方法
async function addImageAuto(workbook, worksheet, input, options = {}) {
  const { buffer, filename } = await loadImage(input);
  const extension = detectExtension(input, buffer);

  const imageId = workbook.addImage({
    ...(buffer ? { buffer } : { filename }),
    extension,
  });

  if (options.range) {
    worksheet.addImage(imageId, options.range);
  } else {
    worksheet.addImage(imageId, {
      tl: options.tl || { col: 0, row: 0 },
      ext: options.ext || { width: 100, height: 100 },
    });
  }

  return imageId;
}

五、多场景使用示例

本地图片

await addImageAuto(workbook, worksheet, './logo.png', {
  tl: { col: 0, row: 5 },
  ext: { width: 200, height: 100 },
});

URL 图片

await addImageAuto(
  workbook,
  worksheet,
  'https://example.com/logo.jpg',
  {
    range: 'B6:D10',
  }
);

Buffer

const buffer = fs.readFileSync('./logo.png');

await addImageAuto(workbook, worksheet, buffer, {
  tl: { col: 2, row: 5 },
});

Base64

const base64 = 'data:image/png;base64,...';

await addImageAuto(workbook, worksheet, base64, {
  tl: { col: 4, row: 5 },
});

六、自动适配单元格尺寸

function fitCellToImage(worksheet, col, row, width, height) {
  worksheet.getColumn(col + 1).width = width / 7;
  worksheet.getRow(row + 1).height = height * 0.75;
}

七、总结

通过以上步骤,已经实现了一套完整的 Excel 导出能力:

  • 表头样式控制(颜色、字体、对齐)
  • 合并单元格
  • 图片插入
  • 自动识别图片类型
  • 支持多种图片输入方式
  • 通用方法封装(addImageAuto)
  • 自动适配布局

该方案适用于大多数业务导出场景,并具备良好的扩展性,可以进一步构建复杂报表或商品导出系统。


发布评论

发布评论前请先 登录
0 评论
点赞
收藏

评论列表 0

暂无评论