React + Cloud Functionsで、Cloud Storageに画像をアップロード
はじめに
いくつかハマった点があったのでメモします。
たまたま同じことで詰まっている方のご参考になれば、なお嬉しいです。
前提条件
すでに、以下の作業をしていることを前提として話を進めていきます。
・React(Typescript)プロジェクトを作成している
・Firebaseにプロジェクトを作成して、アプリを登録している
・Firebaseプロジェクトに、Cloud functions、Cloud Storage、エミュレータを追加している
本記事は、画像アップロードに主眼を置いているため、上記の過程の説明は割愛させていただきます。
本題
ここでは、画像をCloud Storageに追加するシステムの実装を仮定して、説明をしていきます。
ディレクトリ構造は、以下のようにします。
project
├── frontend(フロントエンド側)
└──src
└──Firebase.ts(Firebase設定ファイル)
└──index.tsx(エントリーポイント)
└──views
└──Test.tsx(画像アップロード用コンポーネント)
└──.env(Firebase設定用の機密情報)
└──.gitignore(git管理下から外すリスト)
└──build
└──node_modules
└──package.json
└──tsconfig.json
│
│
├── functions(Cloud Functions側)
│ └──src
│ └──index.ts(関数の定義場所)
├── firebase.json(ここで、StorageやFunctionsなどの各サービスのホスト名やポートを設定する)
├── storage.rules
まずは、クライアント側から実装を進めてまいりましょう。
クライアント側からCloud Functionsの関数を呼び出すためには、Firebase SDKを使えるようにする必要があります。
以下のようなFirebase設定ファイルを記述します。
import firebase from 'firebase';
/**
* firebase接続のための設定
*/
const config = {
// Firebaseにアプリを登録した際に、Firebase SDK snippetに記載されていた情報を、それぞれ当てはめる。
// ただし、ApiKeyなどの値は、公開してはいけない情報なので、プロジェクト下に「.env」ファイルを作成して、そこに値を記述しておく。.
// .envファイル自体は、gitignoreでgit管理下に置かないようにする
// そして、以下のように.envファイルに設定した環境変数を呼び出す。
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID
};
firebase.initializeApp(config);
// エミュレータを使う場合、useEmulatorをする必要がある
const functions = firebase.functions();
functions.useEmulator("0.0.0.0", 5001);
const storage = firebase.storage();
storage.useEmulator("0.0.0.0", 9199);
export default firebase;
ターミナルのprojectディレクトリにて、
npx firebase emulators:start
を実行すれば、エミュレータが立ち上がるはずです。
続いて、画像アップロード用のコンポーネントを記述していきます。
import React, { useState } from "react";
import firebase from './Firebase';
/**
* クライアント側の画像アップロード用のコンポーネント
* @returns 画像アップロードフォームが画面に表示される
*/
export const Test = () => {
const [imageFile, setImateFile] = useState<File | null>()
const submit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
/**
* 画像ファイルが空の場合、アラートを出すだけで、何もせずに返す
*/
if (!imageFile)
{
alert("画像ファイルが入力されていません")
return
}
// 画像ファイルをBase64形式に変換するために、FileReaderを使う
const reader = new FileReader();
/**
* readerによってimageFileが読み込まれる度に、onloadが発動する
*/
reader.onload = async() => {
/**
* DataURLに変換された画像データから、余分な箇所を削ぎ落とす
*/
const imageBase64Data = reader.result?.toString()?.replace(/data:.*\/.*;base64,/, '');
// Cloud Functionsに送るデータ
const requestData = { imageBase64Data : imageBase64Data, imageFileName: imageFile?.name }
try
{
const httpsCallable = firebase.functions().httpsCallable("updateImage");
await httpsCallable(requestData)
console.log("画像をアップロードしました");
}
catch(error)
{
console.log(error);
alert("画像のアップロードに失敗しました");
}
}
/**
* 画像ファイルをDataURLに変換
*/
reader.readAsDataURL(imageFile);
}
return (
<div>
{/* 画像アップロード用のフォーム */}
<form onSubmit={submit}>
<input multiple id="imageFile" name="imageFile" type="file" accept="image/jpeg, image/png" onChange={(e) => setImateFile(e.target.files?.[0])}/>
<button>アップロード</button>
</form>
</div>
)
}
上記のコンポーネントを、ルートで表示されるようにします。
import React from 'react';
import ReactDOM from 'react-dom';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { Switch, Route } from 'react-router-dom';
import { Test } from './views/Test';
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Switch>
<Route exact path="/" component={Test}></Route>
</Switch>
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
さて、次はFunctions側の実装をしていきます。
Test.tsxのhttpsCallableで呼び出した関数を、ここで定義します
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp(functions.config().firebase)
const db = admin.firestore();
const storage = admin.storage();
exports.updateImage = functions.https.onCall(async (data) => {
// 画像をBase64からBufferに変換する
const buffer = Buffer.from(data.imageBase64Data, "base64")
// Cloud Storageへの画像の追加
const bucketFilePath = `Images/${data.imageName}`
const file = storage.bucket().file(bucketFilePath)
await file.save(buffer);
})
これで、仮定したものが完成したはずです。
projectディレクトリで、
npx firebase emulators:start
を実行して、エミュレータを起動し、ルートにアクセスすると、画像アップロードフォームが表示されるはずです。
まとめ
駆け足になりましたが、いかがだったでしょうか。
本記事で紹介したものの他、Authenticationで認証をしたり、Hostingでデプロイしたりすると、より本格的なアプリに近づいていきます。
Discussion