【javascript】FileReaderを使ってプレビューURLを作成しよう!

javascript
この記事は約23分で読めます。

はじめに

ファイルをアップロードする機能は、いまや多くのサービスで提供されています。
その機能を実装する過程を解説していきたいと思います!

クロ
クロ

ファイルアップロードは苦手意識があるにゃ~~~。

わい
わい

勘所さえつかめばかなりシンプルだよ!

結論(全体像)

▼HTML

<body>
    <div>
        <!-- img要素にプレビューURLを表示するよ -->
        <figure>
            <img src="" alt="previewが表示される" width="300" height="300">
        </figure>
     
        <!-- エクスプローラーを開くために必要 -->   
        <input type="file" accept="image/*" style="display: none;">
        <button type="button" onclick="this.previousElementSibling.click()">アップロード</button>
    </div>

    <script src="{{ asset('js/preview.js') }}"></script>
</body>

▼js

const $file_input = document.querySelector('[type="file"]');
const $img_element = document.querySelector('img')

// ファイルがアップロードされたら
$file_input.addEventListener('change', (event) => {
    const files = event.target.files;
    if (files.length === 0) return false;
    // 配列の先頭要素を取得(つまり、ファイル情報)
    const [file] = files
    if (file.type.includes('image') === false) return false;

    const file_reader = new FileReader()
    file_reader.readAsDataURL(file)
    file_reader.addEventListener('load', (event) => {
        // previewURLを取得
        const preview_url = event.target.result;
        $img_element.src = preview_url

        const fd = new FormData();
        fd.append('file', file)
        fetch('/api/upload-img', {
            method: "POST",
            body: fd
        })
            .then((res) => console.log(res))
            .catch((err) => console.log(err))
    })
})

▼php(これは個々のバックエンド言語で登録処理をしてください。)
ちなみに、この記事では Laravel を使った登録となっています!

Route::post('upload-img', function (Request $request) {
    // postされたファイルデータ取得
    $file = $request->file('file');
    $file_name = $file->getClientOriginalName();
    [$file_base_name, $extension] = explode('.', $file_name);

    // テーブルに登録
    $model = new Icon();
    $model->create([
        'file_name' => $file_base_name,
        'extension' => $extension,
    ]);
    // ストレージに格納
    \Storage::putFileAs('public', $file, $file_name);

    return response()->json([
        'msg' => 'ファイルアップロードに成功しました。'
    ]);
});

解説

1. まずはHTMLから!

▼HTML(全体)

<body>
    <div>
        <!-- img要素にプレビューURLを表示するよ -->
        <figure>
            <img src="" alt="previewが表示される" width="300" height="300">
        </figure>
     
        <!-- エクスプローラーを開くために必要 -->   
        <input type="file" accept="image/*" style="display: none;">
        <button type="button" onclick="this.previousElementSibling.click()">アップロード</button>
    </div>

    <script src="{{ asset('js/preview.js') }}"></script>
</body>

↑ではコメントである程度書いてるので、↓のポイントだけ

<button type="button" onclick="this.previousElementSibling.click()">アップロード</button>

クリックすると、クリックされた要素( this )の前の兄弟要素をクリックする という処理。

わい
わい

個人的には、要素を相対位置から取得するのは、
改修のしにくさと可読性が下がるので避けていますが、
当ブログのコンセプトには無駄な処理を書かないというのがあるので、重要なポイントだけ押さえてもらえればと思います!

2. 次にjavascript!

▼javascript(全体)

// エクスプローラーを開く要素を取得
const $file_input = document.querySelector('[type="file"]');
// プレビューURLを格納するimg要素を取得
const $img_element = document.querySelector('img')

// ファイルがアップロードされたら
$file_input.addEventListener('change', (event) => {
    const files = event.target.files;
    // データが空なら終了
    if (files.length === 0) return false;

    // 配列の先頭要素を取得(つまり、ファイル情報)
    const [file] = files
  // データが画像ファイルでなければ終了
    if (file.type.includes('image') === false) return false;

    const file_reader = new FileReader()
    file_reader.readAsDataURL(file)
    file_reader.addEventListener('load', (event) => {
        // previewURLを取得
        const preview_url = event.target.result;
        $img_element.src = preview_url

        const fd = new FormData();
        fd.append('file', file)
        fetch('/api/upload-img', {
            method: "POST",
            body: fd
        })
            .then((res) => console.log(res))
            .catch((err) => console.log(err))
    })
})

コメントで解説している部分は、本筋じゃありませんが、息をするように書くレベルのコードで最低限必要といっても過言ではないです!

const files = event.target.files;

↑でアップロードされたファイル情報を取得する。今回は multiple(画像の複数選択)を許可していないので、1つのファイル情報しか入ってきませんが、multiple の場合は複数入ってきます。

const [file] = files

↑これで配列の先頭要素を取得できます。便利ですね!

const file_reader = new FileReader()

ここからこの記事の本題といってもいい箇所ですね!
FileReader を使って、プレビューURLを取得していきます!

file_reader.readAsDataURL(file)

アップロードされたファイル情報を、readAsDataURL で読み込みます。

file_reader.addEventListener('load', (event) => {

↑readAsDataURL で読み込みが完了したら発火するイベントを定義します。

const preview_url = event.target.result;

↑ファイル情報を取得したのと似た感じで、プレビューURLを取得します!

こうすることによって、ブラウザが画像として解釈できる超ながーーーい文字列が取得できます。
↓みたいな感じ!

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALMAAADICAYAAACj8dDxAAAABHNCSVQICAgIfAhkiAAAAF96VFh0UmF3IHByb2ZpbGUgdHlwZSBBUFAxAAAImeNKT81LLcpMVigoyk/LzEnlUgADYxMuE0sTS6NEAwMDCwMIMDQwMDYEkkZAtjlUKNEABZiYm6UBoblZspkpiM8FAE+6FWgbLdiMAAAgAElEQVR4nOy9d5gmR3Xv/6mq7n7fd3La2STtarUrtMrJSIDIOQkhCSzjC76A7WtsExwwtu/PF3QdwOCfjW24vsYGG2x8ja+NEWCCjEESUVZEAcVdbQ4zszt53rff7q4694+q7vedlQQIrTYIneeZZ2be0KHq9Klzvud7TsGT8qQ8KU/Kk/KkPNGkR7a97JSjfRFPCuijfQHHufTKzivum23W75c9P3XO0b6YH3d5Upkfg8iu13wuzfQJtURRtLPrgcbRvqYfZ1FH+wKOV5Ftr3raYlO+o40BpVBAo2anaE+drU79zv6jfX0/jvKkZf7RRBPFH9WqQFwO4nAitFpuxUIx8uDRvrgfV3miKfPAkTiJ7Hrtx9KWO8M5AWUQV4DLcShioxqy/dL3H4nreFKWyxNGmWXHq3915o5XzsmOy//mcT3PA68ZnZ7N3+icQ+kYpU3Xmw7roNWy75Jtl/yvw37uHa+6Eqgd7uM+UeQJo8zttvx2vVEjzdSbWg9cPiNbX7TmcTlRXX2wt2FAaZSJQBzK1FCmDsoAgohiqcUvyfbLLjpcp5VdP/nnhU0+JQ9e8huH65hPNHlCKLPsuPR1ImqFKwqcA7F2aLFd2yW7r7j8sJ7ngZc+Z2kxfYO1FsSBsyAAzvvOCEoZBAEci0vFDbLrNc99jKc1su2V17QWl96WFxZ18uf/7rHexxNVnhDKDPpNzuZEff3UV46jG3W0Nrq5WHxatl/ynsN0kp6WrX1aKwXOhZcEcQViCwCUjkBHgIDSaA1Zzudk/5s3/ygnlN0vP112XHZXO49eLGhs4eBJ+O8R5YmgzKuXWrwAHRMP9KIjTW10mGRkBERopuYq2XXlPwDmBx7p+4hsf/X7lFajogwCiDhAoUyEMjFKaZRJAK/USmlQBlvY/ubs9O0X3fmOlY/qfFues35mWt/RatnNRZEDyq8G8CTs9why3Cuz7HzN5RrRumaCy1qAQNTbT331OtARrWb7p2Xbpd8Fxn/E0wzMzbffbvO2Ry4QEAGlgmXOq98o3fUD4gStVXLDwK7PPIp7+q9z6cC2eqNmnLPhIXHkeQEw9yPewxNejntlBvsWEUvUO+AtlxRekcSiI0Vj1SgqrtNMizNn73jFhGx7+U892jPIztf+cxx7BVVKo1Qw8spQ5Z2UqpInlJ9RBmVirLWkmXq67P7Jf/yB59r9U3+Qtvl4ksRKnPfBEQEUJorhSTfjEeW4VmZ58BXPbC5mZwJEPQ1QMagoKHUbEJSuUV+5kmhwBXESMb+k/1F2Xva2H/ocO694Z7udv1gEQCPiEJv5QM+2vQJrE5QuBII2Q8ShtMegldI4W9Bayn5Kdl7xM494rh2v/mprqfXfnSu8/oqrHhJQ/hekj2XMnshyXCszJv6vSmmi/pArERuUOfcog0u9gosmGRohGRkhMpo05c9l5xV/BIz8gDP0tNPi9631lhFxKBRoj1h4nDmqXA608T8qDGsZKCoFOkJQLCwUn5Btl57efRLZ/vJNsv3VW1rN4vmuyBHn8EGkCgkZi9KawromAT95Uh4qx60yywMvG5mbL94syhD3D+LjO+WnWmlQiVci1wRycDlRb4Pa+CqcQDtT75Sdr/kWUH/Ec+y84qMiqgbKK215+HLYxPpA0MRUyq4ilI4pdU5cEdwEQBmi2DC3xF2y5RnnAMiDL1gt1G5stvKNooz3j5UK35Hwvz/f8FD9a4/DUD5h5LhVZhpDL6zVE216B1BR6cPGgAMiIPdWmgLv1wo4halFNFavxhaWtO02H7j9kiXZcdmzDj287Hj187PCvE5U5JMjSntLHCxwOGJHUQGcw9nU48wiiMu9YisdPi2IQJIYNdsc+q488KJvzy3W9qapHUbX/Ge1QaEC56N8CAgKXhSP13A+EeT4VWYpfgY0ppYA2k+8FIANvnMclEGH103wPQ3KaOLBAWye01M3eqHJ12X7pe9adnxlPmSLIsBw4q2wKzyOXCq0ivxnXeF/COGgSFgdFLjCf88WlbUVW5DUajQz8/Q41ogyKK1L6C344XHHXVHGJ2ieJDl+XzlOlTk5ZanpXuGcxdQE7Dw+Lip92za4dvis8r40pTKpCrpTJkZQGA3Ntnm/7LjsnwFkx2UfT1N3upTYbvCJVVTzSisSFM/5Y+uoA8eVyg7eZzYxynh/WWzmXzex/x31BB9fEGcRHNjc/11lGMVDflqDNk8Gf99HoqN9AT+KyK5XvT1tOXSjJ/AjLBAUpILKSpdDKjQAVUJ3MUoLKjJIYcMSbmm11WuW7n11u5m6BFH+UVfg2kvevTA1cF4hVVQPyqkqq+wvTsJ5CZbV+786ShBnQWxIeauAcmT+oXIWpXRIxoBSClEqQIEahQPUzsd3ZI9vOaKWWba+9ELZ8erbZevLXv6YDuTyS5y1RH39+MCv9EmDBa785FKZHB0Lrfz7WqNjn7ETmyHO4mwBSiVe8f33xOaoqA4m8RBc1EDHvYFoVEOVVlaHTKBJgq+u/Xuu8JbZWco0txAgN8QjI0Xb++Biq9e9ZXbhQQnuks2aj2ncnuByZN2MqN7XbLqzm3n0hYW7L5mXHZd9WB586YmP5hCy/ZKnLS6xXkUROo5CFKb9jzKgah7JICgPJvjPwe1QkX9NHFGjLwR2QRF1BM4htkCK1FtLHaG07kpXl4pWKplCmQSFILaNFCkEd8IV/pwubwXMWAff1/vHgvJQnzIdtyJYaJ/4Cbi1UihlwdTywzALT1g5osqs1l/9tXpdTypl0Fr1p2355bkls1N2XLFbdr76l/hh3B5d+1WjBR3HXQmFAMXRFfQ9xGKX/5fpaIuKIyQonARfleCRVAhIGcxpg+TNYOWlCs60hiJLWZpboMgtOvKIBC5Hae0RjQpNKToWWcSfWyn/sOgIFSUej3aBlaeNfx3hwL4ZmNu38fDNxhNPjnh4LNte9dx2EV1rXYffAIJRsLBUuLHR5Hpc6/9TJ33pNh6a7RqYv+uSORNpTO8AtZFBkPIWSkjOdf4Xi1dkHayzDe97ZEOKnKXdu73fHRR02YB0oxJlCjsEhEorbJ4xsXOKdpqijbeutUaDlSeuQhvt3QbK7J0qkWewHiVR5bFDXFqiGZXfDORZwb7t+xHniJKIU07Qb1Dn3PTJwzIZTzA54miG2vC5b9VqHOi2UIhgRdHb0LrVKp63uBR9e+m+y2Zl5xUfBlaX35VtL3+zMYKIBMAguAz+yOFDuQ/ySrSh9DelXKEdEAKxyKAjU/nUpbpBQCVCYEaA5kqsGBzOFkzsmiTPc0wUYWL/k7VSpiemUdo/AB5iCw+DiHdjxHnlDtk9/+BJcJnUsqxic6EJSqGjyF9mVHvt4zU3x7scDTQjx8ldSnhuZalKHFdpxFm0Viila2lbfnnxvit+ubehvodt7VtYdC80kVdam7bCd7NgNQ2VC1H5y6WdLahIQdLNBFXE/f1ks/PeJbC5t9LG47rKJBCBFJm3oiXsqzSLpVuhdQA0CsQFBVxcJG8PYoxCVPCVxXr/11lUlAQiUunT+OsU10aZBuC8D49lacEvTkorXF5A1uZJeXg5KjizOukzryisC8FV7K2gDiX7YekVcdgiR7mcVqs4o5mqF/q8gldU127i2q0AwWmv1FUyrly6IzB1hJgiKxBRAQf2gRwoTKPX1/GJ+N8hQSFFFqwyAaWIAkfCIGLJM+9fK60Q51BKYyLj8yTWYe3yVHTHCisUOhjhknUXcOwQSJb3UTHmRLB5gdKKwtJ/5Gbq+JKjhTM3+/viz7TS4jIRH6QpZQI0ZarMmVeyMtdhQHd5tDrGtnN0rR6QAhOstLdsKLBFzsHtO5l6YAvKaJQ2jG5Yx4qT13kEDlDGdGI9BGw7oBslfk3Hipq4UtwoNtgsx2nt/WUE5wSlFNpob5UllFWVwZx4KM4FJl2ZXFHKrwqgECUe0hNBGe3h7ijCei4zUaSebGXwCHIUM4DFB5wNPmhQJK+8FmxeYbwdc+stl45qKGVQ2mDTZlfgpLzFlZb/W2v23LWVqQe2oKMEE9fQxnBw20523nKXT6CgvOIEIr0KMF0wuR2riuCKzF9XsOj1njo2L9Bao8rkRnAX4iQmio1PYbuAHTuv0GUA6RVdPMbdlcaWAMc5m+GKnOGV44izmNiEfEz+wSM2RceZHDVlVuuuvrEeuwlvnCRgqiGaJyi3iStLLa7wIZr4XhVKaVyWd8CLKtjzqWWXt5nduc0nRhTYosDmFm0iFg8cYH7/QVAZUASYLyRYoFK4kksBeO5ERSCyJLWItaeswzmHLXz62TmHAsZWj/rrDFTREtqr7j1KKnZc2aqgU3LlXY+SyNTb30Pv4ACCyjevj35GnX/b9x6vOTne5WhyM5yJ5a2qxG0Jy3KwhsrUO30pRPxSHAIocIhYxGYUzXkP9ooLSEYGktJeavnMm3W4wiLWux5F26egFw7MVrCbjmOPMrii4+ZAsPgh0aGMx4CDKyQCjb4e1mxYQ99gL/VGndFVY6w9eS1R4q272MIHfkXbp76dDaQlV8Fw3t8PPnXgQncKY/3rY6uGOfWcDZPq3Jv+/ojO0HEmR5VopNb/22fqdX3Q/yeVPfIpYg/BubxFlXXDL8MV0UcpXDv1UBzg8eMEUNR6asT1Osr4W4xqSQjmPMMuSkouR4yOa7giBXG4IvDfleqkqssrCxk6D+35lSSuRYytHWf8xBX09kWeu1Gy3MS7GWKz6nr9rYYVyBWIbftzKhXcGBCXh9YF5choENn7OEzBE0qONmvOos
クロ
クロ

ながい~~~。。。

わい
わい

そう、こんな長い文字列を src属性 にセットするのってなんかスマートじゃない気もするよね。。。
そういった人のために、当記事の最終章に URL.createObjectUrl を使って、blob の URL を生成して、セットする方法も紹介しているのでぜひ!

$img_element.src = preview_url

最後に画像要素の src属性 に値をセットして終了!
もちろん、ブラウザをリロードするとプレビューURLなので消えてしまいます。

わい
わい

ここでこの記事の本題は終了です!

以降は、ストレージへの格納やテーブルへの登録などの処理の説明です!
つまり、ブラウザでリロードしてもデータが永続的に残るようにする処理です。

3. javascript と php (アップロードするためには)

▼js (続き)

        const fd = new FormData();
        fd.append('file', file)
        fetch('/api/upload-img', {
            method: "POST",
            body: fd
        })
            .then((res) => console.log(res))
            .catch((err) => console.log(err))
ケン
ケン

Laravelを使っているので、axios を使うのが主流ですが、今回はライブラリを必要としない fetch API を使って実装しています!

const fd = new FormData();
fd.append('file', file)

↑ファイルデータをアップロードするために、FormDataインスタンスを生成して、そこにファイルデータを追加する。

        fetch('/api/upload-img', {
            method: "POST",
            body: fd
        })

fetch で ajax通信を行う。

▼php (web.php)

▼php (全体)

Route::post('upload-img', function (Request $request) {
    // postされたファイルデータ取得
    $file = $request->file('file');
    $file_name = $file->getClientOriginalName();
    [$file_base_name, $extension] = explode('.', $file_name);

    // テーブルに登録
    $model = new Icon();
    $model->create([
        'file_name' => $file_base_name,
        'extension' => $extension,
    ]);
    // ストレージに格納
    \Storage::putFileAs('public', $file, $file_name);

    return response()->json([
        'msg' => 'ファイルアップロードに成功しました。'
    ]);
});
ケン
ケン

Controllerは使っていません。

Controllerで処理を任せるのが一般的だと思いますが、簡略化のために route に処理を書いています。

$file = $request->file('file');

↑Laravelのお作法ですね。
ファイル以外の場合は、下のように get() で取得しますが、ファイルの場合は file() で取得しています。

$request->get('key');

クライアントからアップロードされたファイルの、クライアントがつけたファイル名を取得する。

わい
わい

もちろん、当記事のようにクライアントがつけたファイル名をそのままストレージや、テーブルに格納することは、他ユーザーとの衝突を生むので避けるべきです!

    // テーブルに登録
    $model = new Icon();
    $model->create([
        'file_name' => $file_base_name,
        'extension' => $extension,
    ]);

↑そのまんまですね!↓に migrationファイルもおいておきます!

        Schema::create('icons', function (Blueprint $table) {
            $table->id();
            $table->string('file_name');
            $table->string('extension');
            $table->timestamps();
        });
// ストレージに格納
\Storage::putFileAs('public', $file, $file_name);

↑もLaravelのお作法でストレージに格納していきます!

わい
わい

以上が、ファイルアップロードの過程すべてです!

次章では、超ながーーーい文字列を超コンパクトなURLにするやり方での実装方法を紹介します!

補足: 超ながーーーい文字列をコンパクトなURLへ!

▼js (変更箇所)

const $file_input = document.querySelector('[type="file"]');
const $img_element = document.querySelector('img')

// ファイルがアップロードされたら
$file_input.addEventListener('change', (event) => {
    const files = event.target.files;
    if (files.length === 0) return false;
    // 配列の先頭要素を取得(つまり、ファイル情報)
    const [file] = files
    if (file.type.includes('image') === false) return false;

    const file_reader = new FileReader()
    file_reader.readAsArrayBuffer(file)
    file_reader.addEventListener('load', (event) => {
        // previewURLを取得
/** ↓ここから */
        const array_buffer = event.target.result;
        const blob = new Blob([array_buffer]);
        const preview_url = URL.createObjectURL(blob);
/** ここまで */
        $img_element.src = preview_url

        const fd = new FormData();
        fd.append('file', file)
        fetch('/api/upload-img', {
            method: "POST",
            body: fd
        })
            .then((res) => {
/** ↓ここも */
                // ブラウザがリロードされるまでメモリ上に残り続けるので明示的に削除する
                URL.revokeObjectURL(preview_url)
            })
            .catch((err) => console.log(err))
    })
})

↑変更箇所に印をつけています。

こうすることによって、超ながーーーい文字列が以下のようなURLになります。

blob:http://127.0.0.1/d86d8161-bbea-43e2-a860-93b85ff25b5f

↑は平たく言うと、超長い文字列を格納しているメモリ領域へのアドレス(URL)です!
なので、該当URLをブラウザでたたくと超ながーーーい文字列が閲覧出来るかと思います。

わい
わい

しかし、注意点として明示的に破棄してあげないと、ブラウザがリロードするまでメモリ上に残り続けて、メモリリークの原因にもなるので↓で破棄してあげます。

                // ブラウザがリロードされるまでメモリ上に残り続けるので明示的に削除する
                URL.revokeObjectURL(preview_url)

さいごに

以上、ファイルアップロードに関してのあれこれでした!

コメント