FileReader を Promise で扱いやすくするライブラリを作った
はじめに
こんにちは、株式会社TERASSの myrear です。
あるサービスの開発で FileReader を使うことがあったのですが、いくつかの点で扱いづらさを感じました。
そこで扱いやすくするための小さいライブラリを作ったのでご紹介します。
結論だけ知りたい!という方はリポジトリをご覧ください。
スターを付けてくれると喜びます⭐
FileReader
とは
FileReader
とはざっくり言うと File や Blob を読み取るための Web API です。
以下 MDN より抜粋です。
FileReader オブジェクトを使用すると、ウェブアプリケーションは、ユーザーのコンピューターに保存されているファイル(または生のデータバッファー)の内容を非同期に読み取ることができます。File または Blob オブジェクトを使用して、読み込むファイルまたはデータを指定します。
FileReader
の扱いづらさ
まずは FileReader
のどういったところが扱いにくいと感じたのかをサンプルコードとともに見ていきます。
以下は input[type="file"]
な要素から選ばれた画像を img
要素に表示する React コンポーネントの例です。
export const ImageInput = () => {
const [dataUrl, setDataUrl] = React.useState<string>()
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const file = e.target.files?.item(0)
if (!file) return
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => setDataUrl(reader.result as string)
}
return (
<>
<input type="file" onChange={onChange} />
<img src={dataUrl} />
</>
)
}
簡単に処理の流れを追うと、
- 選択されたファイルを取得
-
FileReader
インスタンスの作成 -
readAsDataURL
メソッドでファイルの読み込みを開始する -
load
イベントを待ち受けてresult
をステートに保持する
となっています。
最低限の処理だけですが雰囲気だけ掴んでもらえればOKです。
Promise ではない
1つ目のイマイチポイントとして Promise
ではないことが挙げられます。
今回の例では readAsDataURL
を使っていますが、その他にも FileReader
には readAsArrayBuffer
readAsBinaryString
readAsText
とデータを読み込むためのメソッドが全4種類あります。
これらすべて戻り値は void
であり、データの読み込みが完了したことを知るためには load
イベントを別途監視しなければなりません。
もし戻り値が load
イベント発火により解決する Promise
であれば、処理を一つにまとめられそうです。
- reader.readAsDataURL(file)
- reader.onload = () => setDataUrl(reader.result as string)
+ reader.readAsDataURL(file).then(() => setDataUrl(reader.result as string))
result
の型が曖昧
2つ目のイマイチポイントは result
の型の曖昧さです。
FileReader.result
の型は string | ArrayBuffer | null
と定義されています。
というのも呼び出される readAs...
メソッドによって result
が変化するからですね。
読み込みが完了するまでは null
なのでこの型なのも納得はできます。
ただ、今回の例のように readAsDataURL
で読み込んだ場合は string
として扱いたいのに、型が曖昧だと型アサーションなどで対応しなければならず少し面倒です。
load
イベント発火により対応する型の値で解決する Promise
にできれば取り回しやすそうです。
- reader.readAsDataURL(file).then(() => setDataUrl(reader.result as string))
+ reader.readAsDataURL(file).then(setDataUrl)
扱いやすくするためには
以上を踏まえると下記2点を満たすことができれば扱いやすくなりそうです。
-
readAs...
メソッドはload
イベントの発火に合わせて解決するPromise
とする - 呼び出された
readAs...
メソッドに対応した型の値でPromise
を解決する
というわけで、冒頭でも紹介しましたがこれらを満たすライブラリを作りました。
このライブラリは readAs
関数と FileReader
クラスの2種類のモジュールを提供します。
readAs
本ライブラリのコア部分とも言える関数です。
オーバーロードによって上述の要件を表現しています。
load
イベントでの解決の他に error
や abort
イベントでのリジェクトにも対応しています。
つまり FileReader.abort()
が呼び出されたり読み込みに失敗すると Promise
はリジェクトします。
FileReader
本ライブラリの本体です。
とはいっても各種 readAs...
メソッドをオーバーライドして上記の readAs
関数を呼び出すようにしただけの薄いラッパークラスです。
globalThis.FileReader
つまり Web API の FileReader
を継承したクラスなので既存のコードベースを大きく変更することなく使えると思います。
適用してみる
上の方に挙げたコード例を本ライブラリを使って書き換えてみます。
+ import { FileReader } from '@myrear/file_reader_promise'
export const ImageInput = () => {
const [dataUrl, setDataUrl] = React.useState<string>()
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const file = e.target.files?.item(0)
if (!file) return
const reader = new FileReader()
- reader.readAsDataURL(file)
- reader.onload = () => setDataUrl(reader.result as string)
+ reader.readAsDataURL(file).then(setDataUrl)
}
return (
<>
<input type="file" onChange={onChange} />
<img src={dataUrl} />
</>
)
}
readAsDataURL
と onload
を別々に書いていたのが一つになり、 as
による型アサーションも消えてコードの見通しが良くなりました。
類似ライブラリとの比較
実は似たようなことをやっているライブラリは(筆者が把握している限りでは)3種類あります。
簡単にですがそれぞれとの比較をしてみます。
promise-file-reader
readAsDataURL
readAsText
readAsArrayBuffer
の3種類の関数をエクスポートしています。
-
readAsBinaryString
に非対応 -
readAsText
の文字コードが指定できない - 引数に渡せるのは
Blob
のみ -
FileReader
インスタンスを内部で作成・保持しておりアクセスできない(abort
などができない)
@tanker/file-reader
Web API の FileReader
に酷似した FileReader
クラスをエクスポートしています。
- コンストラクタ引数で
File
を受け取ることが可能 - Web API の
FileReader
を継承したクラスではない(abort
メソッドは実装している模様) -
ArrayBuffer
のチャンク単位での読み込みに対応
file-reader-promise
FileReader
ライクなオブジェクトをエクスポートする CommonJS ライブラリです。
- 型定義ファイルがない
-
Promise
をイベントオブジェクトとresult
のどちらで解決するかを設定できる -
FileReader
インスタンスを内部で作成・保持しておりアクセスできない(abort
などができない)
おわりに
以上、 FileReader
を扱いやすくするためのライブラリを作成・紹介しました。
実は Node ではなく Deno で作成したのですが、 Deno での開発体験は結構快適でした。
Deno で開発をする上で得た知見はまた別の機会に共有できればと思います。
最後までお読みいただきありがとうございました。
雑談
本題とは全く関係のない話になりますが、弊社には労働環境構築のために最大10万円の購入費が支給される Best Productivity という制度があります。(その他詳細はこちら)
筆者は最近この制度を利用しまして、 Kinesis Advantage360 Keyboard を購入いたしました。
パームレスト込でだいたい85000円くらい(!)でした。
初めての分割キーボードどころかこれまでずっとラップトップで生きてきた人間なので慣れるまでが大変でした(届いて2週間近く経った今でもタイポが多いです)が、肩が縮こまらず使えるので肩への負担を抑えられてかなり満足しています。
以上、人生初の高級キーボードを自慢したかっただけの雑談でした。
Discussion