開発

ExcelJSとは:Node.jsでExcel帳票を作る使い方と10万行の出力、保守停止の注意点【2026年11月】

ExcelJSとは:Node.jsでExcel帳票を作る使い方と10万行の出力、保守停止の注意点【2026年11月】

ExcelJSは、Node.jsとブラウザでxlsxファイルを読み書きするJavaScriptのライブラリです。セルの値だけでなく、フォント・罫線・塗りつぶし・表示形式・セル結合・印刷設定まで書き込めるため、業務システムから請求一覧や月次の集計表をExcelで出す用途でよく選ばれています。この記事では、2026年11月1日時点の最新版4.4.0を前提に、帳票を生成するコード、既存ファイルの読み込み、日付が1日ずれる原因、10万行を超える出力のストリーミング、そして2024年1月以降コミットが止まっている現状を踏まえた採用条件までを実装者向けに整理します。

まとめ:ExcelJSは書式付き帳票の出力向け、保守停止を前提に依存を管理する

書式を作り込んだExcelをNode.jsから出すなら、ExcelJSは第一候補になります。列幅・表示形式・セル結合・印刷範囲まで、公式READMEに書かれたAPIだけで組めます。

実装で詰まりやすい点は3つです。日付はUTC基準でExcelの値に変換されるため、日本時間の0時で作ったDateは前日になります。スタイルのオブジェクトは複数セルで共有されるため、書き換えるときは複製してから代入する手順です。数式は計算されないので、結果を自分で渡すか、開いたときに再計算させます。

注意したいのは保守の状況です。npmの最新は2023年10月の4.4.0のままで、リポジトリの既定ブランチも2024年1月を最後に更新がありません。社内で作った帳票を出すだけなら問題は小さい一方、外部の利用者がアップロードするxlsxを無制限に読む用途では、入力の制限と隔離を自前で用意できない限り採用を見送ります。

ExcelJSとは:Node.js・ブラウザ向けxlsx入出力とMITライセンス

ExcelJSはGitHubのexceljs/exceljsリポジトリで開発されているOSSで、ライセンスはMITです。Node.jsではファイル・ストリーム・Bufferのどれからでもxlsxを読み書きでき、ブラウザでもストリーミング以外の機能を使えます。

最新は4.4.0、npm週2,500万DLでも2024年1月以降コミットが止まった現状

npmの登録情報では、latestタグは4.4.0で、公開日は2023年10月19日です。GitHubのv4.4.0リリースノートには、条件付き書式の表示形式の修正やinlineStr形式のセルへの対応などが並びます。その後に出たのは2024年12月20日の4.4.1-prerelease.0だけで、正式版はありません。

利用は減っていません。npmの集計では、2026年10月23日〜29日の1週間で約2,563万回ダウンロードされています。リポジトリのスターは約1万5,500です。

一方で、既定ブランチの最終コミットは2024年1月12日(テーブル列のスタイル修正)です。ピボットテーブルの追加は2023年10月31日のコミットで、4.4.0の公開より後のため、npmで入る版には含まれていません。READMEに書かれている機能がすべてnpm版で使えるとは限らない点に気をつけてください。

できることと対象外:数式の計算・xls形式・グラフ作成は扱えない

READMEの目次に並ぶ機能は、シートの追加と削除、列と行、セル結合、入力規則、コメント、テーブル、スタイル、条件付き書式、アウトライン、画像、シートの保護、ヘッダーとフッター、印刷設定です。帳票として見栄えを整えるのに要る機能はおおむね揃っています。

やりたいこと ExcelJSでの扱い
書式付きのxlsxを出す font・fill・border・numFmt指定
セル結合・印刷範囲 mergeCellsとpageSetupで対応
CSVの読み書き workbook.csvで対応
数式の結果を計算する 非対応(結果は自分で渡す)
旧形式のxlsを読む 対象外(xlsxとCSVのみ)
グラフを作る READMEに機能の記載なし

グラフ入りの帳票が要るときは、グラフを置いたテンプレートのxlsxを読み込み、参照先のセルへ値を書き込む方法が現実的です。ただし、読み込んで書き戻したときに元のグラフが保たれるかはファイルごとに確かめてください。

インストールから帳票生成まで:npmで入れて書式付きxlsxを書き出す手順

ここからは、請求一覧を1枚のシートに出す例で、生成から配信までを順に見ていきます。コードは公式READMEの記法に沿って組んだもので、Node.jsの現行LTSを想定しています。

npm install exceljsとCommonJS・ESMの読み込みで押さえる違い

インストールはnpm install exceljsだけです。依存にはjszip・archiver・unzipper・saxes・fast-csv・dayjsなどが入り、package.jsonのenginesはNode.js 8.3.0以上と古いままです。

npm install exceljs

// CommonJS
const ExcelJS = require('exceljs');

// ESM(package.json に "type": "module" を置いた場合)
import ExcelJS from 'exceljs';

パッケージ本体はCommonJSで書かれているため、ESMからは既定のimportで受け取ります。import { Workbook } from 'exceljs'のような名前付きimportが通るかはNode.jsの解析に左右されるので、既定importにそろえておくと迷いません。両方式の相互運用はESMとCommonJSの違いとrequire(esm)の移行手順で詳しく扱っています。Node.js自体の版の選び方はNode.jsの仕組みと版の選び方を参照してください。

列定義・表示形式・セル結合・印刷設定を入れた請求一覧の生成コード

次のコードは、1行目にセル結合したタイトル、2行目に見出し、3行目以降に明細、最後の行に合計の数式を置き、A4横で印刷できるxlsxを書き出します。

const ExcelJS = require('exceljs');

async function main() {
  const workbook = new ExcelJS.Workbook();
  workbook.creator = '請求管理システム';

  const ws = workbook.addWorksheet('請求一覧', {
    pageSetup: { paperSize: 9, orientation: 'landscape' }, // 9 = A4
    views: [{ state: 'frozen', ySplit: 2 }],                 // 見出しまで固定
  });

  // 列は key と幅・表示形式だけ決め、見出しは2行目に自分で書く
  ws.columns = [
    { key: 'no', width: 14 },
    { key: 'client', width: 28 },
    { key: 'date', width: 12, style: { numFmt: 'yyyy/mm/dd' } },
    { key: 'amount', width: 14, style: { numFmt: '#,##0' } },
  ];

  ws.mergeCells('A1:D1');
  ws.getCell('A1').value = '2026年10月 請求一覧';
  ws.getCell('A1').font = { size: 14, bold: true };
  ws.getCell('A1').alignment = { horizontal: 'center' };

  ws.getRow(2).values = ['請求番号', '取引先', '請求日', '金額'];
  ws.getRow(2).font = { bold: true };

  const rows = [
    { no: 'INV-0001', client: '株式会社サンプル', date: new Date(Date.UTC(2026, 9, 31)), amount: 132000 },
    { no: 'INV-0002', client: '有限会社テスト', date: new Date(Date.UTC(2026, 9, 31)), amount: 88000 },
  ];
  rows.forEach((r) => ws.addRow(r));

  const last = ws.rowCount;
  const total = rows.reduce((sum, r) => sum + r.amount, 0);
  ws.getCell(`D${last + 1}`).value = { formula: `SUM(D3:D${last})`, result: total };

  ws.pageSetup.printTitlesRow = '2:2'; // 2ページ目以降も見出しを印刷
  await workbook.xlsx.writeFile('invoices.xlsx');
}

main().catch((e) => { console.error(e); process.exit(1); });

見出しをws.columnsのheaderで書くと、1行目に見出しが入ります。タイトル行を別に置く帳票では、この例のようにheaderを指定せず、見出し行を自分で書くと行の位置がずれません。paperSize: 9がA4、printTitlesRowが各ページに繰り返す行で、どちらもREADMEのPage Setupの項に表があります。

日付が1日ずれる原因はUTC換算、Date.UTCで作り直す

上のコードでnew Date(2026, 9, 31)ではなくDate.UTCを使ったのには理由があります。ExcelJSのソースのlib/utils/utils.jsにあるdateToExcelは、DateのgetTime()(UTCのミリ秒)をそのまま日数に換算しています。

日本時間の2026年10月31日0時は、UTCでは10月30日15時です。この値がExcelに書かれると、表示形式がyyyy/mm/ddなら「2026/10/30」と出ます。サーバーのタイムゾーンをAsia/Tokyoにしている環境ほど起きやすい不具合です。

対処は、日付だけを持つ値はDate.UTC(年, 月-1, 日)で作ることです。DBから取り出した日付型をそのまま渡しているなら、ドライバがどのタイムゾーンでDateを組み立てているかを先に確かめます。時刻まで出す帳票では、UTCに直した時刻を書くのか、日本時間の数値を書くのかを仕様として決めておきます。

Expressからダウンロードさせるときのヘッダーとwrite(res)

Webアプリから帳票を返すなら、ファイルに保存せずHTTPのレスポンスへ直接書き込めます。workbook.xlsx.write(stream)は書き込み先に任意のWritableストリームを取ります。

app.get('/reports/invoices.xlsx', async (req, res, next) => {
  try {
    const workbook = await buildInvoiceWorkbook(); // 上の生成処理を関数にしたもの
    const name = encodeURIComponent('請求一覧_202610.xlsx');
    res.setHeader('Content-Type',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    res.setHeader('Content-Disposition',
      `attachment; filename="invoices.xlsx"; filename*=UTF-8''${name}`);
    await workbook.xlsx.write(res);
    res.end();
  } catch (e) {
    next(e);
  }
});

日本語のファイル名はfilename*にUTF-8でエンコードして渡し、古いクライアント向けに英数字のfilenameも並べます。小さな帳票ならwriteBuffer()でBufferにしてからres.sendしてもかまいません。Express 5での書き方の違いはExpress.jsの入門と5系への移行点にまとめています。

既存のExcelを読み込んで値と書式を取り出す実装、テンプレート差し込みの注意

取込処理や、ひな形のxlsxに値を差し込んで出す帳票では、読み込み側の挙動を知っておく必要があります。READMEのFile I/Oの項にある3つの読み方と、値の型を確認します。

readFile・load(buffer)の使い分けとgetCellが返す値の型

ディスク上のファイルはworkbook.xlsx.readFile(path)、アップロードされたBufferはworkbook.xlsx.load(buffer)、ストリームはworkbook.xlsx.read(stream)で読みます。ignoreNodesにdataValidationsなどを指定すると、使わない要素の解析を飛ばせます。

const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer, { ignoreNodes: ['dataValidations', 'conditionalFormatting'] });
const ws = workbook.getWorksheet('請求一覧');

ws.eachRow({ includeEmpty: false }, (row, rowNumber) => {
  if (rowNumber <= 2) return;              // タイトルと見出しを飛ばす
  const amount = row.getCell(4).value;      // 数値、または { formula, result }
  const value = typeof amount === 'object' && amount !== null ? amount.result : amount;
  console.log(row.getCell(1).text, value);
});

cell.valueはセルの種類で形が変わります。数式セルは{ formula, result }、リッチテキストは{ richText: [...] }、ハイパーリンクは{ text, hyperlink }、日付セルはDateです。画面に見えている文字列だけが欲しいならcell.textを使うと分岐が減ります。

セルの色を変えたら他のセルも変わる、スタイル共有の落とし穴と対処

READMEのStylesの項には、同じスタイルのオブジェクトを複数のセルに代入すると共有され、後から書き換えると全部に反映される、と書かれています。さらに、ファイルから読み込んだときも、似たスタイルのセルは同じオブジェクトを参照します。

取込エラーのセルだけ赤くして返す処理でcell.fill.fgColor = ...のように中身を書き換えると、同じ書式だった別のセルまで赤くなります。Qiitaでも2025年12月にこの現象の報告記事が出ています。

const cell = ws.getCell('B5');
// NG: 共有されているオブジェクトの中身を書き換える
// cell.style.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFF0000' } };

// OK: 複製した新しいオブジェクトを代入して参照を切る
cell.style = {
  ...cell.style,
  fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC7CE' } },
};
cell.font = { ...cell.font, color: { argb: 'FF9C0006' } };

入れ子のオブジェクトまで独立させたいときはstructuredClone(cell.style)で深く複製してから変更します。行や列にスタイルを付けた場合も、既存のセルへ同じ規則で反映される点に注意してください。

数式は計算されない、結果の渡し方とfullCalcOnLoadの使いどころ

READMEのFormula Valueの項には、ExcelJSは数式を処理して結果を出せないので結果を渡す必要がある、と明記されています。関数名は英語、引数の区切りはカンマで書きます。

結果を渡さないと、Excelで開いた直後は正しく見えても、プレビューやPDF変換のツールでは空欄や0になることがあります。生成コードで合計を自分で計算してresultに入れておけば、この差は出ません。

テンプレートに複雑な数式が残っていて結果を計算しきれない場合は、workbook.calcProperties.fullCalcOnLoad = trueで、Excelが開いたときに全体を再計算させます。ただし、Excelを通さずにサーバー側で数値を読む後続処理があるなら、この設定では解決しません。

10万行を超える出力はストリーミングのWriterとReaderでメモリを抑える

通常のWorkbookは、全シート・全行・全セルをメモリに組み立ててから書き出します。READMEのStreaming I/Oの項は、この方式では必要なメモリが文書の大きさを制限するとの説明です。明細が数万〜数十万行になる出力は、最初からストリーミングで組みます。

WorkbookWriterの書式・共有文字列設定とcommitの確定順序

ストリーミングの書き込みはExcelJS.stream.xlsx.WorkbookWriterを使います。オプションのuseStylesとuseSharedStringsはどちらも既定がfalseで、書式を付けたいならuseStylesをtrueにします。

const ExcelJS = require('exceljs');

async function exportOrderLines(rows, filename) {
  const wb = new ExcelJS.stream.xlsx.WorkbookWriter({
    filename,               // stream: res を渡せば HTTP に直接流せる
    useStyles: true,        // 表示形式を使うので有効にする
    useSharedStrings: false,
  });
  const ws = wb.addWorksheet('受注明細');
  ws.columns = [
    { header: '受注番号', key: 'orderId', width: 14 },
    { header: '商品コード', key: 'sku', width: 16 },
    { header: '数量', key: 'qty', width: 8 },
    { header: '金額', key: 'amount', width: 12, style: { numFmt: '#,##0' } },
  ];

  for await (const r of rows) {   // DB のカーソルなど非同期イテラブルを想定
    ws.addRow(r).commit();        // 行ごとに確定してメモリから外す
  }
  ws.commit();
  await wb.commit();
}

行はcommit()するまでメモリに残ります。READMEによれば、確定を遅らせるのは行をまたいだセル結合を可能にするためです。結合が要る行は、結合範囲の最後の行を書いてから確定します。

useStylesは書式の情報を足すぶん処理が重くなる、とREADMEにあります。表示形式が不要な中間ファイルなら、falseのまま出す方法も選択肢の一つです。数百万行のデータ連携なら、ExcelではなくCSVの一括取り込みの設計に寄せたほうが運用は楽になります。

WorkbookReaderの非同期読込と書式・共有文字列の設定方法

大きなファイルの読み込みはExcelJS.stream.xlsx.WorkbookReaderです。READMEはfor await による反復を勧めており、sharedStrings・hyperlinks・stylesはそれぞれ'cache'が既定です。

const reader = new ExcelJS.stream.xlsx.WorkbookReader('./upload.xlsx', {
  sharedStrings: 'cache',  // 文字列セルに実際の文字列を入れる
  styles: 'ignore',        // 書式が不要なら読まない
  hyperlinks: 'ignore',
  worksheets: 'emit',
});

for await (const sheet of reader) {
  for await (const row of sheet) {
    if (row.number === 1) continue;          // 見出し行
    const [, orderId, sku, qty, amount] = row.values; // values は1始まり
    await saveLine({ orderId, sku, qty, amount });
  }
}

styles: 'ignore'にすると、日付の表示形式も読まれないため、日付セルが数値のシリアル値で返ることがあります。日付列を含む取込では、列ごとに型を決めて変換するか、stylesを'cache'のままにする方法が選択肢です。ストリームの流量の考え方はNode.jsのStreamのドキュメントと、Web標準側のStreams APIの使い方が参考になります。

ストリーミングで使えない操作:シート削除・unMergeCells・確定済みの行

READMEは、ストリーミングと通常版の違いとして次の3点を挙げています。

  • 追加したシートは削除できない
  • 確定した行はシートから外れ、参照できない
  • unMergeCells()は使えない

このため、全行を書いたあとに先頭行へ件数や合計を書き戻す設計は成り立ちません。件数や合計が要るなら、先に集計クエリで求めて見出しより上に書くか、最終行に置きます。ブラウザ向けのビルドにはストリーミングのReaderとWriterが含まれない点も、READMEのBrowserの項に書かれています。

保守停止のExcelJSを業務システムで採用する条件と見送る場面の判断

機能面だけで比べれば、Node.jsで書式付きのxlsxを出す手段としてExcelJSに代わるものは多くありません。判断を分けるのは、更新の止まった依存をどこまで自社で抱えられるかです。

採用する条件:自社データの帳票出力に限り、依存を自社で監視できる

採用してよいのは、出力するデータが自社のDBにあり、利用者のファイルを解析しない用途です。請求一覧・月次集計・在庫表の出力がこれにあたります。READMEのAPIは2023年以降変わっておらず、既存のコードが新しい版で壊れる心配もありません。

そのうえで、次の運用を入れておきます。package-lock.jsonでexceljsと依存の版を固定する、npm auditをCIで毎回回す、依存のjszip・archiver・unzipperに脆弱性が出たらoverridesで差し替える、の3つです。帳票の出力を含む業務システムを新規に作る、または古い帳票処理を作り直す場合は、業務用・Webアプリ開発としてご相談いただけます。帳票の出力・配信・保存をどの順で電子化するかは帳票電子化の進め方で整理しています。

見送る場面:外部のxlsxを無制限に受け付ける取込と数式の再計算

見送るべきなのは、会員や取引先がアップロードするxlsxを、サイズの上限なしでサーバー上のExcelJSに読ませる設計です。xlsxの実体はZIPで、展開後のサイズは見た目のファイルサイズと一致しません。

GitHub Advisory Databaseでexceljsに登録されている脆弱性は、2018年のGHSA-2j2j-8rrv-264g(1.6.0未満のXSS)の1件だけです。ところが、2026年8月に公開された非公式フォークのexceljs-hardenedは、READMEで「上流のプライベートな脆弱性報告が無効で、プロトタイプ汚染・任意のローカルファイル読み取り・展開サイズの上限なしといった問題が未修正のまま残っている」と主張しています。フォークの主張は上流が確認したものではありませんが、報告を受け取る窓口が無いという指摘は、2024年1月以降の更新停止と整合します。

外部入力を読むなら、アップロードの上限サイズを決め、解析を別プロセスやWorker、コンテナに分けて時間とメモリを制限してください。それができない、あるいはサーバー上で数式を再計算した結果まで使いたいなら、ExcelJSは選びません。前者はCSVでの受け付けに切り替え、後者は計算をアプリ側のコードに移します。

SheetJS・Excelize・ClosedXMLとの比較:言語と書式の要否で選ぶ

Excelを扱うライブラリは、使う言語でほぼ決まります。Node.jsの中で選ぶならExcelJSとSheetJSが候補で、他言語に移せるなら選択肢が広がります。

ライブラリ 言語 版と配布(2026年11月時点) 向く用途
ExcelJS JavaScript 4.4.0(2023年10月)・npm 書式付きの帳票出力
SheetJS JavaScript npmは0.18.5で停止・CDNで0.20.3 多形式の読み込みと変換
Excelize Go 2.11.0系 大量行の高速な出力
ClosedXML C# 0.105系 .NETの業務システム

SheetJSは、公式ドキュメントのNode.jsのインストール手順のとおり、npmのxlsxが0.18.5で止まっており、最新版は自社CDNのtgzから入れます。Goで書けるならExcelizeの使い方と10万行の実測、.NETならClosedXMLの使い方とできないことが比較の材料になります。

よくある質問

ExcelJSを使い始める前に確認されることの多い質問をまとめました。

ExcelJSは無料で商用利用できますか?

できます。ライセンスはMITで、商用のシステムに組み込んで配布しても費用はかかりません。条件は、著作権表示とライセンス文をソフトウェアの写しに含めることです。社内のサーバーで動かすだけなら配布にあたらない場合が多いものの、パッケージにしてお客様に納品するときは、利用しているOSSの一覧と一緒にライセンス文を添えておくと後の確認が楽になります。

ExcelJSで古いxls形式のファイルは読めますか?

読めません。ExcelJSが扱うのはxlsxとCSVで、Excel 97〜2003形式のxlsは対象外です。xlsが混ざる取込では、利用者にxlsxで保存し直してもらうか、xlsを読める別のライブラリやLibreOfficeのヘッドレス変換でxlsxへ変換してからExcelJSに渡します。変換後は日付と数式の結果を数件の実ファイルで突き合わせてください。

ExcelJSのセル結合はどう書きますか?

ws.mergeCells('A1:D1')のように範囲を文字列で渡すか、ws.mergeCells(10, 11, 12, 13)のように開始行・開始列・終了行・終了列の数値で渡します。結合したセルは左上のセルが値とスタイルの持ち主になり、他のセルを参照しても返るのは左上と同じ値です。READMEには、行の挿入や削除(splice)が結合セルにかかると結合範囲が正しく移動しない、という既知の問題も書かれているため、結合を含む帳票では行の挿入を避けて上から順に書きます。

ExcelJSはブラウザだけで使えますか?

使えます。distフォルダにあるexceljs.jsとexceljs.min.jsがブラウザ向けのビルドで、通常のWorkbookによる読み書きはブラウザでも動きます。ただし、ストリーミングのReaderとWriterはブラウザ向けのビルドに含まれません。数万行を超える出力は端末のメモリに左右されるため、件数が多い帳票はサーバー側で生成してダウンロードさせる構成のほうが安定します。

ExcelJSの代わりに非公式フォークへ移るべきですか?

すぐに移る必要はありません。exceljs-hardenedは4.4.0から分岐して同じAPIのまま公開されており、2026年10月16日に5.1.0が出ていますが、元のメンテナとは無関係の個人による配布です。外部のxlsxを読む用途で、上流の修正を待てない事情があるなら、移行先として検討する候補です。その場合は、差分のコミットを自社で読み、フォーク自体の保守が止まったときに戻せるよう、importを1か所にまとめておきます。自社データの出力だけなら、公式版のまま依存を監視する運用で足ります。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策
  3. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  4. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  5. 2026.01.30 コラム 税務調査は個人と法人で何が違う?担当部署・件数・調べられる点と追徴税額を比較【令和6事務年度】

RELATED POSTS 関連記事

目次