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);
このとき、input、select などのフォームの入力フィールドが自動的に取り込まれます。 しかし、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 の各種使用方法を、サンプルコードと共に紹介しました。