さて、前回は色々なHTTP Requestの仕方をご紹介しました。
今回はajaxでファイル送信をする方法について、模索していこうと思います。
まずはajaxとかjavascript無しでファイル送信をする方法をおさらいしましょう。
<form action="file.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="foo" value="fuga" />
<input type="file" name="img" />
<button>送信!</button>
</form>
こんな感じですね。本来ならacceptやら何やら設定すべき項目はあるでしょうけど、まあいいでしょう。
ファイルだけ送信することって実際そこまでない気がするので、一応適当なfoo=fuga
も送信しときます。
今回はjpg画像を送信することを考えます。
POST
Content-Type=multipart/form-data
input[type=file]
でファイル選択今回は、送信されたjpg画像をbase64エンコードしてData URIの形にし、それをsrcとしたimgタグを返すにしましょう。
<?php
if (isset($_FILES['img']) && is_uploaded_file($_FILES['img']['tmp_name'])) {
$img = file_get_contents($_FILES['img']['tmp_name']);
http_response_code(200);
echo '<img src="data:image/jpg;base64,' . base64_encode($img) . '" />';
exit(0);
}
http_response_code(400);
echo "<p>upload failed</p>";
jpgのチェックなどは一旦無しです。jpgが送られることだけを信じているサーバーです。
ファイルアップロードがされない場合は400を返します。
ちなみにさっきのhtmlでフォーム送信してみるとこうなります。
これを非同期処理で行えば、この送信画像のimgタグを取得できますね。
ファイル送信では何が送られているのでしょうか?
普通のフォームではaaa=bbb&ccc=ddd
みたいなリクエストボディとして送られます。この形式のことをapplication/x-www-form-urlencoded
って言いますね。
しかし、ファイル送信時にはformのenctype属性にmultipart/form-data
を指定します。これが一体なんなのか、サーバー側は何を受け取ってどう処理しているのかを知った方が楽しいと思うので、それを調査します。
Content-Type
を見てみます。
multipart/form-data; boundary=----WebKitFormBoundarySlKAOJPci5YYJrxB
Content-Type
がmultipart/form-data
になってますね。formのenctype属性に設定したやつです。
その後にboundary
ってのがあります。これは受け取ったボディはこの文字列で分割しますよーっていう宣言みたいなもんです。
リクエストボディは以下のようになります。
----WebKitFormBoundarySlKAOJPci5YYJrxB
Content-Disposition: form-data; name="img"; filename="emopro.jpg"
Content-Type: image/jpeg
送信ファイルのバイト列.......
----WebKitFormBoundarySlKAOJPci5YYJrxB
Content-Disposition: form-data; name="foo"
fuga
----WebKitFormBoundarySlKAOJPci5YYJrxB--
----WebKitFormBoundarySlKAOJPci5YYJrxB
で分割してみましょう。
Content-Disposition: form-data; name="img"; filename="emopro.jpg"
Content-Type: image/jpeg
送信ファイルのバイト列.......
Content-Disposition: form-data; name="foo"
fuga
上はアップロードした画像について、下はfoo=fuga
のつもりで送ったやつです。
application/x-www-form-urlencoded
のリクエストボディ(aaa=bbb&ccc=ddd
)に比べて随分と様変わりしましたね。
実はこの形式、メールでも見られます。
テキストメッセージと添付ファイルが上記のような方法で送られます。
一応これで、サーバーが何を受け取っているのかを知ることができました。
ここまでわかればajaxで試してみるだけですね。
要はformで送信するのと同様のものを送ればいいんだろって話ですよ。
やってやりましょう。
xhr、jquery、axios、fetchでそれぞれチャレンジしてみます。
まずはこんな画面を用意します。
ボタンを押すとファイル選択ダイアログが表示されます。
送信する画像を選択すると先ほど作ったimgタグ文字列を返すphpに送信されます。
そのimgタグをボタン下に配置することで画像を表示される予定です。
const chooseFile = () =>
new Promise(resolve => {
const input = document.createElement('input')
input.setAttribute('type', 'file')
input.style.display = 'none'
document.body.appendChild(input)
input.addEventListener('change', () => {
const f = input.files[0]
document.body.removeChild(input)
resolve(f)
})
input.click()
})
こんな関数を用意しました。
この関数を呼ぶとファイル選択ダイアログが表示されます。選択したファイルはPromiseのresolveに渡します。
// xhrボタンクリック
btnxhr.addEventListener('click', evt => {
console.log('xhr')
chooseFile().then(f => {
const fr = new FileReader()
fr.onload = ev => {
const xhr = new XMLHttpRequest()
const boundary = 'boundaryyyyyyyyyyy'
xhr.open('post', 'file.php')
xhr.setRequestHeader(
'Content-Type',
'multipart/form-data; boundary=' + boundary
)
xhr.onload = () => {
result.innerHTML = xhr.responseText
}
let before = ''
before += '--' + boundary + '\r\n'
before +=
'Content-Disposition: form-data; name="img"; filename="emopro.jpg"' +
'\r\n'
before += 'Content-Type: image/jpeg' + '\r\n'
before += '\r\n'
let after = '\r\n'
after += '--' + boundary + '\r\n'
after += 'Content-Disposition: form-data; name="foo"' + '\r\n'
after += '\r\n'
after += 'fuga' + '\r\n'
after += '--' + boundary + '--' + '\r\n'
const beforearr = new TextEncoder().encode(before)
const arr = new Uint8Array(fr.result)
const afterarr = new TextEncoder().encode(after)
const body = new Uint8Array(
beforearr.byteLength + arr.byteLength + afterarr.byteLength
)
body.set(beforearr, 0)
body.set(arr, beforearr.byteLength)
body.set(afterarr, beforearr.byteLength + arr.byteLength)
xhr.send(body)
}
fr.readAsArrayBuffer(f)
})
})
どうです?地獄のようでしょう?
一個ずつ処理を追いかけます。
chooseFile
でファイル選択ダイアログを表示FileReder
に食わせて、ArrayBuffer
を取得する。xhr
を生成、boundary
の値を適当に設定(boundaryyyyyyyyyyy
にしてみました)Content-Type=multipart/form-data; boundary=boundaryyyyyyyyyyy
を設定Uint8Array
にするUint8Array
に変換ArrayBuffer
もUint8Array
に変換Uint8Array
を1つに結合リクエストボディは以下の文字列をバイナリにしたものです。
--boundaryyyyyyyyyyy
Content-Disposition: form-data; name="img"; filename="emopro.jpg"
Content-Type: image/jpeg
[画像のバイナリ]
--boundaryyyyyyyyyyy
Content-Disposition: form-data; name="foo"
fuga
--boundaryyyyyyyyyyy--
javascriptはバイナリの扱いが下手くそなので、うまいことやってあげなければなりません。
ちなみにこのリクエストはPOSTの値としてfoo=fuga
も送っています。
結果を見てみましょう。
無事imgタグが書かれて画像が表示されました!
これがformで送信するのと同様のものを愚直に送る方法です。
サーバー側が理解できるモノさえ送ればいいんです。
form使わずに画像送る時っていっつもこんなん書かなきゃならんの?と不安になった方もいるでしょう。
安心してください。簡単な方法あります。
// xhrボタンクリック
btnxhr.addEventListener('click', evt => {
console.log('xhr')
chooseFile().then(f => {
const body = new FormData()
body.append('img', f)
body.append('foo', 'fuga')
const xhr = new XMLHttpRequest()
xhr.open('post', 'file.php')
xhr.onload = () => {
result.innerHTML = xhr.responseText
}
xhr.send(body)
})
})
随分と短くなりましたね!FileReader
に食わせる必要もなく楽チンです。
標準搭載のFormData
というAPIを使用すればこんなに簡単に同じことができます。append
で送りたいものを追加していき、もしその中にファイルが含まれるならば自動でContent-Type
をよしなにしてくれます。
同じ動作をするコードとは全く思ませんね。
// jqueryボタンクリック
btnjquery.addEventListener('click', evt => {
console.log('jquery')
chooseFile().then(f => {
const body = new FormData()
body.append('img', f)
body.append('foo', 'fuga')
$.ajax({
url: 'file.php',
method: 'post',
data: body,
success: data => {
result.innerHTML = data
},
contentType: false,
processData: false,
})
})
})
jqueryはFormDataの送信に対応してくれます。ってか全部内部でxhr使ってるんだからそらそうよ。
ただし$.post
では送れません。少し細やかな設定が必要だからです。
FormData
を送るときはcontentType
とprocessData
をfalseにします。$.post
にはこの設定ができないので、$.ajax
を使用します。
// axiosボタンクリック
btnaxios.addEventListener('click', evt => {
console.log('axios')
chooseFile().then(f => {
const body = new FormData()
body.append('img', f)
body.append('foo', 'fuga')
axios.post('file.php', body).then(resp => {
result.innerHTML = resp.data
})
})
})
まさかaxios
が一番シンプルに書けるとは思いませんでした。
さすがにContent-Type
をfalse
にしたりmultipart/form-data
にしなきゃならないと思っていましたが、何の指定も無くいけちゃいました。axios
愛してるわー。
// fetchボタンクリック
btnfetch.addEventListener('click', evt => {
console.log('fetch')
chooseFile().then(f => {
const body = new FormData()
body.append('img', f)
body.append('foo', 'fuga')
fetch('file.php', {
method: 'post',
body: body,
})
.then(e => e.text())
.then(txt => {
result.innerHTML = txt
})
})
})
こっちもシンプル!こっちももう少し手間がかかるのかなと考えていましたが、意外と簡単な記述で送れるんですね。
今回はmultipart/form-data
で送信しましたが、私はリクエストをapplication/json
にしていることが多いのです。
つまりこのままではファイル送信の時だけ、リクエストの形式が変わってしまうんです。これは良くない!
でも安心してください!方法ならあります!めっちゃ簡単ですよ!
ファイルを文字列にしjsonの値として送っちゃえばいいんです!
具体的には
FileReader
に食わせるreadAsDataURL
でファイルのバイナリをBase64形式で取得するって感じです。readAsDataURL
で取得できる値ってのはdata:image/jpg;base64,[ファイルのBase64エンコードして取得した文字列]
みたいな形式の文字列です。
ただの文字列なんで普通に送れますし、先頭のdata:image/jpg;base64,
を取っ払ってBase64デコードをすれば元のバイナリを取得できます。
今回はなかなか楽しい内容でしたね!multipart/form-data
のリクエストボディ文字列をガリガリ書いてる時はアホなことしてるなーとも思いながら、ちょっと楽しかったですもん。
みなさんも暇だったらサーバー側が理解できるリクエストを手書きしてみるのはいかがでしょうか?
それではまた来週!
コータ=ザッカーバーグ
@kota_zuckerberg
バイクとプログラミングをこよなく愛する編集部の後方支援担当。 愛車はSUZUKI GSR250。 Illustratorの自動化からWEB制作、インフラの整備などをこなしていくうちに いつの間にかフルスタックエンジニアになっちゃった。 主な使用言語はphp, javascript, go, applescript。最近はjsに傾倒ぎみ。