JavaScript FileReader API でブラウザでファイルを扱う (TypeScript 対応)
ブラウザの FileReader API を使用したファイルの扱いについて TypeScript での書き方を含めて紹介します。
やりたいこと
ファイルアップロード時に UX 向上のために、ファイルをサーバ送信する前にプレビュー表示したり、ブラウザ側でファイルの内容を検証したりとファイルを取り扱いたいことが増えてきていると思います。
ブラウザ側 JavaScript でファイルを開くにはブラウザの FileReader
API (FileReader - Web API | MDN) を使用します。
今回はその FileReader API の使い方と TypeScript での書き方、使いやすくしてみた例やこちらの記事で紹介した Result 型を使った書き方を紹介します。
FileReader
の使い方
基本的な使い方
<input type="file">
で選択した画像ファイルをプレビュー表示する例を以下に示します。
まず new FileReader()
で FileReader
のインスタンスを生成しています。
次に FileReader
インスタンスに load
イベントのイベントハンドラを設定しています。
load
イベントは FileReader
が正常にファイルを読み取れたときに発生するイベントです。
FileReader: load イベント
load
イベントは、ファイルが正常に読み込めたときに発生します。
- FileReader: load イベント - Web API | MDN
他に正常に読み込めたかどうかによらず完了時に発生する loadend
イベントもあります。
FileReader: loadend イベント
loadend イベントは、ファイル読み込みが、成功したかどうかにかかわらず完了したときに発生します。
- FileReader: loadend イベント - Web API | MDN
load
イベントのコールバック関数が受け取るイベントターゲット(ここでは分割代入している target
)は FileReader
インスタンスで、target.result
すなわち FileReader.result
を img
要素の src
属性に設定しています。
FileReader.result
には読み込まれたファイルの内容が格納されています。
FileReader.result
FileReader
のresult
プロパティは、ファイルの内容を返します。このプロパティは、読み取り操作が完了した後でのみ有効で、データの形式は、読み取り操作を開始するために使用されたメソッドによって異なります。
- FileReader.result - Web API | MDN
最後に <input type="file">
が change
イベントを発生させた際に FileReader
インスタンスの readAsDataURL()
メソッドに選択されたファイルを渡しています。これでファイル読み込みが完了すると先ほどの load
イベントが発生するという動きです。
FileReader
のファイル読み込みメソッドは他に ArrayBuffer
として読み込む readAsArrayBuffer()
(FileReader.readAsArrayBuffer() - Web API | MDN)、バイナリ文字列として読み込む readAsBinaryString()
(FileReader.readAsBinaryString() - Web API | MDN) 、文字列として読み込む readAsText()
(FileReader.readAsBinaryText() - Web API | MDN) がありますが、今回は data URL として読み込む readAsDataURL()
メソッドを使用します。
FileReader.readAsDataURL()
readAsDataURL
メソッドは、指定されたBlob
またはFile
の内容を読み込むために使用されます。読み込み操作が終了すると、readyState
がDONE
となり、loadend
が発生します。このとき、result
属性には、ファイルのデータを表す、base64 エンコーディングされた data: URL の文字列が格納されます。
- FileReader.readAsDataURL() - Web API | MDN
その理由は img
要素の src
属性は URL であればよいため、ファイルのロケーションを示す URL ではない、URL がファイルのデータである data URL をそのまま指定して使えるからです。
例えばアップロードする CSV ファイルをプレビュー表示するといったユースケースでは readAsText()
を使います。
TypeScript で書いてみる
さて先ほどのコードをそのまま TypeScript にしてみます。
なお、FileReader API や DOM を型推論するには tsconfig.json
の compilerOptions.lib
に DOM
を含めるようにしてください。
load
イベントのイベントハンドラ内で target.result
に <string>
と型アサーションを入れていますが、これは FileReader
の読み込みメソッドの種類によって string
であったり ArrayBuffer
だったりするためです。また、読み込み完了前は null
になっています。実際 FileReader.result
の型は以下のようになっています。
今回は readAsDataURL()
で読み込みますので <string>
と型アサーションを入れています。
余談ですが、querySelector("img")
で取得した要素のイベントターゲットは HTMLImageElement
に型推論されているため、型アサーションがありませんが、querySelector("input[type=file]")
で取得した要素のイベントターゲットは HTMLInputElement
と明示的に型アサーションをしています。この理由は こちらの記事 で解説していますが、querySelector()
の引数の CSS セレクターが単純 HTML タグ名である場合はその要素の HTMLElement
継承インターフェースに型解決してくれるのですが、input[type=file]
のように属性セレクタが付いていたりすると Element
に型解決されてしまうからです。(まあ今回の場合、input
要素は一つしかないので属性セレクタ付けている意味はないのですが)
上記コードではイベントハンドラを addEventListener()
内に直接書いていましたが、別途定義した関数を渡す場合は、イベントターゲットに型推論が効かなくなるため、以下のようにイベントターゲットに <FileReader>
型アサーションを付けます。
読み込みの進捗状況を取得する
FileReader
は progress
イベントを発行し、ファイルの読み込み進捗を取得することができます。
progress
イベントによらず FileReader
が発行するイベントは ProgressEvent
です。
ProgressEvent
ProgressEvent
インターフェイスは、プロセスの進捗、例えば HTTP リクエスト(XMLHttpRequest
、または<img>
,<audio>
,<video>
,<style>
,<link>
のような基本的なリソースの読み込み)などを計測するイベントを表します。
- ProgressEvent - Web API | MDN
ProgressEvent
が持っている以下の属性を使用して進捗をパーセンテージで表したり、プログレスバーを表現したりすることが可能です。
ProgressEvent.loaded
読取専用
64 ビット符号なし整数値で、基礎となるプロセスで既に実行された作業の量を示す。ProgressEvent.total
読取専用
64 ビット符号なし整数で、基礎となるプロセスが実行中の作業の総量を表す。
- ProgressEvent - Web API | MDN
以下は進捗をパーセンテージでコンソールに出すだけの簡単な例です。
TypeScript でイベントハンドラー関数を別定義する場合は引数が ProgressEvent
型であることをアノテーションしましょう。
もっと簡単に書けないの?
ここまでで FileReader の基本的な使い方を紹介しましたが、こう思いませんでしたでしょうか。
「ファイル読み込むだけなのにインスタンス化して、イベントリスナー設定してから読み込みとか面倒臭い」
私はこの面倒臭さどこかで感じたことがあり、XMLHttpRequest
API と同じだと思い至りました。
昨今では XMLHttpRequest
API を使うことはほぼなく、fetch
API を使うことがほとんどだと思います。
その理由として XMLHttpRequest
と fetch
でもちろん機能的な違いもあるのですが、開発者体験としてはやはり fetch
が簡単に使えるという点は小さくはないのではないでしょうか。
非同期関数化してみる
というわけで Promise
でラップして XMLHttpRequest
のように使いづらい FileReader
を fetch
のように使いやすくしてみます。
FileReader
が error
イベントを発生させる際、エラー内容は FileReader.error
プロパティーに DOMException
型として格納されています。
FileReader.error
FileReader
のerror
プロパティは、ファイルの読み取り中に発生したエラーを返します。 値DOMError
に関連するエラーが含まれています。 Chrome 48 以降/Firefox 58 以降では、DOMError
が DOM 標準から削除されているため、このプロパティはDOMException
を返します。
- FileReader.error - Web API | MDN
作成した readFileAsDataUrl()
関数の使い方は以下の通り。
progress
イベントは使えなくなりますが、よほど大きなファイルでない限り 1 回目の progress
イベントで 100% 達していることがほとんどなので、あまり使いどころはないと考え、捨てています。
TypeScript で書く
TypeScript にするとこんな感じになります。
特筆すべきところはあまりありませんが、readFileAsDataUrl
関数が (file: File) => Promise<string>
型であることを明示的にアノテーションするところでしょうか。
呼び出す側の TypeScript 版はこんな感じになります。
(おまけ) Result 型を返すようにしてみる
こちらの記事 で紹介した Result
型を使用してさらに改造してみます。
あとがき
FileReader
API の使い方やより簡単に使える方法を TypeScript も交えて紹介しました。従前よりフロントエンドが担う役割が増えてきており、ファイルを取り扱うことも多くなっています。FileReader
API は使いやすいとは言い難いですが、使いやすい方法も紹介したのでお役に立てれば幸いです。