Next.js で Firebase Storage に画像をアップロードする方法を教えて下さい
解決したいこと
React と Next.js、Typsscript について勉強しているのですが、エラーが解決できないので質問しました。
Next.js で、Firebase Storage に画像をアップロードして、その画像を Next.js 側で表示するという Web アプリを作っています。
画像をアップロードする際に、Next.js のpages/api
からアップロードを試みているのですが、エラーが発生してしまい Firebase Storage にアップロードできません。
なお、画像ファイルは base64 でアップロードしようと考えていますが、現状できていません。
Next.js で Firebase Storage に画像をアップロードする方法、もしくは Next.js で画像をアップロードするより良い方法などあれば教えてください。
主なディレクトリ構成
関係がありそうなディレクトリ構成は以下になります。
pages/index.tsx
で画像ファイルをインプットで選択させようとしています。
画像のアップロード関連の処理は、components/Upload.tsx
で行っています。
そして、実際に Firebase Storage に画像を送信する処理は、components/Upload.tsx
からpages/api/post/[date].ts
を使用してアップロードしようと考えています。
root
├ components
│ └ Upload.tsx
├ firebase
│ └ firebase.ts
└ pages
├ api
│ └ post
│ └ [date].js
└ index.tsx
Base64 で画像をアップロードする場合
Base64 形式でアップロードしようとする場合に発生している問題・エラー
ブラウザ側で以下のようなエラーが表示されます。
Upload.tsx:45 POST http://localhost:3000/api/post/2020-04-03 500 (Internal Server Error)
Upload.tsx:55 {statusCode: 500, message: "Firebase Storage: String does not match format 'ba… Invalid character found (storage/invalid-format)"}message: "Firebase Storage: String does not match format 'base64': Invalid character found (storage/invalid-format)"statusCode: 500__proto__: Object
Base64 形式でアップロードしようとする場合の該当するソースコード
pages/index.tsx
import React from "react";
import Link from "next/link";
import Layout from "../components/Layout";
import Upload from "../components/Upload";
import { GetStaticProps } from "next";
import { db } from "../firebase/firebase";
type ItemDocs = {
date: string;
};
const IndexPage = () => (
<Layout title="画像アップローダー">
<h1>画像アップローダー</h1>
<Upload />
<p>
<Link href="/picture-book">
<a>写真館</a>
</Link>
</p>
</Layout>
);
Components/Upload.tsx
ImageRender(imageRender)部分で、pages/api/post/[date].js
を呼んでいます。
import React, { useState } from "react";
import Image from "next/image";
type ImageData = {
imageData: string;
};
const Upload = () => {
// 選択された画像データを保持しておくState
const [imageData, setImageData] = useState<string>("");
// 「ファイルを選択」ボタンが押されたら発火する
const handleImageChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
// 選択された画像の情報が格納される
const files = event.target.files;
// ファイルが読み込まれたら読み込み作業を実施
if (files && files.length > 0) {
const file = files[0];
// 画像読み込みリーダーのインスタンス生成
const reader = new FileReader();
reader.onload = (event) => {
if (event.target && event.target.result)
setImageData(event.target?.result.toString());
};
// HTML 側で渡された画像を読み込む
reader.readAsDataURL(file);
} else {
setImageData("");
}
};
// 選択した画像を表示させるためのプレビュー用コンポーネント
const ImageRender = (imageData: ImageData) => {
// imageDate のオブジェクトに base64 形式のデータが入っているか確認
if (Object.values(imageData)[0] === "") return null;
// base64 データがあれば、表示させる。
else {
// 画像をアップロードする関数
const onUploadImage = async () => {
// firestore, storage for firebase に API を経由して情報を登録
// ★★★★ここからがアップロード処理部分★★★★
const res = await fetch("api/post/2020-04-03", {
body: JSON.stringify({
filename: Object.values(imageData)[0],
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const result = await res.json();
console.log(result);
console.log("あっぷろーど!");
};
return (
<>
<Image src={imageData.imageData} width={200} height={200} />
<div>
<button onClick={onUploadImage}>アップロード!</button>
</div>
</>
);
}
};
// メインレンダー部分
return (
<>
<input
type="file"
accept="image/jpeg, image/png"
onChange={(event) => handleImageChanged(event)}
/>
<div>
<ImageRender imageData={imageData} />
</div>
</>
);
};
export default Upload;
pages/api/post/[date].ts
こちらでは、Firestore にも別データを保存しようとしており、Firestore へのデータ登録は正常に行われています。
また、firebase.storage().child('images')
のについても取得出来ていることから、firebase への接続、また storage への接続は行えていると考えています。
import { NextApiRequest, NextApiResponse } from "next";
import { db, storage } from "../../../firebase/firebase";
// Upload.tsx コンポーネントから、firebase の firestore にアップロードする API
const handler = (req: NextApiRequest, res: NextApiResponse) => {
// /api/post/[date] で date を受け取り、リクエストに設定する
const {
query: { date },
} = req;
try {
// storage for firebase に画像をアップロードする
const [imageType, rawBase64Data] = req.body.filename.split(",");
console.log(imageType); // -> data:image/png;base64
console.log(rawBase64Data.substr(0, 50)); // -> iVBORw0KGgoAAAANSUhEUgAAAhUAAAIYCAYAAADaeh6gAAAABH
// ★★★★ ここでfirebase storage に画像データを送信 ★★★★
storage
.ref()
.child("images/2021-03-23.jpg")
.putString(rawBase64Data, "base64")
.then((_snapshot) => {
console.log("Uploaded a data_url string!");
});
// firestore の images というコレクションにデータをアップロード
db.collection("images").doc(`${date}`).set({
path: "qwerqwer",
});
} catch (err) {
res.status(500).json({ statusCode: 500, message: err.message });
}
};
export default handler;
自分で試したこと
まずは Firebase のドキュメントを見ながら行ったのですが、うまくアップロードすることができませんでした。
また、エラーメッセージが以下のように表示されていたため、putString()
の引数に入れる値を、データのみにしてみたのですが、それでもうまくアップロードすることはできませんでした。
- (旧):data:image/png;base64, iVBORw0KGgoAAAANSUhEU ... gU==
- (新):iVBORw0KGgoAAAANSUhEU ... gU==
"Firebase Storage: String does not match format 'base64': Invalid character found (storage/invalid-format)"
Blob で行ってみようと思って Components/Upload.tsx
とpages/api/post/[date].ts
の一部を以下のように変えて試してみたのですが、こちらもうまくいきませんでした。
- Components/Upload.tsx
import React, { useState } from 'react';
...
const Upload = () => {
// 選択された画像データを保持しておくState
const [preview, setPreview] = useState('');
const [imageData, setImageData] = useState<File | null>(null);
// 「ファイルを選択」ボタンが押されたら発火する
const handleChangeFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const { files } = event.target;
if (files) {
setPreview(window.URL.createObjectURL(files[0]));
setImageData(files[0]);
}
};
// 選択した画像を表示させるためのプレビュー用コンポーネント
const ImageRender = (preview: PreviewProps) => {
// imageDate のオブジェクトにデータが入っているか確認
if (Object.values(preview)[ZERO] === EMPTY_STRING) return null;
// データがあれば、表示させる。
else {
// 画像をアップロードする関数
const onUploadImage = async () => {
const response = await fetch(preview.preview);
const blob = await response.blob();
// firestore, storage for firebase に API を経由して情報を登録
const res = await fetch('api/post/2020-04-03', {
body: blob,
headers: {
'Content-Type': 'image/png',
},
method: 'POST',
});
// const result = await res.json();
// console.log(result);
console.log('あっぷろーど!');
};
return (
<>
<img src={preview.preview} />
<div>
<button onClick={onUploadImage}>アップロード!</button>
</div>
</>
);
}
};
...
export default Upload;
pages/api/post/[date].ts
import { NextApiRequest, NextApiResponse } from 'next';
...
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// /api/post/[date] で date を受け取り、リクエストに設定する
const {
query: { date },
} = req;
try {
storage
.ref()
.child('images/2021-03-23.jpg')
// .putString(rawBase64Data, 'base64')
.put(req.body, { contentType: 'image/png' })
.then((_snapshot) => {
console.log('Uploaded a data_url string!');
});
...
} catch (err) {
res.status(500).json({ statusCode: 500, message: err.message });
}
};
...
export default handler;