「Authentication App」作成記録 - React + Firebaseで作るWebシステムの認証機能
devchallengesの「Authentication App」に取り組んだ記録です。
devchallenges
devchallengesは、様々なソフトウェアのお題を提供してくれるサイトです。お題には、そのソフトウェアが満たすべきユーザーストーリーと、画面イメージが記載されています。何を作るかは説明がありますが、どう作るかは基本的に書いていないため、アプリケーション開発の学習に最適です。
今回は、Authentication Appというお題に取り組みました。
何を作ったか?
認証機能を持つWebアプリケーションを作成しました。下記の機能があります。
- 認証機能。予めメールアドレスとパスワードを登録しておき、それでログインできます。
- SNS認証機能。Googleアカウントを使用してログインできます。Twitter等もできるようにしたかったのですが、Twitter側でのApp登録が面倒で諦めました。。。
- プロフィール編集機能。ユーザーの名前、アバター画像などが編集できます。
また、一応レスポンシブ対応しているので、スマホで見ることもできます。
(ちょっとトップ画面が崩れてますが…(汗))
リポジトリ・デモ
成果物は下記のリポジトリにコミットしています。使い方はREADME.mdを参照してください。
また、作成したアプリケーションはデプロイしています。下記のURLからお試しできます。
- Authentication App:https://authenticationapp-dc.web.app
画面紹介
- ログイン画面
- トップ画面
- 編集画面
React部品紹介
作成したReact部品のうち、他のアプリにも流用できそうな部品を紹介します。
Auth.tsx
- リポジトリ:Auth.tsx
今回のアプリで一番頑張った部品です。React-Router-Domと組み合わせることで、認証に関するロジックをすべてここへと集約しています。
基本的なつくり・アイデアは、こちらの記事を参考にしました。これをFirebase/Redux用に少しカスタマイズしています。
https://qiita.com/ginban22/items/a36d01b41deaeedd581e
この部品は、下記のように利用します。認証が必要な画面へのRouteはAuthコンポーネントのchildrenに指定します。必要ない画面へのRouteはAuthコンポーネントの外に指定します。すると、firebase上で認証済みの場合は各画面を表示し、未認証状態の場合はloginUriで指定した画面(下の場合はLoginコンポーネント)を表示します。
<Router>
<Switch>
<Route exact path={pathLogin} component={Login} />
<Route exact path={pathSignUp} component={SignUp} />
<Auth loginUri={pathLogin} loadingComponent={LoginLoading}>
<Switch>
<Route exact path={pathUserInfoEdit} component={UserInfoEdit} />
<Route component={UserInfo} />
</Switch>
</Auth>
</Switch>
</Router>
AvatarEdit.tsx
- リポジトリ:AvatarEdit.tsx
画像をアップロードするための部品です。アップロードすると、Avatarタグにプレビュー表示します。プロフィールの編集画面で利用しています。
どうやって作ったか?
Webアプリケーションの中身や、作成時に調べたことをまとめています。
採用した技術/ライブラリ
-
React
- SPAフレームワークとして採用。Vueよりも単純に好き。
-
TypeScript
- 型付けできるTypeScriptを採用。
-
Firebase
- バックエンド。ホスティング・アップロードデータの保存に利用。
-
Material-UI
- 画面のUI用ライブラリ。
-
React Redux
- 状態管理用のライブラリ。
-
React Router Dom
- ルーティングのライブラリ。
デザイン
Material-UIでCSSセレクタのhoverやnth-of-typeを使う
Material-UIではcss-in-javascriptを採用しているため、直接CSSセレクタを使うことはありません。しかし、Material-UI側でちゃんと用意してくれています。
画像ボタンの作り方
CSSを使うと、画像をボタンのように見せかけることができます。本アプリでは、ログイン画面のSNS認証のところで使ってます。
material-UI Grid itemの中央寄せ
レスポンシブ対応
入門的な内容ですが、今の自分にはこれで十分でした。
複数のクラス名を結合する・動的なクラス名の指定
clsxというライブラリが、Material-UIにくっついてきます。これを使うと楽でした。また、clsxを使うと動的にクラスを切り替えることができます。
Webアプリ
textFieldでEnterが押されたときにイベントを実行する
onKeyDownイベントを使うとできます。イベントハンドラーのKeyプロパティで押されたキーを判別します。
検索するとKeyCodeを使うサンプルが多いですが、これは非推奨でWeb標準から削除されています。
画像ファイルのプレビュー表示
Javascriptに標準でついているFileReader APIが使えます。
複数の非同期処理
Firebaseへのアップロード処理を非同期で行いますが、最大で4つのPromiseを並列実行しました。4つの処理を並列実行し、それらがすべて終わった後でやりたい処理があるときは、標準で用意されているPromise.allが使えます。
FirebaseのAPI
今回利用したFirebaseの各機能の使い方をまとめます。(後で自分が使うとき思い出せるように…)
事前準備
どのFirebase Projectのアプリケーションなのか、コード上で設定する必要があります。下記参照。
auth:サインイン
let promise: Promise<firebase.auth.UserCredential>;
// メールアドレス & Passwordの場合
promise = firebase.auth().signInWithEmailAndPassword(mail, password);
// Google認証の場合
promise = firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider());
// 認証後の後処理
promise.then((userCredential) => {
const user = credential.user;
if(user){
const {displayName, email, phoneNumber, photoURL, uid} = user; // この辺のプロパティが利用できます。
}
}).catch((error) => {
const { code, message } = error; // この辺のプロパティが利用できます。
});
auth:サインアップ
firebase.auth().createUserWithEmailAndPassword(mail, password).then((userCredential) => {
const user = credential.user;
if(user){
const {displayName, email, phoneNumber, photoURL, uid} = user; // この辺のプロパティが利用できます。
}
}).catch((error) => {
const { code, message } = error; // この辺のプロパティが利用できます。
});
auth:認証状態のオブザーバーを設定する
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const {displayName, email, phoneNumber, photoURL, uid} = user; // この辺のプロパティが利用できます。
} else {
// User is signed out
}
}).catch((error) => {
const { code, message } = error; // この辺のプロパティが利用できます。
});
auth:サインアウト
- ドキュメント:https://firebase.google.com/docs/auth/web/password-auth
- ※一番下にちょこっと書いてある
firebase.auth().signOut().then(() => {
// サインアウト成功時
}).catch((error) => {
const { code, message } = error; // この辺のプロパティが利用できます。
});
firestore:オブザーバーを設定してデータを取得する
firebase.firestore().collection('CollectionName').doc('id').onSnapshot(
(snapshot) => {
if(snapshot.exists){
const fieldValue = snapshot.get('FieldName');
}
},
(error) => {}
);
- RDBとFirestoreのイメージ対応(個人の感想)
Firestore | RDB |
---|---|
collection | table |
doc | record |
field | column |
firestore:データを保存する
firebase.firestore().collection('CollectionName').doc('id').set({
field1: 'field1',
field2: 'field2'
})
- 補足:setを使うと、ドキュメントがなければ作成し、ドキュメントがあれば上書き更新する。他にもupdate等があり、挙動が若干違う。
storage:ファイルをアップロードする
firebase.storage().ref().child('親フォルダ').child('子フォルダ').child('ファイル名').put(file).then((snapshot) => {
const url = await snapshot.ref.getDownloadURL(); // ファイルのURL。
})