0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Firebase Firestore Databaseでデータを登録してみた

Posted at

はじめに

この記事では、Vue.js + Nuxt.js + Firebase + Vuetifyを使用してプロフィール作成画面を実装する方法を詳しく解説します。ユーザーがニックネーム、年齢、性別を入力し、Firebase Firestoreに保存する機能を構築していきます。

実装する機能

  • プロフィール情報の入力フォーム(ニックネーム、年齢、性別)
  • リアルタイムバリデーション
  • Firebase Firestoreへのデータ保存
  • エラーハンドリング
  • ローディング状態の管理

技術スタック

  • フロントエンド: Vue.js 3 + Nuxt.js 3
  • UI フレームワーク: Vuetify 3
  • データベース: Firebase Firestore
  • 言語: TypeScript
  • 状態管理: Vue Composition API

要件

  • ニックネーム
  • 年齢
  • 性別

上記入力項目を登録する画面を作成
FirebaseのFirestore Databaseを使いプロフィールを登録します

アーキテクチャ概要

この実装では、以下のような構成でプロフィール作成機能を構築します:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Vue Page      │    │   Component     │    │   Composable    │
│ (create-profile)│───▶│ (ProfileForm)   │───▶│ (useProfile)    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │                       │                       ▼
         │                       │              ┌─────────────────┐
         │                       │              │ Firebase        │
         │                       │              │ Firestore       │
         │                       │              └─────────────────┘
         ▼                       ▼
┌─────────────────┐    ┌─────────────────┐
│   Vuetify UI    │    │  Validation     │
│   Components    │    │  Rules          │
└─────────────────┘    └─────────────────┘

コンポーネント設計のポイント

  1. 単一責任の原則: 各コンポーネントは明確な役割を持つ
  2. 再利用性: ProfileFormコンポーネントは作成・編集両方で使用可能
  3. 疎結合: コンポーネント間の依存関係を最小限に抑制
  4. 型安全性: TypeScriptによる型定義で実行時エラーを防止

Firebase Firestore Databaseとは

Firestore Databaseは、Googleが提供するNoSQLドキュメントデータベースです。リアルタイム同期、オフライン対応、スケーラビリティなどの特徴があります。

主な特徴

  • NoSQLドキュメントデータベース: 従来のリレーショナルデータベースとは異なり、JSONライクなドキュメント形式でデータを保存
  • リアルタイム同期: データの変更が即座にクライアントに反映される
  • オフライン対応: ネットワーク接続がなくてもデータの読み書きが可能
  • 自動スケーリング: ユーザー数の増加に応じて自動的にスケールする
  • セキュリティルール: データベースレベルでのアクセス制御が可能

データ構造

Firestoreでは以下の階層構造でデータを管理します:

データベース
└── コレクション(例:users)
    └── ドキュメント(例:user123)
        └── フィールド(例:nickname, age, gender)

プロフィールデータの保存例

// Firestoreにプロフィールデータを保存
const saveProfile = async (profileData: {
  nickname: string;
  age: number;
  gender: string;
}) => {
  try {
    await addDoc(collection(db, 'profiles'), {
      nickname: profileData.nickname,
      age: profileData.age,
      gender: profileData.gender,
      createdAt: serverTimestamp(),
    });
    console.log('プロフィールが正常に保存されました');
  } catch (error) {
    console.error('プロフィールの保存に失敗しました:', error);
  }
};

バリデーションルール

各入力項目には以下のバリデーションルールを設定しています:

ニックネーム

  • 必須入力チェック

年齢

  • 必須入力チェック
  • 数字のみの入力チェック
  • 0〜150の範囲内チェック

性別

  • 必須入力チェック
const nicknameRules = [(v: string) => !!v || 'ニックネームは必須です']
const ageRules = [
  (v: string) => !!v || '年齢は必須です',
  (v: string) => /^\d+$/.test(v) || '年齢は数字で入力してください',
  (v: string) => {
    const num = parseInt(v)
    return (num >= 0 && num <= 150) || '年齢は0〜150の範囲で入力してください'
  },
]
const genderRules = [(v: string) => !!v || '性別は必須です']

入力項目のコンポーネントの作成

プロフィール作成画面の核となるフォームコンポーネントを作成します。このコンポーネントは以下の特徴を持ちます:

設計思想

  • 再利用性: プロフィール作成・編集の両方で使用可能
  • バリデーション: リアルタイムでの入力値チェック
  • アクセシビリティ: Vuetifyのアクセシブルなコンポーネントを活用
  • 型安全性: TypeScriptによる厳密な型定義
app/components/ProfileForm.vue
<script setup lang="ts">
interface Props {
  initialNickname?: string
  initialAge?: string
  initialGender?: string
  submitButtonText?: string
  isSubmitting?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  initialNickname: '',
  initialAge: '',
  initialGender: '',
  submitButtonText: '作成',
  isSubmitting: false,
})

const form = ref()

const nickname = ref(props.initialNickname)
const age = ref(props.initialAge)
const gender = ref(props.initialGender)

const nicknameRules = [(v: string) => !!v || 'ニックネームは必須です']
const ageRules = [
  (v: string) => !!v || '年齢は必須です',
  (v: string) => /^\d+$/.test(v) || '年齢は数字で入力してください',
  (v: string) => {
    const num = parseInt(v)
    return (num >= 0 && num <= 150) || '年齢は0〜150の範囲で入力してください'
  },
]
const genderRules = [(v: string) => !!v || '性別は必須です']

// 親コンポーネントへのイベント送信
const emit = defineEmits(['submit'])

const submit = async () => {
  if (form.value) {
    const { valid } = await form.value.validate()
    if (!valid) return
  }
  emit('submit', {
    nickname: nickname.value,
    age: age.value,
    gender: gender.value,
  })
}
</script>

<template>
  <v-form ref="form" @submit.prevent="submit">
    <v-text-field
      v-model="nickname"
      color="primary"
      variant="underlined"
      label="ニックネーム"
      :rules="nicknameRules"
    />
    <v-text-field
      v-model="age"
      color="primary"
      variant="underlined"
      label="年齢"
      :rules="ageRules"
    />
    <v-select
      v-model="gender"
      color="primary"
      variant="underlined"
      label="性別"
      :rules="genderRules"
      :items="['男性', '女性', 'その他']"
    />

    <v-btn
      color="primary"
      class="mt-4"
      type="submit"
      block
      :loading="props.isSubmitting"
      :disabled="props.isSubmitting"
      >{{ props.submitButtonText }}</v-btn
    >
  </v-form>
</template>

主要な処理の流れ

1. Props定義

interface Props {
  initialNickname?: string
  initialAge?: string
  initialGender?: string
  submitButtonText?: string
  isSubmitting?: boolean
}
  • 親コンポーネントから受け取るプロパティを定義
  • 初期値やボタンテキスト、送信状態などを設定可能

2. リアクティブデータの初期化

const nickname = ref(props.initialNickname)
const age = ref(props.initialAge)
const gender = ref(props.initialGender)
  • 各入力フィールドの値をリアクティブに管理
  • 親から渡された初期値で初期化

3. バリデーションルールの定義

  • ニックネーム: 必須入力チェック
  • 年齢: 必須入力、数字のみ、0〜150の範囲内
  • 性別: 必須入力チェック

4. フォーム送信処理

const submit = async () => {
  if (form.value) {
    const { valid } = await form.value.validate()
    if (!valid) return
  }
  emit('submit', {
    nickname: nickname.value,
    age: age.value,
    gender: gender.value,
  })
}
  • フォームのバリデーションを実行
  • バリデーションが通った場合のみ、親コンポーネントにデータを送信

5. テンプレート部分

  • Vuetifyのコンポーネントを使用したUI
  • v-formでフォーム全体を管理
  • 各入力フィールドにバリデーションルールを適用
  • 送信ボタンは送信中状態に応じてローディング表示と無効化

特徴

  1. 再利用性: Propsで初期値やボタンテキストを変更可能
  2. バリデーション: リアルタイムで入力値の妥当性をチェック
  3. 状態管理: 送信中の状態を適切に管理
  4. イベント駆動: 親コンポーネントとの疎結合な設計

このコンポーネントは、プロフィール作成・編集の両方の場面で使用できる汎用的な設計になっています。

実装のポイント

1. Props設計の工夫

interface Props {
  initialNickname?: string
  initialAge?: string
  initialGender?: string
  submitButtonText?: string
  isSubmitting?: boolean
}
  • すべてのプロパティをオプショナルにすることで、柔軟な使用を可能にする
  • submitButtonTextにより、作成・更新でボタンテキストを変更可能
  • isSubmittingで送信状態を外部から制御

2. バリデーション戦略

  • リアルタイムバリデーション: ユーザーが入力中に即座にフィードバック
  • 複数ルール: 各フィールドに複数のバリデーションルールを適用
  • ユーザーフレンドリー: 分かりやすいエラーメッセージを表示

3. イベント駆動設計

const emit = defineEmits(['submit'])
  • 親コンポーネントとの疎結合を実現
  • フォームの送信処理は親に委譲
  • コンポーネントの再利用性を向上

プロフィールデータをFirestoreに保存する

プロフィールデータをFirestoreに保存するためのComposableを作成します。このComposableは、データの保存処理とローディング状態を管理します。

app/composables/useProfile.ts
import { doc, setDoc, type Firestore } from 'firebase/firestore'
import { useNuxtApp } from 'nuxt/app'

export interface ProfileData {
  nickname: string
  age: number
  gender: string
  createdAt: Date
  updatedAt: Date
}

export const useProfile = () => {
  const { $firestore } = useNuxtApp()
  const firestore = $firestore as Firestore | null
  const loading = ref(false)

  // プロフィール保存
  const saveProfile = async (
    nickname: string,
    age: string,
    gender: string,
    userId: string
  ) => {
    loading.value = true
    if (!firestore) {
      return {
        error: new Error('Firestoreが初期化されていません'),
      }
    }
    try {
      const profileData: ProfileData = {
        nickname,
        age: parseInt(age),
        gender,
        createdAt: new Date(),
        updatedAt: new Date(),
      }
      await setDoc(doc(firestore, 'profiles', userId), profileData)
      return { error: null }
    } catch (error) {
      return { error: error as Error }
    } finally {
      loading.value = false
    }
  }

  return {
    saveProfile,
    loading: readonly(loading),
  }
}

主要な処理の流れ

1. インポートとインターフェース定義

import { doc, setDoc, type Firestore } from 'firebase/firestore'
import { useNuxtApp } from 'nuxt/app'

export interface ProfileData {
  nickname: string
  age: number
  gender: string
  createdAt: Date
  updatedAt: Date
}
  • Firebase Firestore: docsetDocをインポートしてドキュメントの作成・更新を行う
  • Nuxt App: useNuxtAppでNuxtアプリケーションのインスタンスにアクセス
  • ProfileData: プロフィールデータの型定義。作成日時と更新日時も含む

2. Composableの初期化

export const useProfile = () => {
  const { $firestore } = useNuxtApp()
  const firestore = $firestore as Firestore | null
  const loading = ref(false)
  • Firestoreインスタンス: NuxtアプリからFirestoreインスタンスを取得
  • ローディング状態: 非同期処理中の状態を管理するリアクティブ変数

3. プロフィール保存処理

const saveProfile = async (
  nickname: string,
  age: string,
  gender: string,
  userId: string
) => {
  loading.value = true
  if (!firestore) {
    return {
      error: new Error('Firestoreが初期化されていません'),
    }
  }
  try {
    const profileData: ProfileData = {
      nickname,
      age: parseInt(age),
      gender,
      createdAt: new Date(),
      updatedAt: new Date(),
    }
    await setDoc(doc(firestore, 'profiles', userId), profileData)
    return { error: null }
  } catch (error) {
    return { error: error as Error }
  } finally {
    loading.value = false
  }
}

処理の詳細:

  • パラメータ: ニックネーム、年齢(文字列)、性別、ユーザーIDを受け取る
  • ローディング開始: 処理開始時にローディング状態をtrueに設定
  • Firestoreチェック: Firestoreが初期化されているかチェック
  • データ変換: 年齢を文字列から数値に変換(parseInt(age)
  • タイムスタンプ: 作成日時と更新日時を現在時刻で設定
  • Firestore保存: setDocprofilesコレクションのuserIdドキュメントに保存
  • エラーハンドリング: エラーが発生した場合はエラーオブジェクトを返す
  • ローディング終了: 処理完了時にローディング状態をfalseに設定

4. 戻り値

return {
  saveProfile,
  loading: readonly(loading),
}
  • saveProfile: プロフィール保存関数を公開
  • loading: 読み取り専用のローディング状態を公開

特徴

  1. エラーハンドリング: Firestoreの初期化チェックとtry-catch文でエラーを適切に処理
  2. 型安全性: TypeScriptの型定義により、データの整合性を保証
  3. 状態管理: ローディング状態をリアクティブに管理
  4. 再利用性: Composableとして実装することで、複数のコンポーネントで利用可能
  5. Firestore最適化: setDocを使用してドキュメントの作成・更新を効率的に実行

使用例

// コンポーネント内での使用
const { saveProfile, loading } = useProfile()

const handleSubmit = async (formData: any) => {
  const result = await saveProfile(
    formData.nickname,
    formData.age,
    formData.gender,
    currentUser.value?.uid
  )
  
  if (result.error) {
    console.error('保存に失敗しました:', result.error)
  } else {
    console.log('プロフィールが正常に保存されました')
  }
}

このComposableにより、プロフィールデータの保存処理が簡潔かつ安全に実装できます。

Composable設計の利点

1. 関心の分離

  • データアクセス層: Firestoreとの通信処理を分離
  • ビジネスロジック: プロフィール関連の処理を集約
  • 状態管理: ローディング状態を一元管理

2. テスト容易性

// テスト例
const mockFirestore = createMockFirestore()
const { saveProfile } = useProfile(mockFirestore)
  • モックオブジェクトを使用した単体テストが容易
  • ビジネスロジックとデータアクセス層の分離により、テストの範囲を明確化

3. 再利用性

  • 複数のコンポーネントで同じロジックを共有
  • プロフィール関連の機能拡張時の影響範囲を最小化

プロフィール作成ページ

app/pages/home/create-profile.vue
<script setup lang="ts">
// プロフィール作成フォームコンポーネントをインポート
import ProfileForm from '~/components/ProfileForm.vue'

// プロフィール保存機能とローディング状態を取得
const { saveProfile, loading } = useProfile()
// 認証されたユーザー情報を取得
const { user } = useAuth()

// エラーメッセージを管理するリアクティブ変数
const error = ref('')

// プロフィール送信処理
const handleProfileSubmit = async (data: {
  nickname: string
  age: string
  gender: string
}) => {
  // プロフィール保存を実行(ユーザーIDも含める)
  const { error: saveProfileError } = await saveProfile(
    data.nickname,
    data.age,
    data.gender,
    user.value?.uid ?? ''
  )

  // エラーが発生した場合はエラーメッセージを表示して処理を終了
  if (saveProfileError) {
    error.value = saveProfileError.message
    return
  }

  // プロフィール保存成功後、ホームページにリダイレクト
  await navigateTo('/home')
}
</script>

<template>
  <!-- プロフィール作成用のカードコンポーネント -->
  <v-card class="mx-auto my-8" max-width="500">
    <v-card-item>
      <v-card-title>プロフィール作成</v-card-title>
    </v-card-item>
    <v-card-item>
      <!-- エラーが発生した場合に表示するアラート -->
      <v-alert
        v-if="error"
        type="error"
        :text="error"
        variant="tonal"
        class="mb-4"
      />
      <!-- プロフィールフォームコンポーネント -->
      <!-- loading状態を渡し、submitイベントをハンドル -->
      <ProfileForm :is-submitting="loading" @submit="handleProfileSubmit" />
    </v-card-item>
  </v-card>
</template>

ページコンポーネントの設計思想

1. 責任の明確化

  • ページコンポーネント: ルーティング、認証、エラーハンドリングを担当
  • フォームコンポーネント: UI表示とバリデーションを担当
  • Composable: データアクセスとビジネスロジックを担当

2. エラーハンドリング戦略

const error = ref('')

if (saveProfileError) {
  error.value = saveProfileError.message
  return
}
  • ユーザーフレンドリー: 技術的なエラーを分かりやすいメッセージに変換
  • 視覚的フィードバック: Vuetifyのアラートコンポーネントでエラーを表示
  • 処理の継続: エラー発生時は適切に処理を停止

3. ナビゲーション管理

await navigateTo('/home')
  • 成功時の遷移: プロフィール保存成功後に適切なページへリダイレクト
  • 非同期処理: awaitを使用して確実に遷移を実行

ベストプラクティスと注意点

セキュリティ考慮事項

1. Firestoreセキュリティルール

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /profiles/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}
  • 認証チェック: ログインユーザーのみアクセス可能
  • 所有者チェック: 自分のプロフィールのみ編集可能
  • データ検証: 入力データの形式をサーバーサイドで検証

2. クライアントサイドバリデーションの限界

// クライアントサイドバリデーションは補助的なもの
const ageRules = [
  (v: string) => !!v || '年齢は必須です',
  (v: string) => /^\d+$/.test(v) || '年齢は数字で入力してください',
  // サーバーサイドでも同様の検証が必要
]
  • UX向上: リアルタイムフィードバックでユーザビリティを向上
  • セキュリティ: サーバーサイドバリデーションは必須
  • 二重チェック: クライアント・サーバー両方で検証を実装

パフォーマンス最適化

1. コンポーネントの最適化

// 不要な再レンダリングを防ぐ
const nickname = ref(props.initialNickname)
const age = ref(props.initialAge)
const gender = ref(props.initialGender)

// 計算プロパティでメモ化
const isValidForm = computed(() => {
  return nickname.value && age.value && gender.value
})

2. Firestoreクエリの最適化

// 必要なフィールドのみ取得
const profileData: ProfileData = {
  nickname,
  age: parseInt(age),
  gender,
  createdAt: new Date(),
  updatedAt: new Date(),
}
// インデックスを適切に設定
await setDoc(doc(firestore, 'profiles', userId), profileData)

エラーハンドリングのベストプラクティス

1. 階層的エラーハンドリング

try {
  const { error: saveProfileError } = await saveProfile(...)
  if (saveProfileError) {
    // ビジネスロジックエラー
    error.value = saveProfileError.message
    return
  }
} catch (error) {
  // 予期しないエラー
  console.error('予期しないエラーが発生しました:', error)
  error.value = 'システムエラーが発生しました。しばらく時間をおいて再度お試しください。'
}

2. ユーザーフレンドリーなエラーメッセージ

const getErrorMessage = (error: Error): string => {
  switch (error.code) {
    case 'permission-denied':
      return 'この操作を実行する権限がありません。'
    case 'unavailable':
      return 'サービスが一時的に利用できません。しばらく時間をおいて再度お試しください。'
    default:
      return 'エラーが発生しました。しばらく時間をおいて再度お試しください。'
  }
}

アクセシビリティの考慮

1. セマンティックHTML

<template>
  <v-form ref="form" @submit.prevent="submit" role="form" aria-label="プロフィール作成フォーム">
    <v-text-field
      v-model="nickname"
      label="ニックネーム"
      :rules="nicknameRules"
      aria-describedby="nickname-error"
    />
    <!-- エラーメッセージに適切なIDを設定 -->
  </v-form>
</template>

2. キーボードナビゲーション

  • Vuetifyコンポーネントは標準でキーボードナビゲーションをサポート
  • フォーカス管理とタブオーダーを適切に設定
  • スクリーンリーダー対応のaria属性を活用

まとめ

この記事では、Vue.js + Nuxt.js + Firebase + Vuetifyを使用したプロフィール作成画面の実装について詳しく解説しました。

実装のポイント

  1. コンポーネント設計: 再利用性と保守性を重視した設計
  2. 型安全性: TypeScriptによる厳密な型定義
  3. エラーハンドリング: ユーザーフレンドリーなエラー処理
  4. セキュリティ: Firestoreセキュリティルールの適切な設定
  5. アクセシビリティ: すべてのユーザーが利用可能なUI設計

今後の拡張可能性

  • プロフィール画像アップロード: Firebase Storageとの連携
  • プロフィール編集機能: 既存データの更新処理
  • バリデーション強化: より詳細な入力値チェック
  • 国際化対応: 多言語サポートの実装
  • オフライン対応: PWA機能の追加

この実装をベースに、より高度な機能を追加していくことで、本格的なWebアプリケーションを構築できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?