All Articles

HTML5のドラッグドロップAPIで非同期ファイルアップロードするライブラリ書いたよ

あんまりHTML5関連の事をやってなかったのですが、とあるきっかけで作ってみることになり、 作ったら作ったで色々と困ったこともあり、せっかくだからライブラリにしちゃえということで、 えいやとやってみました。

私が思っていた以上にブラウザ間の差異が大きかったので、その辺りも備忘録としてまとめておきます。

あ、ライブラリはgithubにおいてます。

ysugimoto / DDUploader

どんなライブラリ?

いわゆる、HTML5のDrag and Drop APIとFIle APIを使ってJavaScriptからファイルアップロードしたい人向けのライブラリです。ブラウザサポートは、
  • Safari (新しめ)
  • Chrome (新しめ)
  • Firefox (3.6以上)
です。SafariとChormeは細かいバージョンまで調査できませんでした>< より具体的には、Drag and Drop APIとFormData、またはFileReader APIがサポートされているブラウザで動作します。 OperaはDrag and Drop APIが実装されておらず、IEは10でも実装されていないようで動きませんでした。 今後のバージョンアップで動くようになるかもしれませんね。

Firefox3.6はFormData APIが実装されていませんが、代わりにXMLHttpRequest.prototype.sendAsBinary()メソッドがMozilla拡張で実装されているので、自前でヘッダを作って動くようにしてます(詳しくはコードを見てください)

サポートしてる中での実装差異とか

今回の実装でのキモはDrag and Drop、File API、FormDataが実装されているかどうかで決まってます。 おおまかなまとめは以下の通り。
Safari5.1Chrome12.0Firefox3.6-10.0Opera11.50IE 9 or 10
Drag and Drop○※2○※2
FileAPI▲※3
FormData○(4以降)

※1 ドロップ対象に-khtml-user-drag : element の指定が必要 ※2 ドロップ要素にdraggable=“on”の属性指定が必要 ※3 FileReaderは実装されていない

という感じでした。Operaはもうちょっとで動くようになるかもしれませんね。IEはまだダメ。

困った所

event.dataTransfer.files[i]で取れるFileObject

ドロップされたファイル名は、WebkitではFileObject.fileNameで取れるのに対して、FirefoxはFileObject.nameで取れます。細かい差異がありますね。あと、WebkitではFileObject.WebkitRelativePathというプロパティが、FirefoxではmozFullPathとmozSlice()というメソッドがあります。relativeとfullて。

さらに、FirefoxはFileObject.getAsBinary()という拡張により直接バイナリデータが取れますが、Webkitにはないのでがっかり。(初めこれで実装しようとしてつまづきました)

どちらでもバイナリデータを扱う場合は、FileReaderオブジェクトに渡すのがいいと思います。 FileReader.readAsBinarytString()でバイナリが読めます。Safariにはありませんが・・・。

で、Safariではどうするかというと、FormDataオブジェクトにそのままFileObjectを渡してしまいます。あとは送信時によしなにやってくれるみたいですね。バイナリを直接扱うよりも簡単、というかFormDataが超簡単。

ディレクトリのドロップが検出しづらい

ドラッグドロップでディレクトリをドロップすると、そのデータがファイルかディレクトリかどうかの判定がとてもしづらかったです、というか厳密にはできないかもしれないです。

ディレクトリをドロップすると、sizeが4096、typeが空のFileObjectになります。で、拡張子無しのファイルをドロップすると、sizeは可変ですがtypeが空のオブジェクトになる。なので厳密な判定は諦めて、「拡張子の無いファイルなんてアップロードできないぜ」とすることにしました。

余談ですが、typeプロパティにはファイルのmimeType文字列が入りますが、おそらくプラットフォーム依存じゃないかなーって思います。

実装とか、挙動とか

先の通り、FormDataオブジェクトがすごくラクなので、そのままXHR.send()の引数として渡せば自動的にmultipartヘッダにしてくれます。この時、「application/www-form-urlencoded」をsetRequestHeader()しない方がいいみたいです。あとはXHRのイベントをハンドリングして終了です。

FormDataオブジェクトが実装されていないFirefox3.6等は、FileReaderからバイナリデータを読み込んで、さらに自前でmultipartヘッダとかboundary stringとかヘッダを組み立てた後に、XHR.sendAsBinary(headers)で送信すれば、同じリクエストを送信することができました。長い。

使い方

以下のように、ファイルをロードし、DOM構築後にコンストラクタをnewでコールするだけです。
<script src="dduploader.js"></script>
<script>
document.addEventListener('DOMContentLoaded'undefined function() {
	var target = document.getElementById('test');

	new DDUploader(targetundefined {
			requestURI : 'sample.php'undefined
			withParams : { 'foo': 'bar' }undefined
			onDragStart : function() { console.log(this); }undefined
			onUploadSuccess : function(resp) { console.log(resp); }undefined
			onUploadError : function(resp) { console.log(resp); }
		});
}undefined false);
</script>

第一引数はドロップターゲットのHTML Element、第二引数は設定用のハッシュを入れます。 requestURIの設定値がないと例外になるのでご注意を。 その他、完了/失敗時のハンドラなども設定することで、ちょっと便利に使えると思います。

感想とか

「まだもうちょっと早いかなー」と勝手に思ってたんですが、やってみるとブラウザ際はあれど、大体のブラウザで動くようになっててすごいなーって思います。ブラウザ実装差異の吸収はJavaScriptでは結構当たり前にやってると思うので、それほど苦では無い印象でした。

また、新しいAPIなどは素の記述の方が意外と楽な感じでした。ライブラリ、いりませんね。

バグ報告とかあれば、教えていただけると嬉しいです。 あと、これらの機能はseezooに取り込まれてます。新しいブラウザを使ってるユーザさんは、ファイルマネージャへはドラッグドロップでOKですね!!