エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

フロントエンド(Node.js)でスタイル指定したExcelを出力する

Excelは好きですか?
僕は嫌いです。(UTF-8のCSVを開こうとしたら文字化けさせてきたり、0埋め数字を勝手に数値型に変えてくる辺りとか)
しかし、日本はExcel大国ですので基本的にどう足掻いてもExcelから逃れることはできません…。Excel出力機能の実装を強いられることもあるかと思います。

f:id:tansantktk:20201118205539p:plain

さて、つい先日Excelをフロントエンド側で出力させる要件があった為、 Node.js環境で使える便利なライブラリExcelJSを紹介したいと思います。

ExcelJSとは

Node.js環境で動かせるエクセル操作ライブラリです。 同様の機能を持ったライブラリは有名どころだとxlsx(https://github.com/SheetJS/sheetjs)がありますが、自分の場合は直感的に記述できてセルのスタイルを手軽に変更できるという部分に重点を置いていたのでxlsxは採用しませんでした。

セルのスタイルを手軽に指定できるjsライブラリとなると割と選択肢は狭まってくる印象です。 ExcelJSはGitHubのStarも6.6kとたくさん付いたライブラリですので、選択肢の1つとして申し分無いのではないかと思います。

かなり多機能ですので、より詳しい使い方はGitHubのREADME.mdを参照下さい。

github.com

当記事では、エクセル生成の方法に絞って紹介していきたいと思います。

使う前の準備

まずはnpmからインストールします。

npm install exceljs

xlsxを生成する

今回はReactで実装していきます。Node.jsが動くなら別のフレームワークでもOKです。

まずはセルのスタイル指定はせず、単純なエクセルデータ生成をする処理を書きます。

App.js

const ExcelJS = require('exceljs');

const clickButtonAsync = async (e) => {
  e.preventDefault();
  
  // Workbookの作成
  const workbook = new ExcelJS.Workbook();
  // Workbookに新しいWorksheetを追加
  workbook.addWorksheet('My Sheet');
  // ↑で追加したWorksheetを参照し変数に代入
  const worksheet = workbook.getWorksheet('My Sheet');

  // 列を定義
  worksheet.columns = [
    { header: 'ID', key: 'id' },
    { header: '氏名', key: 'name' },
    { header: '価格', key: 'price' },
  ];

  // 行を定義
  worksheet.addRow({id: 1001, name: 'ハンバーガー', price: 170});
  worksheet.addRow({id: 1002, name: 'チーズバーガー', price: 200});
  worksheet.addRow({id: 1003, name: '照り焼きチキンバーガー', price: 260});
  
  // UInt8Arrayを生成
  const uint8Array = await workbook.xlsx.writeBuffer();
  // Blobを生成
  const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
  // DL用URLを生成し、aタグからダウンロードを実行
  const url = window.URL.createObjectURL(blob);
  // aタグを生成
  const a = document.createElement('a');
  // aタグのURLを設定
  a.href = url;
  // aタグにdownload属性を付け、URLがダウンロード対象になるようにします
  a.download = `price_list.xlsx`;
  // aタグをクリックさせます
  a.click();
  // ダウンロード後は不要なのでaタグを除去
  a.remove();  
}

const App = () => {
  return (
    <div>
      <button onClick={(e) => clickButtonAsync(e)}>エクセル生成!</button>
    </div>
  );
}

export default App;

デバッグ実行すると「エクセル生成!」というボタンだけが画面に表示されますのでボタンを押します。 すると下記のようなエクセルファイルがダウンロードできます。 ただデータをエクセルとして出力するだけならこの処理だけで十分です。

f:id:tansantktk:20201117203730p:plain

worksheet.columns = [...]でまずは列を定義しておいて、実データをworksheet.addRow(...)で次々と追加していくだけです。

worksheet.addRow(...)の引数の連想配列のキーは、worksheet.columnsで定義している連想配列のkeyと一致している必要があります。

workbook.xlsx.writeBuffer()でバイナリを生成します。 戻り値がPromiseの非同期処理なので、awaitthen(x => {...})で戻り値を取得します。

ブラウザ上でDLさせる場合、window.URL.createObjectURL()で生成したファイルへのURLを生成し、プログラムからaタグをクリックしてDLをさせます。 createObjectURL()の引数にはFile, Blob, MediaSourceのいずれかのオブジェクトが指定可能ですので、 今回は、UInt8ArrayからBlobオブジェクトに変換した上で、BlobオブジェクトへのURLを生成しています。

jsでバイナリを扱う上でUInt8Array, Blobといった単語はよく出てくるのですが、どのオブジェクトが何に変換できるのか良く忘れがちなので ↓のサイトをいつも参考にさせていただいております。

var.blog.jp

バイナリ関係のオブジェクトの相関関係が分かりやすくまとめられているのでお勧めです。何度も助けられました。

スタイル指定してExcelを生成してみる

基本の記述方法が分かったところで、今度はスタイルを指定してきます。 ↓のようなスタイルのエクセルを作っていきます。

f:id:tansantktk:20201118202211p:plain

App.js

const ExcelJS = require('exceljs');

/** 枠線のスタイル */
const borderStyle = {
  top: { style: 'thin', color: { argb: 'FFCCCCCC' } },
  left: { style: 'thin', color: { argb: 'FFCCCCCC' } },
  bottom: { style: 'thin', color: { argb: 'FFCCCCCC' } },
  right: { style: 'thin', color: { argb: 'FFCCCCCC' } },
};
/** ヘッダ行の背景色 */
const headerFillStyle = {
  type: 'pattern',
  pattern: 'solid',
  fgColor: { argb: 'FFDDDDDD' },
};
/** ヘッダ行のフォント */
const headerFontStyle = {
  bold: true,
};
/** 偶数行の背景色 */
const bodyEvenFillStyle = {
  type: 'pattern',
  pattern: 'solid',
  fgColor: { argb: 'FFF5F5F5' },
};
/** 奇数行の背景色 */
const bodyOddFillStyle = {
  type: 'pattern',
  pattern: 'solid',
  fgColor: { argb: 'FFFFFFFF' },
};

const clickButtonAsync = async (e) => {
  e.preventDefault();
  
  // Workbookの作成
  const workbook = new ExcelJS.Workbook();
  // Workbookに新しいWorksheetを追加
  workbook.addWorksheet('My Sheet');
  // ↑で追加したWorksheetを参照し変数に代入
  const worksheet = workbook.getWorksheet('My Sheet');

  // 列を定義
  worksheet.columns = [
    { header: 'ID', key: 'id' },
    { header: '氏名', key: 'name' },
    { header: '価格', key: 'price' },
  ];

  // 行を定義
  worksheet.addRow({id: 1001, name: 'ハンバーガー', price: 170});
  worksheet.addRow({id: 1002, name: 'チーズバーガー', price: 200});
  worksheet.addRow({id: 1003, name: '照り焼きチキンバーガー', price: 260});

  // すべての行を走査
  worksheet.eachRow((row, rowNumber) => {
    // すべてのセルを走査
    row.eachCell((cell, colNumber) => {
      if (rowNumber === 1) {
        // ヘッダ行のスタイルを設定
        cell.fill = headerFillStyle;
        cell.font = headerFontStyle;
      } else {
        if (rowNumber % 2 === 0) {
          // ボディ行(偶数行)のスタイルを設定
          cell.fill = bodyEvenFillStyle;
        } else {
          // ボディ行(奇数行)のスタイルを設定
          cell.fill = bodyOddFillStyle;
        }
      }
      // セルの枠線を設定
      cell.border = borderStyle;
    });
    // 行の設定を適用
    row.commit();
  });

  // UInt8Arrayを生成
  const uint8Array = await workbook.xlsx.writeBuffer();
  // Blobを生成
  const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
  // DL用URLを生成し、aタグからダウンロードを実行
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `price_list.xlsx`;
  a.click();
  a.remove();  
}

const App = () => {
  return (
    <div>
      <button onClick={(e) => clickButtonAsync(e)}>エクセル生成!</button>
    </div>
  );
}

export default App;

worksheet.eachRow()はexceljsにおけるワークシート内の全て行を順番に走査するメソッドです。forEachみたいなものです。
row.eachCell()は全てのセルを順に走査するメソッドです。

cell.fillは主に背景色、cell.fontはフォント、cell.borderは枠線のスタイルを定義しているプロパティです。
ここにそれぞれ任意のスタイルを連想配列で代入してあげることで、好きなスタイルのエクセルを生成することができます。
よく使うスタイルの指定例を載せておきます。

枠線の指定

cell.border = {
  top: { style: 'thin', color: { argb: 'FFCCCCCC' } },
  left: { style: 'thin', color: { argb: 'FFCCCCCC' } },
  bottom: { style: 'thin', color: { argb: 'FFCCCCCC' } },
  right: { style: 'thin', color: { argb: 'FFCCCCCC' } },
}

背景色の指定

cell.fill = {
  type: 'pattern',
  pattern: 'solid',
  fgColor: { argb: 'FFDDDDDD' },
}

フォントの指定

cell.font = {
  bold: true, 
  name: 'Comic Sans MS'
}

↓もっと詳しいフォーマットは公式で詳しく書いてくれてます。 https://github.com/exceljs/exceljs#styles

列幅も変えてみる

列定義をする際にwidthをキーとして加えてあげるだけです。

  worksheet.columns = [
    { header: 'ID', key: 'id', width: 10 },
    { header: '氏名', key: 'name', width: 40 },
    { header: '価格', key: 'price', width: 30 },
  ];

CSVも出力できる

エクセルの時はworkbook.xlsx.writeBuffer()でバイナリを生成しましたが、 これをworkbook.csv.writeBuffer()に変えるだけでCSVも出せちゃいます。便利! (出力されるCSVの文字コードはUTF8です。ShiftJISが必要な場合は適宜変換が必要です。)

  // UInt8Arrayを生成
  const uint8Array = await workbook.csv.writeBuffer();
  // Blobを生成
  const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
  // DL用URLを生成し、aタグからダウンロードを実行
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `price_list.xlsx`;
  a.click();
  a.remove(); 

参考

URL.createObjectURL() - Web API | MDN

Blob, ArrayBuffer, Uint8Array, DataURI の変換

GitHub - SheetJS/sheetjs: SheetJS Community Edition -- Spreadsheet Data Toolkit