Table(HTML)の中身をExcelにエクスポートする

## やりたかったこと

業務系のWebサービスを作っていて、データを表に集計するページで「表の中身をExcel形式でダウンロードできるようにしたい」という要求がありました。「ライブラリ拾ってサクッとできるだろ」とググり始めたものの意外とうまくいかず苦戦しました。

## 最初のアプローチ

最初は、DOMを追ってxlsデータを作ってダウンロードするまでをフロントエンドで一括してできるライブラリがあるはずだ、と思って探しました。以下の２つを見つけましたが、SafariやChromeですら動かなくて採用できませんでした。

* [HTML table export](http://ngiriraj.com/pages/htmltable_export/demo.php#)
* [ExcellentExport.js](http://jordiburgos.com/post/2014/excellentexport-javascript-export-to-excel-csv.html)

そして次の記事を見つけました。

* [js-xlsx で js で excel のファイルを生成する](http://blog.64p.org/entry/2014/06/17/115212)

JSでxlsデータを作るjs-xlsxとBlobなどを保存することのできるFileSaver.jsを使った方法で、見たとき軽く感動しました。書いている方も有名なPerl Hackerのtokuhiromさんであるし、コードも簡単でわかりやすかったです。

さっそくこのやり方を実装して一安心しましたが、そこでFileSaver.jsがSafariでうまく動かないということを知りました。IEならともかくSafariでも動かないJSライブラリがあるとはちょっと意外でしたが、、ここでつまづきました。

## 解決策

最終的な解決策として、「js-xlsxで生成したBlobデータをサーバ側に一旦POSTして保存し、それをまたダウンロードするのが確実なやり方」という結論に至りました。

このやり方であれば、ただサーバに保存してあるxlsファイルをダウンロードするだけなのでクソブラウザとして名高いIE8でも難なく動きます。

より具体的には、大きく次の３ステップで実行します。

1. js-xlsxを使ってDOMからBLOBデータを作成する。
2. $.ajax(post)を使ってファイルを保存する
3. 成功したらダウンロードURLにリダイレクトする

## 実装

実装するにあたり、先のtokuhiromさんの記事に加え、こちらの記事を参考にさせていただきました: http://marcanguera.net/blog/2013/07/01/download-file-via-ajax/

```coffeescript
self.exportExcel = ->
  sheet_from_array_of_arrays = (data, opts) -> 
    # dataをcellに適用させるメソッド(省略）
  Workbook = ->
    # 省略
  s2ab = (s) ->
    # 省略
  # Excelのblobデータを作る
  data = []  
  $("#order-book-table tr").each (i, tr) -> # DOMを追ってdataを作る
    row = []
    $(tr).find("th,td").each (j, td) ->
      o = ""
      if $(td).find('div').length > 0 # div要素があればさらにその中身を加える
        $(td).find('div').each (k, div) ->
          o += div.innerHTML
      else                            # なければそのまま加える
        o += td.innerHTML
      row.push o
      return

    data.push row
    return

  key = XLSX.utils.encode_cell(c: 0, r: 0)
  ws = sheet_from_array_of_arrays(data)
  workbook = new Workbook()
  workbook.SheetNames.push "Order Book"
  workbook.Sheets["Order Book"] = ws
  wbout = XLSX.write(workbook, bookType: "xlsx", bookSST: true, type: "binary")
  blob = new Blob([s2ab(wbout)], type: "") # ここでxlsのblobデータが完成
  filename = "#{ファイル名}.xls"
  # formにPOSTする準備
  fd = new FormData()
  fd.append('data', blob)
  fd.append('filename', filename)
  $.ajax({
    type: "POST", url: "/api/controller_name/save_xls",
    processData: false,
    contentType: false
    data: fd
    success: (result) ->
      # 成功したら、ファイルのある場所に移動してダウンロード
      document.location.href = "/api/controller_name/download?filename=#{filename}"
  })
  return
```

フロントエンドは上のような実装です。

省略してあるところ（xlsデータ作成部分）は[tokuhiromさんの記事](http://blog.64p.org/entry/2014/06/17/115212)のjs-xlsxを使う部分そのままなのでそちらをご覧ください。
xlsを作成したあと、それをFormDataに入れ、ajaxでPOSTし、サーバ側に保存した上でそのURLに移動するという３ステップですね。

サーバ側はこんな感じ。

```rb:とあるコントローラ
  # POST save_xls
  def save_xls
    FileUtils.mkdir_p("public/xls/") unless FileTest.exist?("public/xls/")
    filename = params[:filename]
    sent_file = params[:data]
    file_path = File.join("public/xls/", filename)
    file = File.new(file_path, 'wb')
    file.write sent_file.read
    file.close
    render json: { basename: File.basename(file) }
  end

  # GET download
  def download
    file_path = File.join("public/xls/", params[:filename])
    file = File.open(file_path)
    send_file file.path
  end
```

上のアクション（save_xls）でファイルを保存して、その後downloadアクションを叩いてファイルを送ってもらう、という流れで２アクション用意しています。

## 感じたこと

* ブラウザ間の機能的差異の問題は思ったより根深い。適当に実装していると、ほぼ確実に引っかかって実装しなおす必要にせまられる。常にメソッドやライブラリがサポートするブラウザを確認する必要がある。
* というか、できればあまりフロントエンドのライブラリに頼りすぎない方がよい。jQueryとかなら安心だが、誰かがちょいっと開発したライブラリとかだとほぼ確実にIEでつまづく。
* 今回のようにサーバ側で解決できれば、ブラウザ間の差異を根本的に避けられる。