0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript と React での Excel ファイルのダウンロード/エクスポート実装方法

0
Posted at

日常の開発において、ページ上のテーブルやレポートデータを Excel ファイルとしてエクスポートする機能は、非常に頻繁に求められる要件です。社内管理システム、データ分析プラットフォーム、エンドユーザー向けの SaaS アプリケーションを問わず、この機能はほぼ必ず必要になります。

従来の実装アプローチでは、一般的にバックエンドサービスが Apache POI や EPPlus などのライブラリを利用してファイルを生成し、レスポンスストリームを通じてフロントエンドにダウンロードさせる方式が採られてきました。この方法は成熟しており安定していますが、いくつかの固有のコストが存在します。エクスポートのたびにネットワークリクエストが発生し、バックエンドは追加の同時処理負荷やメモリオーバーヘッドに対応する必要があり、またデータがサーバーを経由することで機密情報の露出面が増える可能性もあります。

近年、WebAssembly 技術がフロントエンド領域で実用化されるに伴い、ブラウザ側で直接 Excel ファイルを処理できるソリューションが徐々に登場しています。これらのソリューションは、従来サーバーサイドに依存していた処理をクライアントサイドに移行することで、上記の課題に対する別のアプローチを提供します。

本記事では Spire.XLS for JavaScript を例に、バックエンドサービスに依存せず、ネイティブ JavaScript および React プロジェクトにおいて Excel ファイルの作成、データ入力、エクスポートダウンロードを完了する方法を紹介します。本記事で示す手法とコードはあくまで技術検討の参考として提供するものであり、特定製品の推奨や宣伝を意図するものではありません。

利用前提と初期化

Spire.XLS for JavaScript は WebAssembly ベースの Excel 操作用ライブラリであり、XLS、XLSX、XLSB、ODS などの一般的な形式をサポートし、セルスタイル、数式、グラフ、ピボットテーブルなどの機能を提供します。その動作ロジックは多くの WASM ライブラリと同様で、ランタイム環境を読み込んだ後、JavaScript 経由で公開された API を呼び出します。

インストール

npm を使用して依存パッケージをインストールします:

npm i spire.xls

インストール完了後、node_modules 内に spire.office ディレクトリが作成され、WebAssembly ランタイムファイルおよびコアライブラリスクリプトが含まれます。

WASM モジュールの初期化

このライブラリは WebAssembly に基づいているため、Excel API を呼び出す前にモジュールの初期化を完了する必要があります。初期化プロセスでは必要なリソースをロードし、ランタイム環境をセットアップします。この手順は通常、アプリケーションの起動時に実行します:

// 共通モジュールをインポートして初期化
import('/node_modules/spire.office/spire.common.js').then(async (commonModule) => {
    // WASM ランタイムを初期化
    await commonModule.initializeWasm();
    
    // XLS モジュールをロード
    await import('/node_modules/spire.office/spire.xls.js');
    
    console.log('Spire.XLS 準備完了');
});

初期化完了後、ライブラリの API はグローバルオブジェクト window.spirexls または window.xlswasm にマウントされます。以降のすべての Excel 操作はこのグローバルオブジェクトを介して呼び出します。

バージョンによって API のマウント位置が異なる場合があるため、実際に使用する前に console.log(window.spirexls || window.xlswasm) で確認することを推奨します。

ネイティブ JavaScript 環境でのエクスポート実装

以下は、ワークブックの作成、データ入力、仮想ファイルシステムへの保存、そしてブラウザダウンロードのトリガーまでを含む、完全なブラウザサイドエクスポートフローです。

// WASM モジュールが初期化されていることを確認
if (!window.spirexls && !window.xlswasm) {
    console.error("Spire.XLS が初期化されていません。");
    return;
}

// 初期化済みの WebAssembly モジュールを取得
const wasmModule = window.spirexls || window.xlswasm;

// 新しいワークブックを作成
const workbook = new wasmModule.Workbook();
const worksheet = workbook.Worksheets.get(0);

// サンプルデータを作成
const products = [
    ["製品", "数量", "価格"],
    ["ノートパソコン", 10, 999.99],
    ["マウス", 50, 24.99]
];

// データを作業シートに挿入
for (let i = 0; i < products.length; i++) {
    for (let j = 0; j < products[i].length; j++) {
        const cell = worksheet.Range.get({ row: i + 1, column: j + 1 });
        if (typeof products[i][j] === "string") {
            cell.Text = products[i][j];
        } else {
            cell.NumberValue = products[i][j];
        }
    }
}

// 合計列(4列目)を追加
worksheet.Range.get({ row: 1, column: products[0].length + 1 }).Text = "合計";
worksheet.Range.get({ row: 2, column: products[0].length + 1 }).Formula = "=B2*C2";
worksheet.Range.get({ row: 3, column: products[0].length + 1 }).Formula = "=B3*C3";

// ワークブックを仮想ファイルシステム(VFS)に保存
const outputFileName = "Report.xlsx";
workbook.SaveToFile({
    fileName: outputFileName,
    version: wasmModule.ExcelVersion.Version2016
});

// ワークブックリソースを解放
workbook.Dispose();

// VFS から生成されたファイルを読み取り
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);

// Blob オブジェクトを作成
const excelBlob = new Blob(
    [fileArray],
    {
        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    }
);

// ブラウザダウンロードをトリガー
const url = URL.createObjectURL(excelBlob);
const a = document.createElement("a");
a.href = url;
a.download = outputFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);

上記コードの重要なポイント

  • 仮想ファイルシステム(VFS):このライブラリの SaveToFile() はファイルをオペレーティングシステムのディスクに書き込むのではなく、WebAssembly インスタンスのメモリ内ファイルシステムに保存します。エクスポート時には window.dotnetRuntime.Module.FS.readFile() で元のバイナリデータを読み出し、ブラウザが認識可能な Blob オブジェクトに変換する必要があります。
  • リソース管理:各 Workbook インスタンスは保存完了後、明示的に Dispose() メソッドを呼び出してメモリを解放することを推奨します。頻繁にエクスポートを行うシナリオでは特に重要であり、これを怠るとメモリ使用量が徐々に増加する可能性があります。
  • 行・列インデックス:Excel の行番号と列番号は 1 から始まるのに対し、配列のインデックスは 0 から始まるため、代入時にオフセットに注意が必要です。
  • グローバルオブジェクトの依存関係window.dotnetRuntime は WASM ランタイムによって自動的に注入されるため、追加の宣言は不要です。

React プロジェクトでのエクスポートコンポーネントのカプセル化

React フレームワークでこのライブラリを使用する場合、エクスポートロジックを独立した Hook またはコンポーネントとして分離し、異なるページで再利用できるようにすることを推奨します。同時に、useState を利用してローディング状態を管理し、ユーザーの連続クリックを防止します。

コンポーネント例

import { useState } from 'react';

const ExcelExportButton = ({ data, fileName = 'Export.xlsx' }) => {
  const [isExporting, setIsExporting] = useState(false);

  const handleExport = async () => {
    // WASM モジュールが初期化されていることを確認
    const wasmModule = window.spirexls || window.xlswasm;
    if (!wasmModule || isExporting) {
      console.error("Spire.XLS が初期化されていないか、エクスポート中です");
      return;
    }

    setIsExporting(true);

    try {
      const workbook = new wasmModule.Workbook();
      const worksheet = workbook.Worksheets.get(0);

      // データオブジェクトから動的にヘッダーを生成
      const headers = Object.keys(data[0] || {});
      // ヘッダーを書き込み(1行目)
      headers.forEach((key, index) => {
        worksheet.Range.get({ row: 1, column: index + 1 }).Text = key;
      });

      // データ行を書き込み(2行目から)
      data.forEach((item, rowIndex) => {
        headers.forEach((key, colIndex) => {
          const value = item[key];
          const cell = worksheet.Range.get({
            row: rowIndex + 2,
            column: colIndex + 1
          });
          if (typeof value === 'string') {
            cell.Text = value;
          } else if (typeof value === 'number') {
            cell.NumberValue = value;
          } else if (value instanceof Date) {
            cell.Text = value.toLocaleDateString();
          }
          // その他の型(boolean など)は必要に応じて処理を拡張
        });
      });

      const exportFileName = fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx`;
      workbook.SaveToFile({
        fileName: exportFileName,
        version: wasmModule.ExcelVersion.Version2016
      });

      workbook.Dispose();

      const fileArray = window.dotnetRuntime.Module.FS.readFile(exportFileName);
      const blob = new Blob([fileArray], {
        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
      });

      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = exportFileName;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);

    } catch (error) {
      console.error('エクスポート失敗:', error);
      // ここにトーストメッセージなどのユーザー通知ロジックを追加可能
    } finally {
      setIsExporting(false);
    }
  };

  return (
    <button onClick={handleExport} disabled={isExporting}>
      {isExporting ? '生成中...' : 'Excel エクスポート'}
    </button>
  );
};

export default ExcelExportButton;

使用例

const App = () => {
  const tableData = [
    { 氏名: '張三', 部署: '技術部', 工数: 40 },
    { 氏名: '李四', 部署: '製品部', 工数: 38 }
  ];

  return (
    <div>
      <ExcelExportButton data={tableData} fileName="月次工数表" />
    </div>
  );
};

代替案:既存の HTML テーブルからのエクスポート

ページ上に既にレンダリング済みの HTML テーブルが存在する場合、データ構造を再構築することなく、直接 Excel ファイルに変換することが可能です。公式サンプルでは、エクスポートしたテーブルに組み込みスタイル(見出し行、交互の背景色など)を適用し、列幅を自動調整する方法も示されています。

async function exportTableToExcel() {
    if (!window.spirexls && !window.xlswasm) {
        alert("Spire.XLS モジュールが読み込まれていません。");
        return;
    }

    const button = document.getElementById("exportBtn");
    button.disabled = true;
    button.innerText = "エクスポート中...";

    const wasmModule = window.spirexls || window.xlswasm;

    try {
        // HTML テーブルを取得
        const tableHtml = document.getElementById("salesTable").outerHTML;

        // インラインスタイルを削除(スタイル干渉を防止)
        const safeTableHtml = tableHtml.replace(/style="[^"]*"/g, '');

        const htmlContent = `
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
            </head>
            <body>
                ${safeTableHtml}
            </body>
            </html>
        `;

        const htmlFileName = "Table.html";

        // HTML コンテンツを VFS に書き込み
        window.dotnetRuntime.Module.FS.writeFile(htmlFileName, htmlContent);

        // HTML ファイルを読み込んで Excel ワークブックに変換
        const workbook = new wasmModule.Workbook();
        workbook.LoadFromHtml(htmlFileName);

        const sheet = workbook.Worksheets.get(0);

        // テーブルの実際の行・列範囲を取得
        const lastRow = Number(sheet.LastRow);
        const lastCol = Number(sheet.LastColumn);

        // 見出し行に組み込みタイトルスタイルを適用
        const headerRow = sheet.Range.get_Item(1, 1, 1, lastCol);
        headerRow.BuiltInStyle = wasmModule.BuiltInStyles.Heading3;

        // データ行に交互の背景色を適用
        for (let i = 2; i <= lastRow; i++) {
            const row = sheet.Range.get_Item(i, 1, i, lastCol);
            row.BuiltInStyle = i % 2 === 0
                ? wasmModule.BuiltInStyles.Accent3_20
                : wasmModule.BuiltInStyles.Accent3_60;
        }

        // すべての列幅を内容に合わせて自動調整
        for (let j = 1; j <= lastCol; j++) {
            sheet.AutoFitColumn(j);
        }

        const outputFileName = "SalesReport.xlsx";

        workbook.SaveToFile({
            fileName: outputFileName,
            version: wasmModule.ExcelVersion.Version2016
        });

        workbook.Dispose();

        const fileData = window.dotnetRuntime.Module.FS.readFile(outputFileName);
        const blob = new Blob([fileData], {
            type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        });

        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = outputFileName;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

    } catch (error) {
        alert("エクスポート失敗:" + error.message);
    } finally {
        button.disabled = false;
        button.innerText = "Excel エクスポート";
    }
}

この方式の重要なポイント:

  • FS.writeFile() で HTML コンテンツを仮想ファイルシステムに書き込み、LoadFromHtml() で読み込む。
  • テーブルのインラインスタイルを削除することで、変換時に一部のスタイル属性が予期しない影響を与えるのを防ぐ。
  • BuiltInStyle プロパティは、ライブラリに組み込まれたプリセットスタイルセットに対応し、エクスポートテーブルを迅速に美化するために使用できる。
  • AutoFitColumn() メソッドは列幅を自動調整し、生成ファイルのユーザビリティを向上させる。

この方法は、テーブル構造が比較的整っており、スタイルに対する要求が高くないシーンに適しています。セルの結合やカスタムフォントカラーなどの複雑なスタイルについては、変換結果がライブラリの実装詳細によって異なる可能性があるため、実際の使用前に十分なテストを行うことを推奨します。

よくある質問と注意事項

日本語・中国語フォントの表示異常

一部の環境では、エクスポートした Excel ファイルを開いた際に日本語や中国語が四角や文字化けとして表示されることがあります。これは通常、仮想ファイルシステムに該当するフォントファイルが不足していることが原因です。解決策としては、初期化後にフォントファイル(例:simsun.ttcarial.ttf)を FetchFileToVFS メソッドを使用して /Library/Fonts/ ディレクトリに読み込む必要があります。詳細なコードは公式サンプルのフォント読み込み部分を参照してください。

モジュール読み込みと初期化のタイミング

WebAssembly ファイルはサイズが比較的大きく、初回読み込みに一定の時間を要します。ユーザーがエクスポートボタンをクリックしてから読み込みを開始するのではなく、アプリケーション起動フェーズで非同期に初期化を実行することを推奨します。これによりインタラクションの体験が向上します。

メモリ管理と大容量データ

ブラウザサイドでの Excel 処理は、JavaScript ヒープメモリおよび WASM リニアメモリの制約を受けます。10MB を超えるファイルや数万行以上のデータについては、クライアントサイド方式ではパフォーマンスのボトルネックが発生する可能性があります。そのような場合は引き続きサーバーサイド生成方式の採用が推奨されます。

ブラウザ互換性

WebAssembly はすべてのモダンデスクトップブラウザ(Chrome 96+、Firefox 90+、Safari 15+、Edge 96+)でネイティブサポートされています。Internet Explorer および一部の旧版モバイルブラウザはこの技術をサポートしていない点に注意が必要です。

まとめ

Spire.XLS for JavaScript は WebAssembly ベースのブラウザサイド Excel 操作機能を提供し、開発者がバックエンドサービスに依存せずにファイルの作成、編集、エクスポートを完了できるようにします。その中核となるワークフローは以下のとおりです:

WASM モジュールの初期化 → Workbook オブジェクトの作成/操作 → 仮想ファイルシステムへの保存 → バイナリデータの読み取り → Blob の構築 → ダウンロードのトリガー

React プロジェクトでは、エクスポートロジックをコンポーネントやカスタム Hook としてカプセル化し、状態管理と組み合わせることでユーザー体験を向上させることができます。また、このライブラリは HTML テーブルからの直接変換もサポートしており、組み込みスタイルの適用や列幅の自動調整などの実用的な機能を提供するため、既存ページへのエクスポート機能の追加を容易にします。

すべてのクライアントサイド方式と同様に、この方法にはデータ量、フォント処理、互換性の面で適用範囲の境界があります。実際の技術選定にあたっては、プロジェクトのデータ規模、対象ブラウザ環境、チームの保守コストを総合的に考慮することをお勧めします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?