🔥

【Next.js】Firebase Storageに画像をアップしてURLを取得する

12 min read

経緯

Firebase Storageを初めて使ってみて,とても使いやすいと感じたので共有したいと思いました.
Next.jsをフレームワークとして利用しています.


環境

react@17.0.2
firebase@8.6.8
typescript@4.3.4
next@11.0.1
tailwindcss@2.2.4

事前準備

事前にFirebaseプロジェクトを作成し,Reactのfirebase.jsなどにfirebaseの設定を記述してください.また

npm install firebase 
or
yarn add firebase

によりfirebaseをインストールしてください.

firebase.js
import firebase from 'firebase/app';
import 'firebase/storage';

const firebaseConfig = {
  // config
  apiKey: 'API_KEY',
  authDomain: 'AUTO_DOMAIN',
  projectId: 'PROJECT_ID',
  storageBucket: 'STORAGE_BUCKET',
  messagingSenderId: 'MESSAGING_SENDER_ID',
  appId: 'APP_ID',
  measurementId: 'MEASUREMENT_ID'
};
// Initialize Firebase
if (firebase.apps.length === 0) {
  firebase.initializeApp(firebaseConfig);
}
export const storage = firebase.storage();
export default firebase;

セキュリティルール

firebaseのセキュリティルールは動くかどうかを試すときはread, writeどちらともtrueに設定してください.試しが終わったら適宜セキュリティルールを設定してください.

Uploadコンポーネント

今回は画像をアップロードするコンポーネントを作成するので使いどころは適宜プロジェクトにより決めてください.関数や機能を他のファイルなどに切り分けても良いと思います.むしろその方がコードの可読性が上がりますが,今回は画像をアップロードするコンポーネントということにしたかったので全て1ファイルに記述しました.

Upload.tsx
import React, { useState, useCallback, useContext } from 'react';

import { useDropzone } from 'react-dropzone';
import firebase, { storage } from '../firebase/firebase';

export type firebaseOnLoadProp = {
  bytesTransferred: number;
  totalBytes: number;
  state: firebase.storage.TaskState;
  // このほかにもmetadata,task,refがある
};

const Upload: React.FC = () => {
  const [myFiles, setMyFiles] = useState<File[]>([]);
  const [clickable, setClickable] = useState(false);
  const [src, setSrc] = useState('');

  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    if (!acceptedFiles[0]) return;

    try {
      setMyFiles([...acceptedFiles]);
      setClickable(true);
      handlePreview(acceptedFiles);
    } catch (error) {
      alert(error);
    }
  }, []);

  const onDropRejected = () => {
    alert('画像のみ受け付けることができます。');
  };

  const { getRootProps, getInputProps } = useDropzone({
    onDrop,
    onDropRejected
  });

  const handleUpload = (accepterdImg: any) => {
    try {
      // アップロード処理
      const uploadTask: any = storage
        .ref(`/images/${myFiles[0].name}`)
        .put(myFiles[0]);

      uploadTask.on(
        firebase.storage.TaskEvent.STATE_CHANGED,
        function (snapshot: firebaseOnLoadProp) {
          const progress: number =
            (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
          console.log('Upload is ' + progress + '% done');
          switch (snapshot.state) {
            case firebase.storage.TaskState.PAUSED: // or 'paused'
              console.log('Upload is paused');
              break;
            case firebase.storage.TaskState.RUNNING: // or 'running'
              console.log('Upload is running');
              break;
          }
        },
        function (error: any) {
          // 失敗した時
          switch (error.code) {
            case 'storage/unauthorized':
              // User doesn't have permission to access the object
              console.error('許可がありません');
              break;

            case 'storage/canceled':
              console.error('アップロードがキャンセルされました ');
              // User canceled the upload
              break;

            case 'storage/unknown':
              console.error('予期せぬエラーが発生しました');
              // Unknown error occurred, inspect error.serverResponse
              break;
          }
        },
        function () {
          // 成功した時
          try {
            uploadTask.snapshot.ref
              .getDownloadURL()
              .then(function (downloadURL: string) {
                console.log('ダウンロードしたURL' + downloadURL);
              });
          } catch (error) {
            switch (error.code) {
              case 'storage/object-not-found':
                console.log('ファイルが存在しませんでした');
                break;
              case 'storage/unauthorized':
                console.log('許可がありません');
                break;
              case 'storage/canceled':
                console.log('キャンセルされました');
                break;
              case 'storage/unknown':
                console.log('予期せぬエラーが生じました');
                break;
            }
          }
        }
      );
    } catch (error) {
      console.log('エラーキャッチ', error);
    }
  };

  const handlePreview = (files: any) => {
    if (files === null) {
      return;
    }
    const file = files[0];
    if (file === null) {
      return;
    }
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      setSrc(reader.result as string);
    };
  };

  return (
    <div>
      <div className="w-4/5 px-4 py-2 mx-auto my-4 text-center rounded-md">
        <div
          className="bg-gray-400 border-2 border-gray-500 rounded-md"
          {...getRootProps()}
        >
          {/* この中をタップすれば画像を選択できる */}
          <input {...getInputProps()} />
          {myFiles.length === 0 ? (
            <p className="py-4">画像を選択またはドラッグ&ドロップできます</p>
          ) : (
            <div>
              {myFiles.map((file: File) => (
                <React.Fragment key={file.name}>
                  {src && <img src={src} />}
                </React.Fragment>
              ))}
            </div>
          )}
        </div>
        <button
          disabled={!clickable}
          type="submit"
          className="px-4 py-2 my-4 bg-gray-200 rounded-md"
          onClick={() => handleUpload(myFiles)}
        >
          UPLOAD
        </button>
      </div>
    </div>
  );
};
export default Upload;

Upload.tsxの説明

行数が多いので分けて説明して行こうと思います.

Upload.tsx
export type firebaseOnLoadProp = {
  bytesTransferred: number;
  totalBytes: number;
  state: firebase.storage.TaskState;
  // このほかにもmetadata,task,refがある
};

Firebase Storageのアップロード状況を知る時に使用するtypeです.
詳しくはここに書かれています.


Upload.tsx
  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    if (!acceptedFiles[0]) return;

    try {
      setMyFiles([...acceptedFiles]);
      setClickable(true);
      handlePreview(acceptedFiles);
    } catch (error) {
      alert(error);
    }
  }, []);

  const onDropRejected = () => {
    alert('画像のみ受け付けることができます。');
  };

  const { getRootProps, getInputProps } = useDropzone({
    onDrop,
    onDropRejected
  });

画像をドロップした時に処理をする関数です.
onDropRejectedはドロップが失敗した時に実行されるので,そこはいろいろカスタマイズできると思います.


Upload.tsx
const handleUpload = (accepterdImg: any) => {
    try {
      // アップロード処理
      const uploadTask: any = storage
        .ref(`/images/${myFiles[0].name}`)
        .put(myFiles[0]);

      uploadTask.on(
        firebase.storage.TaskEvent.STATE_CHANGED,
        function (snapshot: firebaseOnLoadProp) {
          const progress: number =
            (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
          console.log('Upload is ' + progress + '% done');
          switch (snapshot.state) {
            case firebase.storage.TaskState.PAUSED: // or 'paused'
              console.log('Upload is paused');
              break;
            case firebase.storage.TaskState.RUNNING: // or 'running'
              console.log('Upload is running');
              break;
          }
        },
        function (error: any) {
          // 失敗した時
          switch (error.code) {
            case 'storage/unauthorized':
              // User doesn't have permission to access the object
              console.error('許可がありません');
              break;
	      
            case 'storage/canceled':
              console.error('アップロードがキャンセルされました ');
              // User canceled the upload
              break;

            case 'storage/unknown':
              console.error('予期せぬエラーが発生しました');
              // Unknown error occurred, inspect error.serverResponse
              break;
          }
        },
        function () {
          // 成功した時
          try {
            uploadTask.snapshot.ref
              .getDownloadURL()
              .then(function (downloadURL: string) {
                console.log('ダウンロードしたURL' + downloadURL);
              });
          } catch (error) {
            switch (error.code) {
              case 'storage/object-not-found':
                console.log('ファイルが存在しませんでした');
                break;
              case 'storage/unauthorized':
                console.log('許可がありません');
                break;
              case 'storage/canceled':
                console.log('キャンセルされました');
                break;
              case 'storage/unknown':
                console.log('予期せぬエラーが生じました');
                break;
            }
          }
        }
      );
    } catch (error) {
      console.log('エラーキャッチ', error);
    }
  };

画像のアップロードを実行する関数です.
今回は/imagesというディレクトリ以下に画像を保存していくようにしています.

uploadTask.onはここでは4つの引数をとっています.

  • 発火させる条件
  • 発火した時に実行する関数
  • アップロードが失敗した時に実行する関数
  • 成功した時に実行する関数

それぞれ解説していきます.

発火させる条件

ここではタスクイベントの状態が変化した時としています.

発火した時に実行する関数

ここでは現在何%アップロードされているかを表示するようにしています.
bytesTransferredtotalBytesを使って自分でローディングイベントを作成しても良いと思います.

アップロードに失敗した時に実行する関数

ここではエラーコードを読み取りそれに合わせてメッセージを表示しています.
エラーの種類は記載している他にもここに書いてあります.

アップロードに成功した時に実行する関数

ここでは成功するとその画像が挙げられているURLを取得するようにしています.またダウンロード時にエラーが起きた場合の処理も書いています.
エラーの種類はにもあります.


Upload.tsx
const handlePreview = (files: any) => {
    if (files === null) {
      return;
    }
    const file = files[0];
    if (file === null) {
      return;
    }
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      setSrc(reader.result as string);
    };
  };

  return (
    <div>
      <div className="w-4/5 px-4 py-2 mx-auto my-4 text-center rounded-md">
        <div
          className="bg-gray-400 border-2 border-gray-500 rounded-md"
          {...getRootProps()}
        >
          {/* この中をタップすれば画像を選択できる */}
          <input {...getInputProps()} />
          {myFiles.length === 0 ? (
            <p className="py-4">画像を選択またはドラッグ&ドロップできます</p>
          ) : (
            <div>
              {myFiles.map((file: File) => (
                <React.Fragment key={file.name}>
                  {src && <img src={src} />}
                </React.Fragment>
              ))}
            </div>
          )}
        </div>
        <button
          disabled={!clickable}
          type="submit"
          className="px-4 py-2 my-4 bg-gray-200 rounded-md"
          onClick={() => handleUpload(myFiles)}
        >
          UPLOAD
        </button>
      </div>
    </div>
  );

プレビューする時に行う処理と画面にレンダーする要素を記述しています.
React.Fragmentの中に画像を表示しています.

参考

https://qiita.com/JSON_HardCoder/items/8d1f9b4bdd578076595d

https://firebase.google.com/docs/storage/web/upload-files?hl=ja