FormData の使い方

1/21/22 更新: FormData の JSON でのポストの節を追加。また、Express (サーバー側) のサンプルコードを multer を使うように変更。

FormData とは?

FormData を使うと Web サイト上のフォームの内容を簡単にキャプチャできます。 キャプチャした内容はキー・バリューのペアとして (要はディクショナリとして) 利用することができます。

append() などのメソッドも用意されていて、FormData オブジェクトの内容を編集することも可能です。

また、必ずしもページの DOM ツリー内に form 要素を作って、それをキャプチャするというステップを踏む必要はありません。

XMLHttpRequest や Fetch API を使って、JavaScript から非同期的にデータをサブミットしたいときなどは、単に FormData オブジェクトを作成してその中にデータを詰め込んでサブミットすれば良いです。

JSON などでデータ送信する場合と違って、FormData オブジェクトに詰め込んだ内容はマルチパートのフォームデータとして扱われます。 単純な文字列データだけでなく、個別の MIME タイプの設定が必要なファイルのアップロードなども簡単に実現できます。

FormData でフォームの内容をキャプチャする

FormData のコンストラクタに、form 要素の DOM オブジェクトを渡すと自動的にフォームの内容を取り込みます。

// 'f1' は form 要素に設定した id とします。
const f1 = document.getElementById('f1');
const fd = new FormData(f1);

FormData

このとき、inputselect などのフォームの入力フィールドが自動的に取り込まれます。 しかし、disabled になっているフィールドは取り込まれません。

FormData の中身

FormData オブジェクトの中身は、FormData の entries() メソッドを利用して確認することができます。 このメソッドがキー・値の配列を返します。

FormData では .entries() は省略可能です。

// 'f1' は form 要素に設定した id とします。
const f1 = document.getElementById('f1');
const fd = new FormData(f1);

for (let d of fd.entries()) { // let d of fd でも同じ
    console.log(`${d[0]}: ${d[1]}`);
}

または、次のようにスプレッド構文を使って、内容を配列 (の配列) として表示することもできます。

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

console.log([...fd.entries()]); // ...fd でも同じ

FormData へのデータのセットと追加

FormData は form 要素の内容を自動的にキャプチャします。しかし、 必ずしも form 要素は必要ではありません。

空の FormData オブジェクトを作成して、それにデータを設定、変更などをすることもできます。

FormData への単純なキー・バリューペアのセットと追加

FormData オブジェクトの set() メソッドや append() メソッドを使うと、FormData オブジェクトに新しいキーと値をセットできます。

const fd = new FormData();
fd.set('user', 'Yamada');
fd.set('mail', 'yamada@example.com');
for (let d of fd) {
    console.log(`${d[0]}: ${d[1]}`);
}

この結果、次の内容がコンソールに出力されます。

user: Yamada
mail: yamada@example.com

FormData の set() と append() の違いは?

FormData には set() メソッドと append() メソッドがあり、 どちらも FormData オブジェクトにエントリーリストを設定するために使えます。

これらはどのような違いがあるのでしょうか?

set() メソッドは同じキーのエントリーが既に存在していたら、そのエントリーを新しい値で上書きします。

const fd = new FormData();
fd.set('user', 'Yamada');
fd.set('mail', 'yamada@example.com');
fd.set('user', 'Suzuki'); // user をもう一度

for (let d of fd) {
    console.log(`${d[0]}: ${d[1]}`);
}

この結果は次のようになります。

user: Suzuki
mail: yamada@example.com

キー user の値が、最後に呼び出した Suzuki という値で上書きされています。

一方、append() メソッドはエントリーリストにエントリーを追加します。 同じキーのエントリーがあっても上書きしません。

const fd = new FormData();
fd.append('user', 'Yamada');
fd.append('mail', 'yamada@example.com');
fd.append('user', 'Suzuki'); // user をもう一度 append

この結果は次のようになります。

user: Yamada
mail: yamada@example.com
user: Suzuki

キー user の値が、最後に呼び出した Suzuki という値が追加されて、user というキーを持つエントリーが二つ設定されています。

このように set() は同名のキーが既に存在すればその値を上書きし、append() は同名のキーが存在していてもエントリーを追加します。

同じ名前のエントリーが複数あっても大丈夫?

上で append() を使うことで、同じ名前のエントリーが追加されることがわかりました。 この FormData データをサーバーに POST したら、サーバー側ではどのように受け取れるでしょうか。

後述する Node.js のミドルウェア multer を使って、POST データのボディをパースすると、次のように認識されました。

{ user: [ 'Yamada', 'Suzuki' ], ... }

user をキーとするエントリーが複数ありましたが、それらの値は配列の要素として問題なく認識されました。

FormData の Submit

FormData を非同期でサーバーにサブミット (submit) する方法を説明します。

XMLHttpRequest を使って FormData を送信する方法

XMLHttpRequest を使って FormData をサーバーに送信するためには、 FormData オブジェクトを作成したら、send()にそのまま渡せば OK です。

// 'f1' は form 要素に設定した id とします。
const f1 = document.getElementById('f1');
const fd = new FormData(f1);
const xhr = new XMLHttpRequest();

xhr.open("POST", "/test1");
xhr.addEventListener('load', (evt) => {
    // レスポンスが JSON 形式で返っていることを想定
    let response = JSON.parse(xhr.responseText);
    console.log(response);
});
xhr.addEventListener('error', (evt) => {
    // エラー処理
    console.log('** xhr: error');
    console.log(evt);
});

xhr.send(fd);

この場合、Content-Type は自動的に multipart/form-data に設定されます。

Fetch API を使って FormData を送信する方法

fetch() を使って FormData をサブミットするには、 送信オプションの body に FormData オブジェクトをセットします。

// 'f1' は form 要素に設定した id とします。
const f1 = document.getElementById('f1');
const fd = new FormData(f1);

fetch('/test1', {
    method: 'POST',
    body: fd
})
.then((response) => {
    if (!response.ok) {
        throw new Error(`${response.status} ${response.statusText}`);
    }
    // レスポンスが JSON 形式で返っていることを想定
    return response.json();
})
.then((data) => {
    console.log('** fetch: then(data)');
    console.log(data);
})
.catch((reason) => {
    // エラー処理
    console.log('** fetch: catch');
    console.log(reason);
});

fetch でも、Content-Type は自動的に multipart/form-data に設定されます。

fetch() について詳しくは、「fetch の使い方」をみてください。

FormData を用いてファイルをアップロード

HTML の file 型の input 要素をフォームに組み込めば、 フォームの内容がファイルであることを特別に意識することなく、FormData でファイルをアップロードすることができます

enctype は指定しないの?

form 要素のデフォルトの方法で (JavaScript で FormData を使わない方法で) ファイルをアップロードする場合は、明示的に enctype を multipart/form-data を指定する必要があります。

<form enctype="multipart/form-data" method="post" action="...">
<div>input type="file" name="file1"></div>
...

HTML フォームのデフォルトの enctype は "application/x-www-form-urlencoded" です。

しかし、FormData で送信データを作成するときは自動的に multipart/form-data の形式になるので、何も意識しなくてもファイルが送信できます。

FormData を JSON で POST したい場合

上に書いたように FormData を fetch() などでポストするとき、自動的に enctype として multipart/form-data がセットされます。 しかし、単純なキー・値のペアとして、JSON データを作ってポストしたい時もあるかも知れません。

その場合は、Object.fromEntries() メソッドで一旦 Object オブジェクトに変換してから JSON.stringify() する方法もあります。

詳しくは FormData から Object オブジェクトへ変換する をご覧ください。

次の例では FormData を Object.fromEntries() でシンプルなオブジェクトに変換し、それを JSON.stringify() して送信しています。

このときは明示的に Content-Type ヘッダーに application/json をセットする必要があります。

const fd = new FormData(form1)
const obj = Object.fromEntries(fd)

fetch('/test1', {
  method: 'POST',
  body: JSON.stringify(obj),
  headers: {
    'Content-Type': 'application/json',
  },
})
  .then((response) => {
    // ...
    return response.json()
  })
  .then((data) => {
    // ...
  })
  .catch((reason) => {
    // ...
  })

この場合、マルチパートのフォームデータでないため、正統的なフォームベースアップロード、という意味ではファイルのアップロードはできません。 代替案としてはファイルを Base64 でエンコードして、JSON で送信するなどの方法があります。

FormData で Blob をファイルとしてアップロード

file 型の input 要素を使わなくても、 JavaScript で Blob を使うことによって、データをファイルとして POST することも可能です。

次のようにすると、Blob オブジェクトはテキストファイル相当のデータとみなされます。

const f1 = document.getElementById('f1');
const fd = new FormData(f1);
let blob = new Blob(['Sample text'], {
    type: 'text/plain'
});
fd.append('data1', blob, 'foo.txt');

append() メソッドの第三引数がファイル名になります。

次のログは後述の「6-3. Node.js + Express サーバー側での受け取り例」で示したサーバー側のプログラムでデータを受け取り、 コンソールに表示したものです。

確かに Blob で作成したプレーンテキストの部分は、ファイルとして認識されています。

FormData のサンプルコード

ここでは上で説明したことを試すための、サンプルコードを掲載します。

画面は次のような画面です。プログラムの動作は console.log() でデバッガのコンソールに出力していますので、 ブラウザのデバッガの Console をみながら動作確認してください。

UI は Bootstrap で整えています。

また、サーバー側のコードは Node.js と Express を利用しています。

Node.js を使う準備として、適当なフォルダを作成し (ここでは foo というディレクトリとします)、 次のコマンドで package.json を作成してください。

mkdir foo
cd foo
npm init -y

HTML ファイル

foo 内に public という名前のディレクトリを作成して、 その中にindex.html という名前で次の内容を保存します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>FormData Test</title>
    <script src="test.js"></script>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div class="container mt-5">
      <form id="form1">
        <div class="input-group mb-2">
          <input
            type="text"
            name="city"
            value="Los Angeles"
            class="form-control"
          />
        </div>
        <div class="input-group mb-2">
          <input
            type="text"
            name="state"
            value="California"
            class="form-control"
          />
        </div>
        <div class="form-check mb-2">
          <input class="form-check-input" type="checkbox" id="appendBlob" />
          <label class="form-check-label" for="appendBlob">
            Append Blob (File)
          </label>
        </div>
      </form>
      <p>
        <button id="button1" class="btn btn-primary">Fetch TEST</button>
        <button id="button2" class="btn btn-primary">XHR TEST</button>
        <button id="button3" class="btn btn-primary">Fetch (JSON)</button>
      </p>
    </div>
  </body>
</html>

FormData を使う JavaScript の例

同じく public フォルダ内に、test.js という名前で次の内容を保存します。

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

window.addEventListener('load', () => {
  const form1 = $('form1')

  // "Fetch TEST" button
  $('button1').addEventListener('click', (evt) => {
    evt.preventDefault()

    const fd = new FormData(form1)
    if ($('appendBlob').checked) {
      let blob = new Blob(['Sample text'], {
        type: 'text/plain',
      })
      fd.append('data1', blob, 'foo.txt')
    }

    for (let d of fd) {
      console.log(`${d[0]}: ${d[1]}`)
    }

    fetch('/test1', {
      method: 'POST',
      body: fd,
    })
      .then((response) => {
        console.log('** fetch: then(response)')
        if (!response.ok) {
          throw new Error(`${response.status} ${response.statusText}`)
        }
        return response.json()
      })
      .then((data) => {
        console.log('** fetch: then(data)')
        console.log(data)
      })
      .catch((reason) => {
        console.log('** fetch: catch')
        console.log(reason)
      })
  })

  // "XHR TEST" button
  $('button2').addEventListener('click', (evt) => {
    evt.preventDefault()

    const fd = new FormData(form1)
    if ($('appendBlob').checked) {
      let blob = new Blob(['Sample text'], {
        type: 'text/plain',
      })
      fd.append('data1', blob, 'foo.txt')
    }

    const xhr = new XMLHttpRequest()

    xhr.open('POST', '/test1')
    xhr.addEventListener('load', (evt) => {
      let response = JSON.parse(xhr.responseText)
      console.log(response)
    })
    xhr.addEventListener('error', (evt) => {
      console.log('** xhr: error')
      console.log(evt)
    })

    xhr.send(fd)
  })

  // "Fetch (JSON)" button
  $('button3').addEventListener('click', (evt) => {
    evt.preventDefault()

    const fd = new FormData(form1)
    if ($('appendBlob').checked) {
      let blob = new Blob(['Sample text'], {
        type: 'text/plain',
      })
      fd.append('data1', blob, 'foo.txt')
    }

    const obj = Object.fromEntries(fd)

    fetch('/test1', {
      method: 'POST',
      body: JSON.stringify(obj),
      headers: {
        'Content-Type': 'application/json',
      },
    })
      .then((response) => {
        console.log('** fetch: then(response)')
        if (!response.ok) {
          throw new Error(`${response.status} ${response.statusText}`)
        }
        return response.json()
      })
      .then((data) => {
        console.log('** fetch: then(data)')
        console.log(data)
      })
      .catch((reason) => {
        console.log('** fetch: catch')
        console.log(reason)
      })
  })
})

Node.js + Express サーバー側での受け取り例

Web サーバーとしては Node.js で Express を使います。FormData のポストはマルチパートのリクエストになるので、 ボディ部の読み取りには multer を使用しています。

次のコマンドで Express と multer を取り込みます。

npm i express multer

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

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

const multer = require('multer')
const storage = multer.memoryStorage()
const upload = multer({
  storage,
  limits: {
    fields: 10,
    fileSize: 1024, // only 1kb
    files: 1,
  },
}).single('data1')
// const upload = multer({ dest: 'uploads/' })

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

app.post('/test1', (req, res) => {
  if (req.headers['content-type'].startsWith('multipart/form-data')) {
    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('* file')
        console.log(req.file)

        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',
          })
        }
      }
    })
  } else {
    // JSON
    console.log('/test1')
    console.log(req.headers['content-type'])
    console.log('* body')
    console.log(req.body)
    console.log('* file')
    console.log(req.file)

    res.status(200).json({ message: 'OK' })
  }
})

app.listen(3000, () => console.log('Listeninig on port 3000...'))

Visual Studio Code で作業すると、次のようになるでしょう。

上記のコードができたら

node server.js

として、サーバーを開始し、ブラウザから http://localhost:3000/ を要求すると、 上述のテスト用のフォームが表示されるはずです。

以上で、FormData の各種使用方法を、サンプルコードと共に紹介しました。

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

© 2024 JavaScript 入門