masalibの日記

システム開発、運用と猫の写真ブログです

React Firebase入門 アバター変更:基礎

アバターを登録できるようにします。このアバターがないとChatアプリを作った時に悲しい😢ことになりますwww。システム的には必須ではない機能です。

React Firebase入門シリーズ
1-1・ React Firebase入門 初期設定とsignup - masalibの日記
1-2・ 「react-hook-form」を入れてみた - masalibの日記
1-3・ React Firebase入門 signupの通信エラー対応 - masalibの日記
2・ React Firebase入門 ログイン処理 - masalibの日記
3・ React Firebase入門 ログイン認証とログアウト処理 - masalibの日記
4・ React Firebase入門 パスワード初期化処理 - masalibの日記
5・ React Firebase入門 メールアドレスの有効化 - masalibの日記
6・ React Firebase入門 メールアドレスとパスワード変更 - masalibの日記
7・ React Firebase入門 表示名変更 - masalibの日記
8-1・React Firebase入門 アバター変更:基礎 今ここ

アバターとは

アバター(photoURL)というとわかりにくいのですが、

f:id:masalib:20201129181843p:plain

になります。

アバターがない場合はハンドル名の1文字を使うとう方式があるのでこの機能も必須ではありません

事前作業

Firebase Storageを使うためには管理画面で初期設定をしないといけない

  1. Firebaseのプロジェクト画面からCloud Storageを作成します。
  2. 権限に関する注意が出てきます。そのまま「次へ」ボタンでOK。
  3. Cloud Storageのロケーションを選択します。ロケーションは後から変更できないので注意が必要ですが、とりあえず近い場所(asia northeast)を選択しておきます。
    4.プロジェクトにあわせた権限に変更をする
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /images/users/{userId}/{imageId} {
             allow read;
        allow write: if request.auth.uid == userId;
        allow delete: if request.auth.uid == userId;
    }
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }

  }
}

今の所はimagesしか使わないからこんな形にしました。もう少しセキュリティルールの事を勉強した方がいいかも・・・

更新処理

updateProfileは前回の記事で作っているので今回は何も触らないです。

React Firebase入門 表示名変更 - masalibの日記

プロフィール変更画面の変更

ソース

全文をみたいひとはこちらから

https://github.com/masalib/Learn-Firebase/blob/updateProfire_image/src/components/UpdateProfile.js

ソース解説

State関連の修正

入力した内容をStateに保管したかったので色々と追加

初期state

 let initialState: State = {
  username: "",
  password: "",
  passwordconfirm: "",
  displayName: "",
+   photoURL: "",
  isButtonDisabled: true,
  helperText: "",
  isError: false
};

stateの型

type State = {
  username: string,
  password:  string,
  passwordconfirm:  string,
  displayName:  string,
+   photoURL:  string,
  isButtonDisabled: boolean,
  helperText: string,
  isError: boolean
};

useReducerの実行されるreducer関数の修正

type Action =
  | { type: "setUsername", payload: string }
  | { type: "setPassword", payload: string }
  | { type: "setPasswordConfirm", payload: string }
  | { type: "setDisplayName", payload: string }
+   | { type: "setphotoURL", payload: string }
  | { type: "setIsButtonDisabled", payload: boolean }
  | { type: "signupSuccess", payload: string }
  | { type: "signupFailed", payload: string }
  | { type: "setIsError", payload: boolean };

useReducerの実行されるActionを追加

    case "setDisplayName":
    return {
        ...state,
        displayName: action.payload
    };
+     case "setPhotoURL":
+     return {
+         ...state,
+         photoURL: action.payload
+     };
    case "setIsButtonDisabled":
      return {
        ...state,
        isButtonDisabled: action.payload
      };

画像追加フィールドを追加

<Typography className={classes.subtitle2} variant="subtitle2">アバター</Typography>
<Paper elevation={1} justify="center">
    <input
        accept="image/*"
        className={classes.inputFile}
        id="contained-button-file"
        multiple
        type="file"
        onChange={handleImage}
    />
    <label htmlFor="contained-button-file">
        {state.photoURL && <div><img className={classes.imagephotoURL} src={state.photoURL} alt="photoURL" /><Fab component="span" className={classes.button}><AddPhotoAlternateIcon /></Fab></div>}
        {!state.photoURL && <>アバターが登録されていません{' '}{' '}{' '}<Fab component="span" className={classes.button}><AddPhotoAlternateIcon /></Fab></>}
        
    </label>
</Paper>

画像がある時とない時で出力する内容を切り替えました。
reactのJSXを触った事がない人だとわかりにくいのですが

{state.photoURL && <>何か入っている場合</>}
{!state.photoURL && <>何も入っていない場合</>}

上記のように切り替え(IF文)になっています。 単純にinputタグだとマテリアルデザインじゃないみたいなのでアイコンを押してファイル選択をさせています

具体的には

<label htmlFor="contained-button-file">
 XXXX
</label><input
 id="contained-button-file"
 type="file"
 onChange={handleImage}
/>

  labelの範囲をクリックするとhtmlForで示されているidを同じ動きになります

ファイルを選択された時にonChangeで動く関数を追加

const handleImage = event => {
        const image = event.target.files[0];
        
        // アップロード処理
        console.log("アップロード処理");
        const storages = app.storage();//storageを参照
        const storageRef = storages.ref("images/users/" + currentUser.uid + "/");//どのフォルダの配下に入れるか
        const imagesRef = storageRef.child("profilePicture.png");//ファイル名

        console.log("ファイルをアップする行為");
        const upLoadTask = imagesRef.put(image);

        console.log("タスク実行前");

        upLoadTask.on(
            "state_changed",
            (snapshot) => {
                console.log("snapshot", snapshot);
            },
            (error) => {
                console.log("err", error);
            },
            () => {
                upLoadTask.snapshot.ref.getDownloadURL().then((downloadURL) => {
                    const url = new URL(downloadURL)
                    console.log(url.href + url.pathname + "?alt=media")
                    dispatch({
                        type: "setPhotoURL",
                        payload: url.href + url.pathname + "?alt=media"
                    });
                });
            }
        );
    };

const image = event.target.files[0]はファイル選択で選ばれたファイルになります。

const storages = app.storage();//Firebaseのstorageを参照するために宣言しています。
import app from "../firebase"はFirebaseの設定の全体です。
本当は

import {storage}  from "../firebase"

で宣言して使いたかったが、なぜかエラーになる

UpdateProfile.js:322 Uncaught TypeError: Object(...) is not a function

参考にしたサイトも同じように使っていたのでこの形で落ち着きました。

https://qiita.com/shouhi/items/8cf7cac85fdcf204b22c
https://qiita.com/tatsuya1970/items/bdceb10071547c68f711

const storageRef = storages.ref("images/users/" + currentUser.uid + "/");//どのフォルダの配下に入れるか
const imagesRef = storageRef.child("profilePicture.png");//ファイル名

アップするフォルダとファイル名を指定しています。

const upLoadTask = imagesRef.put(image);

実際のアップロードは分割でおこなうみたいでそのためのタスクを作るようです。

       upLoadTask.on(
            "state_changed",
            (snapshot) => {
                console.log("snapshot", snapshot);
            },
            (error) => {
                console.log("err", error);
            },
            () => {
                upLoadTask.snapshot.ref.getDownloadURL().then((downloadURL) => {
                    const url = new URL(downloadURL)
                    console.log(url.href + url.pathname + "?alt=media")
                    dispatch({
                        type: "setPhotoURL",
                        payload: url.href + url.pathname + "?alt=media"
                    });
                });
            }
        );

タスクはお決まりのLoading,error,dataの形式です。
Firebaseの場合は
Loading => snapshot
error => error
data => () => { の部分
のようです。

成功した時にupLoadTask.snapshot.ref.getDownloadURL()でURLを取得する事ができます。もう少し簡単にとれてもいいような気がします

downloadURLはTokenといういらないパラメータもあるのでそちらは消しています

 const url = new URL(downloadURL)
 console.log(url.href + url.pathname + "?alt=media")

ちなみに?alt=mediaをつけないとjsonデータが取れるという意味のわからない仕様だった。 downloadURLをsetPhotoURLでURLデータを保存する。

更新処理の追加

- if (state.displayName !== currentUser.displayName) {
-     updatProfileData = {...updatProfileData,displayName:state.displayName}
-     promises.push(updateProfile(updatProfileData))
- }
+ if (state.displayName !== currentUser.displayName || state.photoURL !== currentUser.photoURL) {
+     updatProfileData = {...updatProfileData
+                         ,displayName:state.displayName
+                         ,photoURL:state.photoURL
+                         }
+     promises.push(updateProfile(updatProfileData))
+ }

更新条件や更新する内容を変更しているだけで更新の処理は変わっていません

初期読み込ませる時にハンドル名をセットする

プロフィール変更画面を読み込み時にFirebaseからデータを取得して photoURLに設定しています

    //NULLだと@material-uiのButtonでエラーになったのでvalueに値をいれる
    currentUser.displayName ? initialState = {...initialState,displayName:currentUser.displayName} : initialState = {...initialState,displayName:""}
+     currentUser.photoURL ? initialState = {...initialState,photoURL:currentUser.photoURL} : initialState = {...initialState,photoURL:""}

    //...initialStateはinitialStateの配列です。「,username:currentUser.email,displayName:currentUser.email」はinitialStateのusernameとdisplayNameだけを更新しています
    initialState = {...initialState,username:currentUser.email}

結果

アバターが登録されていない時

アバターが登録されている時

できていない事

  • アバター画像は基本的には正方形なのでアップする時に正方形する
  • 画像がある状態でアバターの変更をするとプロフィール更新ボタンを押さなくても変更されてしまう。
    普通のシステムだとtempで本番のファイルに移動するだけど・・Firebase storagesにはファイル移動というものがない。
    つまり自分で作らないといけない・・・
  • 画像アップのボタンなのですが、きれいに右下に配置できていない。マテリアルデザインをまだ理解できておらず、ちょっと時間がかかりそう
  • テストの問題なんだけど、認証なしてファイルアップロードをしたときにエラーになるはずなんだけど、そのエラーのハンドリングができていない。
  • セキュリティルールがちょっと曖昧であっている自信がないな〜・・・本番のサービスとしてはダメダメ。
  • ファイルアップ時に「現在アップロード中です」みたいなメッセージを出さないと、ユーザーから見るとワケガワカラナイヨ状態になる

感想

  • 単純なファイルアップロードは、参考にしたサイトどおりにやれば問題なく動きます。設定ファイルを打ち間違えさえしなければ楽勝です。
  • できていない事がありすぎて、ちゃんとできたとは言い切れないのがかなしい😢

参考URL

https://qiita.com/shouhi/items/8cf7cac85fdcf204b22c
https://qiita.com/tatsuya1970/items/bdceb10071547c68f711
https://coders-shelf.com/react-firebase-image-upload/