画像をbase64エンコードしてアップロードするプログラムを実装したい、けど全然わからないや。
大丈夫!わかりやすく解説するね!
画像をAPI経由で送信し、サーバーやS3に保存する処理の実装は実務でもよくあると思います。
方法としては下記の2種類があります。
- formDataで送る形式
- base64エンコードして送る形式
今回はbase64エンコード形式で、実装方法を説明します!
formData形式を見たい方はこちら↓
サンプルアプリのgithub
こちらのbase64ディレクトリにコードが入っています。
READMEに起動手順が書いてあるので、クローンしたら起動してみてください。
サンプルアプリの構成
下記の構成で説明します。
- フロントエンド React
- バックエンド Express
アプリの説明
画面はこんな感じです。
画像を選択してみるとプレビューが出ます↓
送信ボタンを押すと、アップロードされサーバー側のディレクトリに保存されます。
起動中のExpress側のターミナルに、アップロードしたファイルの情報が文字列で表示されます。
base64エンコードって何?
画像などのバイナリデータを、base64と呼ばれる文字列形式に変換することです。
画像を文字列に変えているんだな、くらいの理解で大丈夫です。
実装手順としてはこんな感じです👇
- ブラウザ上で、画像をbase64形式の文字列にする。
- ブラウザからサーバーに、base64形式に変換した文字列形式で画像を送信
- サーバー側で受け取った文字列を、元の画像データに戻す
- 画像データをディレクトリやS3に保存する
フロント側のアップロード処理
ImageUploadFormコンポーネントに処理が書いてあります。
import React, { useState, ChangeEvent } from "react";
import "./ImageUploadForm.css";
const ImageUploadForm: React.FC = () => {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [submissionMessage, setSubmissionMessage] = useState<string | null>(
null
);
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
setSelectedImage(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const handleSubmit = () => {
// サーバーへの送信ロジック
fetch("http://localhost:3000/upload", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image: selectedImage,
}),
})
.then((response) => response.json())
.then((data) => {
console.log(data);
setSubmissionMessage("アップロードに成功しました!");
})
.catch((error) => {
console.error(error);
setSubmissionMessage(
"アップロードに失敗しました。エラーが発生しました。"
);
});
};
return (
<div className="image-upload-form">
<input
type="file"
id="image-input"
accept="image/*"
onChange={handleImageChange}
/>
{selectedImage && (
<div className="preview-container">
<img src={selectedImage} alt="Preview" className="preview-image" />
</div>
)}
<button onClick={handleSubmit}>送信</button>
{submissionMessage && <p>{submissionMessage}</p>}
</div>
);
};
export default ImageUploadForm;
handleImageChangeで、アップロードする画像を選択した時の処理、
handleSubmith関数で画像の送信処理をしています。
画像選択時
input type=”file”のonChangeに指定している関数を見てみます。
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
setSelectedImage(reader.result as string);
};
reader.readAsDataURL(file);
}
};
引数で渡ってきたイベントハンドラの、e.target.files
に、配列形式で選択した画像の情報が渡ってきます。
試しに、e.target.filesをコンソールで見てみます。
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.files);
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
setSelectedImage(reader.result as string);
};
reader.readAsDataURL(file);
}
};
次に、FileReaderでファイルを読み取ります。
読み取り完了時(render.onload)に、render.resultで、読み取り結果を取得しています。
今回はreader.readAsDataURLを使用し、base64エンコードされた文字列形式で読み取ります。
これで画像をbase64文字列に変換することができます。
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result);
setSelectedImage(reader.result as string);
};
reader.readAsDataURL(file);
}
};
reader.resultをコンソールに出力してみます。
このように、base64エンコードされた画像のデータが出力されました。
この文字列をステートとして管理し、imgタグのsrc属性に当てることで画像を表示できます。
あとは、この文字列をサーバーに送ります。
画像送信時
送信ボタンを押した時の処理はこちらです↓
const handleSubmit = () => {
// サーバーへの送信ロジック
fetch("http://localhost:3000/upload", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image: selectedImage,
}),
})
.then((response) => response.json())
.then((data) => {
console.log(data);
setSubmissionMessage("アップロードに成功しました!");
})
.catch((error) => {
console.error(error);
setSubmissionMessage(
"アップロードに失敗しました。エラーが発生しました。"
);
});
};
こちらは特別なことはしていません。
文字列になったので、テキスト情報を送るのと同じように送ればOKです。
あとはリクエストヘッダーに
"Content-Type": "application/json"
を指定するのを忘れないようにしてください!
フロントエンドの実装はこれで終わりです!
API側のアップロード処理
主要な処理はこのようになっています。
app.post('/upload', async (req, res) => {
const base64Data = req.body.image;
// 先頭のdata:image/png;base64を除いたもの
const fileData = base64Data.replace(/^data:\w+\/\w+;base64,/, '');
// 拡張子を取得、/と;の間にある部分をsliceする
const fileExtension = base64Data
.toString()
.slice(base64Data.indexOf('/') + 1, base64Data.indexOf(';'));
// 注意:実際には送られたファイルの検証をしますが、ここでは省いています。
const binaryData = Buffer.from(fileData, 'base64');
fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, binaryData);
// または下記で、デコードとファイルの書き込みを一発でできる
// fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, fileData, {
// encoding: 'base64'
// });
res.json(`アップロード成功!`);
});
まずはリクエストの受け取り部分です👇
const base64Data = req.body.image;
リクエストbodyにあるbase64エンコードされた画像を取得しています。
ブラウザでコンソールに出力したものが、
Express側のコンソールで受け取れました👇
画像形式に復元したいのですが、data:image/png;base64,の部分は不要です。
そこで、正規表現を使ってトリミングしています。
const fileData = base64Data.replace(/^data:\w+\/\w+;base64,/, '');
fileDataにはカンマ以降の値が入ります。
これを
const binaryData = Buffer.from(fileData, 'base64');
で、base64文字列からバイナリデータに復元します。
また、画像にはpngやjpegなどの拡張子があります。
data:image/png;base64の部分から、拡張子「png」を取得するコードがこちら👇
const fileExtension = base64Data
.toString()
.slice(base64Data.indexOf('/') + 1, base64Data.indexOf(';'));
toString()で、TypeScript上で文字列型として扱えるようにし、sliceで「/」と「;」の間を切り取っています。
これで拡張子が判別できました。
あとはアップロードして終わりです。
fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, binaryData);
ファイル名はrandomUUID()で、ランダムな値にしています。
または、writeFileSyncでエンコード方式を指定し、デコードとファイルの書き込みの2行分を1つにまとめることができます。
// 2行で書いた場合
const binaryData = Buffer.from(fileData, 'base64');
fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, binaryData);
// または下記で、デコードとファイルの書き込みを一発でできる
fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, fileData, {
encoding: 'base64'
});
フロントエンドからbase64文字列形式で画像を送信する説明は以上です!
formDataの実装より難しいですが、こちらで送信するケースもあると思います。
どのような処理が必要か、流れを押さえておけば実務では焦る心配はないでしょう👍