XMLHttpRequest でファイルをアップロードする方法

この記事では XMLHttpRequest と FormData を使って、ファイルをアップロードする方法を説明します。

XMLHttpRequest によるファイルアップロードの概要

XMLHttpRequest を使うと JavaScript から、非同期でサーバーと通信を行うことが可能になります。

非同期処理というのは、処理を開始したら、その場で処理の終了を待たずに、なんらかの方法で後で処理が終了したことの通知を受けるような仕組みのことです。

ファイルのアップロードは、ネットワークの状況や、アップロードするファイルの数やサイズによって、 どのくらい時間がわかりません。このため、ファイルのアップロード処理は、同期処理で行うより、非同期で行う方が望ましいです。

また、ファイルサイズによってはアップロードに時間がかかるので、データ転送の進捗状況を、ユーザーに示すセルのが望ましいです。

Fetch API などでもファイルアップロードは可能ですが、現時点では fetch() にはアップロードの進捗状況のチェックイベントがありません。 このため進捗状況のチェックをしたいときは fetch() は使えません。この点で XMLHttpRequest を使う方法は重宝します。

file 型の input 要素でファイルを選択する

ファイルを選択するには、 file 型の input 要素を使います。

<form id="f1">
  <input type="file" id="file1" name="file1">
</form>

複数のファイルを選択する場合には、 multiple 属性を設定します。

<form id="f1">
  <input type="file" id="file1" name="file1" multiple >
</form>

file 型の input 要素に change イベントを設定すると、 ファイルの選択状態が変わった時に、 change イベントハンドラが呼び出されます。

このときに、inputfiles プロパティに、選択されたファイルの情報が設定された、 File オブジェクトの配列が設定されます。

const f = document.getElementById('file1');
f.addEventListener('change', (evt) => {
  const fileIn = evt.target;
  // fileIn.files[0]
  // fileIn.files[1]
  // ...
});

ひとつだけファイルを選択した場合は、files[0] がそのファイルに対する File オブジェクトです。

このファイルオブジェクトは JavaScript で取り置いておけば、インプット要素で選択するファイルを変更しても、取り置いたファイルオブジェクトは有効です。

アップロードファイルは FormData に詰め込む

アップロードするファイルの File オブジェクトを取得したら、それを FormData オブジェクトにセットします。

もしお行儀よく、インプット要素が form 要素の子要素として作成されていれば、FormData の作成は簡単です。

フォーム要素を FormData に渡した上で FormData オブジェクトを作成すれば、自動的に FormData オブジェクトにセットされます。

const form = document.getElementById('f1');
const fd = new FormData(form);

FormData の使い方」 も参考にしてください。

また、手動で FormData オブジェクトにファイルオブジェクトを設定しても構いません。

このときはインプット要素でファイルを選択した結果作成された File オブジェクトを、JavaScript でどこかに保存しておいて後から FormData オブジェクトにセットするか、 あるいはアップロード用のデータを Blob として作成してそれを FormData にセットすることができます。

アップロードの進行状況は upload プロパティのイベントで確認可能

アップロードの進行状況を確認するにはどうしたらよいでしょうか?

アップロードの状況は、XMLHttpRequest オブジェクトに紐付く XMLHttpRequestUpload オブジェクトで取得できます。

XMLHttpRequestUpload オブジェクトは、XMLHttpRequest オブジェクトの upload プロパティにセットされています。

このオブジェクトにイベントリスナーを追加することで、アップロードの開始、進行状況のチェック、エラー発生、アップロード終了などのイベントを処理することができます。

設定できるイベントの種類と意味は、次のコードサンプル内に書きました。

xhr.open("POST", "/test1");

...
// アップロード関連イベント
xhr.upload.addEventListener('loadstart', (evt) => {
  // アップロード開始
});

xhr.upload.addEventListener('progress', (evt) => {
  // アップロード進行パーセント
  const percent = (evt.loaded / evt.total * 100).toFixed(1);
  console.log(`++ xhr.upload: progress ${percent}%`);
});

xhr.upload.addEventListener('abort', (evt) => {
  // アップロード中断
  console.log('++ xhr.upload: abort (Upload aborted)');
});

xhr.upload.addEventListener('error', (evt) => {
  // アップロードエラー
  console.log('++ xhr.upload: error (Upload failed)');
});

xhr.upload.addEventListener('load', (evt) => {
  // アップロード正常終了
  console.log('++ xhr.upload: load (Upload Completed Successfully)');
});

xhr.upload.addEventListener('timeout', (evt) => {
  // アップロードタイムアウト
  console.log('++ xhr.upload: timeout');
});

xhr.upload.addEventListener('loadend', (evt) => {
  // アップロード終了 (エラー・正常終了両方)
  console.log('++ xhr.upload: loadend (Upload Finished)');
});

xhr.send(fd);

以上のように、一連のイベントをセットした後で、XHR オブジェクトの send() メソッドで、 FormData オブジェクトを POST すればアップロードできます。

全体のコードサンプルはこの後の例をみてください。

XMLHttpRequest による基本的なファイルアップロード・サンプル

概要は上で説明した通りです。ここではファイルをアップロードする、具体的なサンプルを紹介します。

【サーバー側】アップロードファイルを受け取るサーバー側のコードサンプル (Node+Express)

まず、アップロードされたファイルを受け取る側の、サーバー側を作りましょう。

ここでは Node.js 環境で開発します。

Web サーバーとして Express、 マルチパートデータ (multipart/form-data) 受取用のミドルウェアとしては multer を利用します。

Node のコマンド例を示します。

mkdir demo
cd demo
npm init -y
npm install express multer

また、入力表示用の HTML および JavaScript などのスタティックファイルは public と言う名前のディレクトリに、 アップロードされたファイルは uploads と言う名前のディレクトリに保存することとします。

mkdir public
mkdir uploads

次の内容を server.js という名前で保存します。

const express = require('express')
const app = express()

const multer = require('multer')
const upload = multer({
  dest: './uploads/',
  limits: {
    fileSize: 104857600, // byte per file
    files: 10,
  },
}).array('file1', 10)

app.use(express.static('public'))

app.post('/test1', (req, res) => {
  upload(req, res, (err) => {
    try {
      if (err) {
        throw err
      }

      console.log('/test1')
      console.log(req.headers['content-type'])
      console.log('* body')
      console.log(req.body)
      console.log('* files')
      console.log(req.files)

      res.status(200).json({ message: 'OK' })
    } catch (err) {
      if (err instanceof multer.MulterError) {
        res.status(500).json({
          message: err.code,
        })
      } else {
        res.status(500).json({
          message: 'Unknown Error',
        })
      }
    }
  })
})

app.listen(3000, () => console.log('Listening...'))

これを実行するには次のコマンドになります。

node server.js

サーバー開発時には nodemon を利用して、 スクリプトを実行すると、スクリプト変更時に直ちにサーバーが再起動し変更が有効になるので便利です。

npm install nodemon --save-dev
nodemon server.js

ちなみに Express でないとアップロードがうまくいかない、ということはありませんので、サーバー側のコードは好きな物を使ってください。

単一のファイルをアップロードするサンプル

ここではファイル選択インプット要素と、アップロードボタンと、クリアボタンを作ります。

Clear を押すとファイルの選択が解除されます。Upload を押すとファイルがアップロードされます。

ファイルのアップロード進行状況は、プログレスバーで表示します。プログレスバーは Bootstrap の UI 要素として作っています。進捗の詳細はデバッグコンソールの方に出力します。

publicディレクトリ内にtest1.html として次を作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Upload Test (single)</title>
    <!-- Bootstrap -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <!-- Bootstrap Icons -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"
    />
    <script src="test1.js"></script>
  </head>
  <body>
    <div class="container">
      <!-- Navigation -->
      <nav aria-label="breadcrumb">
        <ol class="breadcrumb">
          <li class="breadcrumb-item"><a href="/">Home</a></li>
          <li class="breadcrumb-item active" aria-current="page">
            Single File Upload
          </li>
        </ol>
      </nav>
      <!-- Form -->
      <form id="form1" class="mt-3">
        <div class="input-group">
          <input type="file" id="file1" name="file1" class="form-control" />
        </div>
        <div
          id="progressBarContainer"
          class="progress invisible mb-1"
          style="height: 5px"
        >
          <div
            id="progressBar"
            class="progress-bar progress-bar-striped progress-bar-animated"
            role="progressbar"
          ></div>
        </div>
        <div>
          <button type="button" id="uploadButton" class="btn btn-primary">
            <i class="bi bi-upload"></i> Upload
          </button>
          <button type="button" id="clearButton" class="btn btn-secondary">
            <i class="bi bi-x"></i> Clear
          </button>
        </div>
      </form>
    </div>
  </body>
</html>

同じく public ディレクトリ内にtest1.js として次を作成します。

'use strict'

const $ = (id) => document.getElementById(id)

window.addEventListener('load', () => {
  $('uploadButton').addEventListener('click', (evt) => {
    evt.preventDefault()

    if (!$('file1').value) {
      console.log('No file is selected.')
      return
    }

    const fd = new FormData($('form1'))
    const xhr = new XMLHttpRequest()

    xhr.open('POST', '/test1')

    // Basic Events
    xhr.addEventListener('load', (evt) => {
      console.log('** xhr: load')
      const response = JSON.parse(xhr.responseText)
      console.log(response)
    })

    xhr.addEventListener('progress', (evt) => {
      console.log('** xhr: progress')
    })

    xhr.addEventListener('error', (evt) => {
      console.log('** xhr: error')
    })

    // Upload Events
    xhr.upload.addEventListener('loadstart', (evt) => {
      console.log('++ xhr.upload: loadstart')
      setProgressBar(0)
    })

    xhr.upload.addEventListener('progress', (evt) => {
      const percent = ((evt.loaded / evt.total) * 100).toFixed(1)
      console.log(`++ xhr.upload: progress ${percent}%`)
      setProgressBar(percent)
    })

    xhr.upload.addEventListener('abort', (evt) => {
      console.log('++ xhr.upload: abort (Upload aborted)')
    })

    xhr.upload.addEventListener('error', (evt) => {
      console.log('++ xhr.upload: error (Upload failed)')
    })

    xhr.upload.addEventListener('load', (evt) => {
      console.log('++ xhr.upload: load (Upload Completed Successfully)')
    })

    xhr.upload.addEventListener('timeout', (evt) => {
      console.log('++ xhr.upload: timeout')
    })

    xhr.upload.addEventListener('loadend', (evt) => {
      console.log('++ xhr.upload: loadend (Upload Finished)')
      setTimeout(() => clear(), 1e3)
    })

    xhr.send(fd)
  })

  $('clearButton').addEventListener('click', (evt) => {
    evt.preventDefault()
    clear()
  })
})

const setProgressBar = (percent) => {
  if (percent < 0) {
    showProgressBar(false)
  } else {
    showProgressBar(true)
    $('progressBar').style.width = `${percent}%`
  }
}

const showProgressBar = (show) => {
  const c = $('progressBarContainer')
  if (show) {
    removeClass(c, 'invisible')
    addClass(c, 'visible')
  } else {
    removeClass(c, 'visible')
    addClass(c, 'invisible')
    $('progressBar').style.width = '0%'
  }
}

const removeClass = (elm, cls) => {
  if (elm.classList.contains(cls)) {
    elm.classList.remove(cls)
  }
}

const addClass = (elm, cls) => {
  if (!elm.classList.contains(cls)) {
    elm.classList.add(cls)
  }
}

const clear = () => {
  showProgressBar(false)
  $('file1').value = ''
}

これを試すには、上で作ったサーバープログラムを開始して、ブラウザから http://localhost:3000/test1.html にアクセスしてください。

次のようなページになるはずです。

ファイルサイズの大きなファイルをアップロードすれば、アップロードの進行状況が表示されるはずです。 ファイルサイズが小さいといきなり 100% 完了となりますので、テストのとき気をつけてください。

複数のファイルをアップロードするサンプル

次の例では複数のファイルを選択して、一度にアップロードします。

public ディレクトリ内に test2.html として次を作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Upload Test (multiple)</title>
    <!-- Bootstrap -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <!-- Bootstrap Icons -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"
    />
    <script src="test2.js"></script>
  </head>
  <body>
    <div class="container">
      <!-- Navigation -->
      <nav aria-label="breadcrumb">
        <ol class="breadcrumb">
          <li class="breadcrumb-item"><a href="/">Home</a></li>
          <li class="breadcrumb-item active" aria-current="page">
            Multiple File Upload
          </li>
        </ol>
      </nav>
      <!-- Form -->
      <form class="mt-3 mb-3">
        <div id="progressBarContainer" class="progress invisible mb-1">
          <div
            id="progressBar"
            class="progress-bar progress-bar-striped progress-bar-animated"
            role="progressbar"
          ></div>
        </div>
        <div>
          <button type="button" id="selectButton" class="btn btn-primary">
            <i class="bi bi-files"></i> Select Files
          </button>
          <button type="button" id="uploadButton" class="btn btn-primary">
            <i class="bi bi-upload"></i> Upload
          </button>
          <button type="button" id="clearButton" class="btn btn-secondary">
            <i class="bi bi-x"></i> Clear
          </button>
        </div>
      </form>
      <ul id="fileList" class="list-group"></ul>

      <input
        type="file"
        id="file1"
        name="file1"
        style="display: none"
        multiple
      />
    </div>
  </body>
</html>

ちなみに、これまで作成した test1.htmltest2.html を行ったり来たりできるように、 ナビゲーションがついてます。これは当然ながらファイルアップロードとは直接関係ありません。

一応、目次ページも作ったので載せておきます。 public ディレクトリ内に index.html として作成してください。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Upload Test</title>
    <style>
      body {
        font-family: sans-serif;
      }
    </style>
  </head>
  <body>
    <ul>
      <li><a href="test1.html">Upload Single File</a></li>
      <li><a href="test2.html">Upload Multiple Files</a></li>
    </ul>
  </body>
</html>

さて、public ディレクトリ内に test2.js として次を作成します。

'use strict'

const $ = (id) => document.getElementById(id)
const selectedFiles = []

window.addEventListener('load', () => {
  $('uploadButton').addEventListener('click', (evt) => {
    evt.preventDefault()

    if (selectedFiles.length === 0) {
      console.log('No file is selected.')
      return
    }

    const fd = new FormData()
    selectedFiles.forEach((f) => fd.append('file1', f, f.name))

    const xhr = new XMLHttpRequest()

    xhr.open('POST', '/test1')

    // Basic Events
    xhr.addEventListener('load', (evt) => {
      console.log('** xhr: load')
      const response = JSON.parse(xhr.responseText)
      console.log(response)
    })

    xhr.addEventListener('progress', (evt) => {
      console.log('** xhr: progress')
    })

    xhr.addEventListener('error', (evt) => {
      console.log('** xhr: error')
    })

    // Upload Events
    xhr.upload.addEventListener('loadstart', (evt) => {
      console.log('++ xhr.upload: loadstart')
      setProgressBar(0)
    })

    xhr.upload.addEventListener('progress', (evt) => {
      const percent = ((evt.loaded / evt.total) * 100).toFixed(1)
      console.log(`++ xhr.upload: progress ${percent}%`)
      setProgressBar(percent)
    })

    xhr.upload.addEventListener('abort', (evt) => {
      console.log('++ xhr.upload: abort (Upload aborted)')
    })

    xhr.upload.addEventListener('error', (evt) => {
      console.log('++ xhr.upload: error (Upload failed)')
    })

    xhr.upload.addEventListener('load', (evt) => {
      console.log('++ xhr.upload: load (Upload Completed Successfully)')
    })

    xhr.upload.addEventListener('timeout', (evt) => {
      console.log('++ xhr.upload: timeout')
    })

    xhr.upload.addEventListener('loadend', (evt) => {
      console.log('++ xhr.upload: loadend (Upload Finished)')
      setTimeout(() => clear(), 1e3)
    })

    xhr.send(fd)
  })

  $('file1').addEventListener('change', (evt) => {
    const input = $('file1')
    if (!input.value) {
      return
    }
    for (const f of input.files) {
      selectedFiles.push(f)
    }
    updateFileList()
    input.value = ''
  })

  $('selectButton').addEventListener('click', (evt) => {
    evt.preventDefault()
    $('file1').click()
  })

  $('clearButton').addEventListener('click', (evt) => {
    evt.preventDefault()
    clear()
  })
})

const setProgressBar = (percent) => {
  if (percent < 0) {
    showProgressBar(false)
  } else {
    showProgressBar(true)
    $('progressBar').style.width = `${percent}%`
  }
}

const showProgressBar = (show) => {
  const c = $('progressBarContainer')
  if (show) {
    removeClass(c, 'invisible')
    addClass(c, 'visible')
  } else {
    removeClass(c, 'visible')
    addClass(c, 'invisible')
    $('progressBar').style.width = '0%'
  }
}

const removeClass = (elm, cls) => {
  if (elm.classList.contains(cls)) {
    elm.classList.remove(cls)
  }
}

const addClass = (elm, cls) => {
  if (!elm.classList.contains(cls)) {
    elm.classList.add(cls)
  }
}

const clear = () => {
  showProgressBar(false)
  selectedFiles.length = 0
  updateFileList()
}

const updateFileList = () => {
  const fl = $('fileList')
  fl.innerHTML = '' // remove all children
  for (const f of selectedFiles) {
    const li = document.createElement('li')
    li.innerHTML = f.name
    li.className = 'list-group-item'
    fl.appendChild(li)
  }
}

これを試すには、ブラウザで http://localhost:3000/index2.html にアクセスします。

Select Files ボタンを押すと、ファイル入力フィールドの click() イベントを発生させることによって、 ファイルの選択ダイアログが表示され、ファイルが選択可能になります。

また、 input タグに multiple 属性がついているので、複数のファイルを選択することが可能です。

ここではファイルインプット入力フィールドで拾ってきた File オブジェクトを、selected_files という配列に取り置いています。取り置いた内容は、リストとして表示しています。

アップロードボタンを押した時に、そこから File オブジェクトをとってきて、FormData に詰め込んでアップロードしています。

以上、ここでは XMLHttpRequest と FormData を使って、非同期でファイルをアップロードする方法を紹介しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 JavaScript 入門