🌟

Vue3(TS)×firebase×ElementUIでレビューアプリを作ってみた!

2022/10/01に公開

はじめに

見て頂きありがとうございます!
初心者なので間違っている部分もあります。あれば教えて頂きたいです。
特に苦労したところだったりわからないとこを書いていこうと思います!
今回は学校の先生レビューアプリの実装をします!

使用環境

macOS: Monterey 12.5
vue: 3.2.25
vue-router: 4.0.16
firebase: 9.8.3
element-plus: 2.2.5

ディレクトリ構造

src
├─components
│  └─ReviewView.vue
│  └─ReviewInput.vue
├─plugins
│  └─firebase.ts
├─types
│  └─types.ts
├─router
│  └─router.ts
├─App.vue

firestoreの定義

firestoreをこのように実装しました。ご参照ください。

- hoge
  - department: String
  - lecture: String
  - review_star: Number
  - teacher: String
  - comment: String

ルーティングの設定

src/router/router.ts
import { createApp } from 'vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import App from './App.vue';

const app = createApp(App);

const routes: RouteRecordRaw[] = [
  {
    path: '/reviewinput',
    component: () => import('../components/ReviewInput.vue'),
    name: 'reviewinput',
    meta: { isPublic: true },
  },
  {
    path: 'reviewview',
    component: () => import('../components/ReviewView.vue'),
    name: 'ReviewView',
    meta: { isPublic: true },
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

Vue入力画面

src/components/reviewinput.vue
<template>
  <section class="review_wrapper">
    <div class="container">
      <el-alert v-if="alert" 
      title="レビューが投稿されました。" 
      type="success" 
      show-icon />
      <el-form
        label-position="top"
        label-width="100px"
        :model="form"
        style="max-width: 460px"
      >
        <h3>レビューを入力する</h3>
        <div class="review_star">
          <span class="review_star-text">{{review_star}}</span>
          <el-rate v-model="review_star" />
        </div>
        <el-form-item label="先生">
          <el-input v-model="form.teacher" />
        </el-form-item>
        <el-form-item label="授業">
          <el-input v-model="form.lecture" />
        </el-form-item>
	<el-form-item label="対象学部">
          <el-input v-model="form.department" />
        </el-form-item>
        <el-form-item label="コメント">
        <el-input v-model="form.comment" 
	  type="textarea" 
	  rows="6" 
	  cols="40" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit">Create</el-button>
          <el-button>Cancel</el-button>
        </el-form-item>
      </el-form>
    </div>
  </section>
</template>

templete内の記述をこのように記述します。
気になるようなところがあればelementUIを見て各自修正お願いします。

まずv-modelとは?

v-modelの説明

v-modelはVueで用意してある変数とタグの値を同期してくれます!
inputタグにv-modelを書いて、変数と結びつけたときに変数の内容とタグを同期しています。

 <el-input v-model="form.lecture" />

例えばこんな感じでform.lectureが変数とタグの値を同期しています。
formとか作る時とかに便利です!

初期値の設定

src/components/reviewinput.vue
  import { reactive, ref } from 'vue';
  import { collection, addDoc } from 'firebase/firestore';
  import { db } from '../plugins/firebase';
     export type Props = {
    id: string;
  };
  const alert = ref(false);
  const props = withDefaults(defineProps<Props>(), {
    id: "0",
  });
  const review_star = ref(1)
  const form = reactive({
    title: '',
    review_star: '',
    lecture: '',
    teacher: '',
    comment: ''
  });

refとreactiveの違いについて

refとreactiveの違いは、標準のJavaScriptロジックを作成する方法と多少比較できます。

さらに公式ドキュメントではコード例と共に補足説明がされていますが、結局refとreactiveの違いはJavaScriptでプリミティブを使うかオブジェクトを使うか程度の違いしかないようです。

なので言いたいことは1.プリミティブにはref、オブジェクトにはreactiveを使用するということTypescriptと使うらしい

https://kobatech-blog.com/vue-composition-api-ref-reactive/

1. Use ref and reactive just like how you’d declare primitive type variables and object variables in normal JavaScript.
It is recommended to use a type system with IDE support when using this style.

2.Use reactive whenever you can, and remember to use toRefs when returning reactive objects from composition functions.
This reduces the mental overhead of refs but does not eliminate the need to be familiar with the concept.

翻訳すると、、、

1. 通常のJavaScriptでプリミティブ型変数やオブジェクト変数を宣言するのと同じように、refやreactiveを使用します。
このスタイルを使用する場合は、IDEがサポートするタイプシステムを使用することをお勧めします。

2.可能な限りreactiveを使用し、合成関数からreactiveオブジェクトを返すときはtoRefを使用することを忘れない。
これにより、refの精神的なオーバーヘッドを減らすことができますが、その概念に精通する必要性がなくなるわけではありません。

今のところ明白な使い分けは提唱されていない見たいです。
ということなので、refにはプリミティブ、reactiveにはオブジェクトで使いましょう!

ここではこのように使用したいと思います。
ref:プリミティブ型
reactive:オブジェクト型

firebaseに書き込み

src/components/reviewinput.vue
  const onSubmit = async () => {
    const docRef = await addDoc(collection(db, 'hoge'), {
      comment: form.comment,
      id: props.id,
      teacher: form.teacher,
      review_star: review_star.value,
      lecture: form.lecture,
      department: form.department,
    }).catch((e) => {
      console.error('Error adding document: ', e);
    });
    if (docRef) {
    console.log(`Document ID: ${docRef.id}`);
    alert.value = true;
    };
  };

ここではfirestoreに渡す処理の部分なので、このように書いていきます。
ここで使うaddDocはfirebaseの関数なので説明していきたいと思います。

addDocの使い方

ランダムなIDが付与されたドキュメントの作成(addDoc)
addDoc関数ではコレクションリファレンスを利用します

FireStoreが自動的に一意のドキュメントIDを割り振ってドキュメントを作成します。
※基本的にランダムなIDでドキュメントを作り、必要な時のみドキュメントIDを指定して作成。

const colRef = collection(db, "cities");
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA"
};
// addDoc
await addDoc(コレクションへの参照, data); // 例
await addDoc(colRef, data); // 実際のコード

このようにaddDoc()の前にcollectionで接続したいテーブルを個別で定義してください!
あとは非同期処理を使用しているので、例外処理を書くだけで終わりです。

Vueレビュー画面

firebaseで実装

src/plugins/firebase.ts
import { collection, getDocs } from 'firebase/firestore';
import { db } from './plugins/firebase';
import { onMounted, ref, onBeforeUpdate } from 'vue';

const getData = async () => {
  const querySnapshot = await getDocs(collection(db, 'hoge'));

  querySnapshot.forEach((doc) => {
    console.log(`${doc.id} => ${doc.data().name}`);
  });

  return querySnapshot;
};

関数で定義した後でgetDocsでfirestoreのhogeからデータを取得
関数(querySnapshot)からforEachで配列(doc)をログで取得

型定義

src/types/types.ts
export type Review = {
  title: string;
  teacher: string;
  review_star: number;
  comment: string;
  department: string;
};

Vue側の実装

src/components/ReviewView.vue
export type Props = {
  id: string;
};
const props = withDefaults(defineProps<Props>(), {
  id: '0',
});

const views = ref([{}]);
const getData = async () => {
  const querySnapshot = await getDocs(collection(db, 'hoge'));

  return querySnapshot;
};

const items = ref<Review[]>([]);

onBeforeUpdate(() => {
  items.value = [];
});

onMounted(async () => {
  const snapshot = await getData();
  snapshot.forEach((doc) => {
    return items.value.push(doc.data() as Review);
    // doc dataでとったものをReviewの型でつけた
  });
  views.value = [];
  for (let index = 0; index < items.value.length; index++) {
    console.log(items.value[index].id);
    if (props.id === items.value[index].id) {
      views.value.push(items.value[index]);
    }
  }
});

ここでのポイント
Typescriptのasの使い方。
onMounted使い方。

型アサーション(as)について

TypeScriptはなんちゃって型付けなので型推論をしてくれます。
型アサーション(Type Assertion)とは、その推論された型や、既に型定義済みの変数の型を上書きします。

ここで型推論とは?
型がついていないときに、いい感じに型を型を補完してくれる便利な機能です!

https://typescriptbook.jp/reference/values-types-variables/type-assertion-as

次にonMountedのところ、まずonMountedとは????

OnMountedの使い方

Vueのライフサイクルの一種です。
状態遷移にフックさせることで、特定の状態の時に行いたい処理を書くことができます。

created(vue2):インスタンス初期化時、DOMが生成される前→ここだったらデータ取得前
OnMounted:インスタンス初期化時、DOMが生成された後→今回はここ。firebaseで定義した関数を使いたかったから。

今回はmountedを使うときにsnapshotを使用しデータをpushする動作を加えました。

push() メソッドは、配列の末尾に 1 つ以上の要素を追加することができます。また戻り値として新しい配列の要素数を返します。使い方配列に入れていく

const animals = ['pigs', 'goats', 'sheep'];

const count = animals.push('cows');
console.log(count);
// expected output: 4
console.log(animals);
// expected output: Array ["pigs", "goats", "sheep", "cows"]

animals.push('chickens', 'cats', 'dogs');
console.log(animals);
// expected output: Array ["pigs", "goats", "sheep", "cows", "chickens", "cats", "dogs"]

ライフサイクルフック

ライフサイクルフックは、直接インポートされた onX 関数に登録できます:

import { onMounted, onUpdated, onUnmounted } from 'vue'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
  }
}

これらのライフサイクルフック登録関数は、[setup()](https://v3.ja.vuejs.org/api/composition-api.html#setup) の間にのみ、同期的に使用できます。これらの関数は、現在アクティブなインスタンス(今まさに setup() が呼び出されているコンポーネントインスタンス)を見つけるために内部のグローバル状態に依存しています。現在アクティブなインスタンスがない状態でそれらを呼び出すと、エラーになります。

コンポーネントのインスタンスコンテキストは、ライフサイクルフックの同期的な実行中にも設定されます。その結果、ライフサイクルフック内で同期的に作成されたウォッチャと算出プロパティも、コンポーネントがアンマウントされるとき自動的に破棄されます。

ハマったとこ

export default {
    setup() {
      const list = reactive([1, 2, 3])
      const divs = ref([])

      // ref が各更新の前に必ずリセットされるようにしてください
      onBeforeUpdate(() => {
        divs.value = []
      })

      return {
        list,
        divs
      }

https://v3.ja.vuejs.org/guide/composition-api-template-refs.html#v-for-内部での使用Composition API のテンプレート参照を v-for
 内部で使う場合、特別な処理はされません。代わりに、関数を使って ref に独自処理を行うようにします。

onBeforeUpdateについて

これも先述したようにVueのライフサイクルの一種です。
onBeforeUpdateはUpdateの前に行いたい処理を実行することです。なので今回は、全てのライフサイクルの前に書くことにより、配列の初期化をしています。ここができていなかったので上手くfirestoreに書き込めなかったです、(自分の書くコードが悪いのかも)

ReviewViewのtemplateの実装

src/components/ReviewView.vue
<template>
  <el-timeline>
    <el-timeline-item
      v-for="(item, index) in views"
      :key="index"
      placement="top"
    >
      <el-card>
        <h4>{{ item.title }}</h4>
        <p>{{ item.comment }}</p>
	//変数から欲しいデータを取得
      </el-card>
    </el-timeline-item>
  </el-timeline>
</template>

あとはv-forでitemと引数と 配列で回してからitem.titleとitem.commentを入れて、コンポーネントとして使用するだけです!お疲れ様でした。(検索機能とかも作りたい)

参考

Discussion