seansie's blog

Node.js 試算表利器:ExcelJS 完整實戰指南(概念、語法與 CRUD 操作)

在現代 Web 應用程式與後端系統開發中,Excel 檔案(.xlsx)的匯入、匯出與自動化處理是極為常見的需求。無論是財務報表生成、批次資料匯入,還是複雜試算表的客製化排版,選擇一個合適且穩健的工具至關重要。

在 Node.js 生態系中,ExcelJS 憑藉著物件導向的 API 設計、強大的樣式自訂能力,以及對大型檔案串流(Streaming)的完整支援,成為許多企業級專案的首選方案。本文將深入介紹 ExcelJS 的核心架構,並透過完整的 CRUD(Create, Read, Update, Delete)範例,協助您在專案中快速落地應用。


一、ExcelJS 套件介紹與核心優勢

ExcelJS 是一個專為 Node.js 與瀏覽器環境打造的 Excel 檔案處理函式庫,專注於提供現代 .xlsx 格式的深度控制。相較於其他純資料轉換的工具,ExcelJS 的優勢體現在以下面向:

  1. 完整的樣式控制(Styling):支援字型、前景色/背景色、外框、對齊方式、數字格式(Number Formats)以及富文本(Rich Text)。
  2. 結構化物件導向模型:按照 Workbook(活頁簿)$\rightarrow$ Worksheet(工作表)$\rightarrow$ Row(資料列)$\rightarrow$ Cell(儲存格)的層級進行操作,邏輯清晰直覺。
  3. 優異的記憶體管理(Stream I/O):提供基於 Node.js Stream 的 Reader 與 Writer,即便處理數十萬筆的大型資料集,也能有效避免記憶體溢出(Out-of-Memory, OOM)。
  4. 進階試算表特性:支援公式運算結果保留、合併儲存格(Merge Cells)、資料驗證(Data Validation)、凍結窗格(Frozen Panes)及插入圖片。

二、安裝與環境準備

在 Node.js 專案中,透過 npm 或 yarn 即可完成安裝:

npm install exceljs

在程式碼中引入套件:

const ExcelJS = require('exceljs');
// 若使用 ES Module:
// import ExcelJS from 'exceljs';

三、ExcelJS 核心 CRUD 實戰

接下來,我們將以具體的程式碼範例展示如何對 Excel 檔案進行建立(Create)讀取(Read)、修改(Update)刪除(Delete)操作。


1. 建立(Create):建立活頁簿、設定欄位與寫入資料

建立 Excel 時,通常會先定義工作表的欄位結構(Columns),再逐筆或批次新增資料列(Rows),並可針對表頭進行樣式美化。

const ExcelJS = require('exceljs');

async function createExcelFile() {
  // 1. 初始化活頁簿
  const workbook = new ExcelJS.Workbook();
  workbook.creator = 'System Admin';
  workbook.created = new Date();

  // 2. 新增工作表
  const worksheet = workbook.addWorksheet('員工名單');

  // 3. 定義欄位標頭與寬度
  worksheet.columns = [
    { header: '員工編號', key: 'id', width: 15 },
    { header: '姓名', key: 'name', width: 20 },
    { header: '部門', key: 'dept', width: 20 },
    { header: '薪資 (TWD)', key: 'salary', width: 18, style: { numFmt: '#,##0' } },
    { header: '到職日期', key: 'joinedDate', width: 18, style: { numFmt: 'yyyy-mm-dd' } }
  ];

  // 4. 新增資料列
  worksheet.addRow({ id: 'EMP001', name: '王小明', dept: '技術部', salary: 75000, joinedDate: new Date('2022-03-15') });
  worksheet.addRow({ id: 'EMP002', name: '李美華', dept: '行銷部', salary: 62000, joinedDate: new Date('2023-01-10') });
  worksheet.addRow({ id: 'EMP003', name: '張志豪', dept: '產品部', salary: 81000, joinedDate: new Date('2021-08-01') });

  // 5. 表頭樣式美化(深色背景、白色粗體字、置中)
  const headerRow = worksheet.getRow(1);
  headerRow.eachCell((cell) => {
    cell.font = { name: 'Microsoft JhengHei', bold: true, color: { argb: 'FFFFFFFF' } };
    cell.fill = {
      type: 'pattern',
      pattern: 'solid',
      fgColor: { argb: 'FF1F4E79' } // 深藍色
    };
    cell.alignment = { vertical: 'middle', horizontal: 'center' };
  });
  headerRow.height = 25;

  // 6. 儲存檔案
  await workbook.xlsx.writeFile('員工清冊.xlsx');
  console.log('檔案建立成功:員工清冊.xlsx');
}

createExcelFile();

2. 讀取(Read):載入檔案並解析內容

讀取既有檔案時,ExcelJS 提供了便利的走訪機制,可逐列(Row)或逐格(Cell)取得資料。

const ExcelJS = require('exceljs');

async function readExcelFile() {
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.readFile('員工清冊.xlsx');

  // 取得第一個工作表(亦可使用 workbook.getWorksheet('工作表名稱'))
  const worksheet = workbook.worksheets[0];
  const records = [];

  console.log(`讀取工作表:${worksheet.name},總列數:${worksheet.rowCount}`);

  // 走訪每一列資料(排除第 1 列表頭)
  worksheet.eachRow((row, rowNumber) => {
    if (rowNumber === 1) return;

    // 取得各儲存格數值
    const rowData = {
      rowNumber,
      id: row.getCell(1).value,
      name: row.getCell(2).value,
      dept: row.getCell(3).value,
      salary: row.getCell(4).value,
      joinedDate: row.getCell(5).value
    };

    records.push(rowData);
  });

  console.log('解析結果:', records);
}

readExcelFile();

3. 修改(Update):更新內容、調整格式與增加計算公式

修改既有 Excel 時,可先讀入檔案,針對特定的儲存格重新賦值、套用樣式,甚至新增總計列與公式。

const ExcelJS = require('exceljs');

async function updateExcelFile() {
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.readFile('員工清冊.xlsx');
  const worksheet = workbook.getWorksheet('員工名單');

  // 情境 A:精準修改特定儲存格(例如:調升 EMP001 的薪資)
  const targetCell = worksheet.getCell('D2');
  targetCell.value = 80000;
  targetCell.font = { color: { argb: 'FFC00000' }, bold: true }; // 標註為紅色加粗

  // 情境 B:在底部新增「薪資總計」與公式計算
  const totalRowNumber = worksheet.rowCount + 1;
  const totalRow = worksheet.getRow(totalRowNumber);

  totalRow.getCell(3).value = '薪資總計';
  totalRow.getCell(3).font = { bold: true };
  totalRow.getCell(3).alignment = { horizontal: 'right' };

  // 設定 Excel 內建公式 SUM(D2:D4)
  totalRow.getCell(4).value = { formula: `SUM(D2:D${totalRowNumber - 1})` };
  totalRow.getCell(4).numFmt = '#,##0';
  totalRow.getCell(4).font = { bold: true };

  // 加上雙底線樣式以符合會計規範
  totalRow.getCell(4).border = {
    top: { style: 'thin' },
    bottom: { style: 'double' }
  };

  // 儲存為新檔案或覆寫原檔
  await workbook.xlsx.writeFile('員工清冊_已更新.xlsx');
  console.log('檔案修改成功:員工清冊_已更新.xlsx');
}

updateExcelFile();

4. 刪除(Delete):刪除指定資料列或欄位

ExcelJS 支援動態移除指定的資料列(Row)或欄(Column),並自動重整後續的行索引。

const ExcelJS = require('exceljs');

async function deleteRowAndColumn() {
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.readFile('員工清冊.xlsx');
  const worksheet = workbook.getWorksheet('員工名單');

  // 1. 刪除指定的資料列:spliceRows(開始列號, 刪除列數)
  // 例如:刪除第 3 列(EMP002)
  worksheet.spliceRows(3, 1);

  // 2. 若要刪除指定欄位:spliceColumns(開始欄號, 刪除欄數)
  // 例如:刪除「到職日期」這一欄(第 5 欄)
  // worksheet.spliceColumns(5, 1);

  await workbook.xlsx.writeFile('員工清冊_已刪除部分資料.xlsx');
  console.log('刪除作業完成。');
}

deleteRowAndColumn();

四、大型檔案高效能處理:Stream 寫入技巧

當匯出資料量達到數萬甚至數十萬筆時,將所有資料一次性載入記憶體容易導致 Node.js 程序耗盡記憶體而崩潰。ExcelJS 提供了串流寫入器(Stream Writer),能以極低且穩定的記憶體佔用完成匯出。

const ExcelJS = require('exceljs');
const fs = require('fs');

async function exportLargeDataStream() {
  const options = {
    filename: '大型報表.xlsx',
    useStyles: true,
    useSharedStrings: true
  };

  // 建立串流活頁簿寫入器
  const workbook = new ExcelJS.stream.xlsx.WorkbookWriter(options);
  const worksheet = workbook.addWorksheet('大數據清單');

  worksheet.columns = [
    { header: '序號', key: 'id', width: 10 },
    { header: '交易代碼', key: 'code', width: 25 },
    { header: '金額', key: 'amount', width: 15 }
  ];

  // 模擬寫入 50,000 筆資料
  for (let i = 1; i <= 50000; i++) {
    worksheet.addRow({
      id: i,
      code: `TXN-${Date.now()}-${i}`,
      amount: Math.floor(Math.random() * 10000)
    }).commit(); // 每一列寫入後立即 commit 釋放記憶體
  }

  // 完成工作表與活頁簿
  await worksheet.commit();
  await workbook.commit();

  console.log('大型串流匯出完成。');
}

exportLargeDataStream();

五、實務開發的最佳實踐與建議

  1. 日期格式轉換:Excel 內部對日期的儲存是基於序列值。使用 ExcelJS 時,建議直接傳入 JavaScript Date 物件,並在欄位或儲存格中明確指定 numFmt: 'yyyy-mm-dd hh:mm:ss',以確保跨平台開啟時格式一致。
  2. 公式計算特性:ExcelJS 寫入公式(如 { formula: 'SUM(A1:A10)' })時,主要負責建立公式結構。若未指定 result 預算值,實際計算結果會在使用者以 Microsoft Excel 或 LibreOffice 開啟該檔案時自動運算呈現。
  3. Web API 檔案下載整合:在 Express 或 NestJS 等後端框架中,若需要讓使用者下載產生的 Excel,可利用 workbook.xlsx.write(res) 直接將檔案內容寫入 HTTP Response 串流,無需在伺服器硬碟儲存暫存檔:
// Express.js 範例
app.get('/download-report', async (req, res) => {
  const workbook = new ExcelJS.Workbook();
  const worksheet = workbook.addWorksheet('Report');
  worksheet.addRow(['項目', '數值']);
  worksheet.addRow(['A', 100]);

  res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  res.setHeader('Content-Disposition', 'attachment; filename="report.xlsx"');

  await workbook.xlsx.write(res);
  res.end();
});

六、結語

ExcelJS 兼具直覺的 API 設計強大的排版樣式支援,同時在面對巨量資料時提供了穩健的 Stream 機制。在 Node.js 開發中,若您的應用場景需要產出專業排版、套用品牌視覺、撰寫公式,或是安全處理大型試算表匯出,ExcelJS 是相當成熟且值得信賴的選擇。