やりたかったこと
業務系のWebサービスを作っていて、データを表に集計するページで「表の中身をExcel形式でダウンロードできるようにしたい」という要求がありました。「ライブラリ拾ってサクッとできるだろ」とググり始めたものの意外とうまくいかず苦戦しました。
最初のアプローチ
最初は、DOMを追ってxlsデータを作ってダウンロードするまでをフロントエンドで一括してできるライブラリがあるはずだ、と思って探しました。以下の2つを見つけましたが、SafariやChromeですら動かなくて採用できませんでした。
そして次の記事を見つけました。
JSでxlsデータを作るjs-xlsxとBlobなどを保存することのできるFileSaver.jsを使った方法で、見たとき軽く感動しました。書いている方も有名なPerl Hackerのtokuhiromさんであるし、コードも簡単でわかりやすかったです。
さっそくこのやり方を実装して一安心しましたが、そこでFileSaver.jsがSafariでうまく動かないということを知りました。IEならともかくSafariでも動かないJSライブラリがあるとはちょっと意外でしたが、、ここでつまづきました。
解決策
最終的な解決策として、「js-xlsxで生成したBlobデータをサーバ側に一旦POSTして保存し、それをまたダウンロードするのが確実なやり方」という結論に至りました。
このやり方であれば、ただサーバに保存してあるxlsファイルをダウンロードするだけなのでクソブラウザとして名高いIE8でも難なく動きます。
より具体的には、大きく次の3ステップで実行します。
- js-xlsxを使ってDOMからBLOBデータを作成する。
- $.ajax(post)を使ってファイルを保存する
- 成功したらダウンロードURLにリダイレクトする
実装
実装するにあたり、先のtokuhiromさんの記事に加え、こちらの記事を参考にさせていただきました: http://marcanguera.net/blog/2013/07/01/download-file-via-ajax/
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さんの記事のjs-xlsxを使う部分そのままなのでそちらをご覧ください。
xlsを作成したあと、それをFormDataに入れ、ajaxでPOSTし、サーバ側に保存した上でそのURLに移動するという3ステップですね。
サーバ側はこんな感じ。
# 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アクションを叩いてファイルを送ってもらう、という流れで2アクション用意しています。
感じたこと
- ブラウザ間の機能的差異の問題は思ったより根深い。適当に実装していると、ほぼ確実に引っかかって実装しなおす必要にせまられる。常にメソッドやライブラリがサポートするブラウザを確認する必要がある。
- というか、できればあまりフロントエンドのライブラリに頼りすぎない方がよい。jQueryとかなら安心だが、誰かがちょいっと開発したライブラリとかだとほぼ確実にIEでつまづく。
- 今回のようにサーバ側で解決できれば、ブラウザ間の差異を根本的に避けられる。