AUTOVICE

TECH BLOG

Vue.js+Nuxt.js+Firebaseでサンプルアプリを作ってみた話

2020-03-16

はじめに

Vue.jsベースのWebフレームワーク、Nuxt.jsでサンプルアプリを作ってみました。



みんなの方程式

機能の紹介

詳細

投稿された方程式の詳細を見ることができます。

サンプルアプリはプリレンダリングしているので、詳細ページに直接アクセスしても何も表示されません。詳細ページの表示内容は、トップページのリンクからデータのパラメータを受け取っているためです。

本来、このサンプルアプリのような動的サイトはSSR(サーバーサイドレンダリング)、またはSPA(シングルページアプリケーション)としてデプロイする必要があります。

追記

https://whitia.firebaseapp.com/cards/c98a74c1-c137-4b49-89cd-7e66ba540eef

詳細ページに直接アクセスした場合でもちゃんと内容が表示されるように修正しました(上のURLに直接アクセスしてみてください)。動的ルーティングは使ってません。どのように実装したかはまた記事を書こうと思います。

ログイン

右上の「Google アカウントでログイン」ボタンからログインできます。

すでにGoogleアカウントにログインしている場合、メールアドレスやパスワードを入力する必要はありません。認証はFirebase AuthenticationのGoogle認証で行われるため、Googleアカウントの情報がアプリ固有のデータベースに登録されることはありません

Google認証で返されるUID(Googleアカウントを識別するID)とアカウント名のみ、ブラウザのローカルストレージに保存します(次回アクセス時にログイン状態を保持するため)。

新規投稿

ログインするとオリジナルの方程式が新規投稿できます。

新規投稿すると、題名などのカラム情報、作成/更新日時、ログイン情報(UID、アカウント名)をFirestore(Firebase Database)に保存します。

画像ファイルはFirebase Storageにアップロードし、URLをFirestoreに保存します。

編集

ログインすると自分が投稿した方程式の編集ができます。

感想

React+Next

実は、最初はReact+Nextを勉強していました。Reactのコンポーネントという概念は、これまで私が学んできたWebアプリの実装方式とは根本的に異なるものでした。

これまではHTML+CSSで構築したDOMがベースにあり、JavaScriptで動的にDOMを操作するという流れでしたが、ReactはベースのDOMからJavaScriptで構築します。あのHTMLの階層構造がベースという考え方が染み付いている私には、Reactの考え方は一朝一夕には身に付きそうにないと感じました。

とはいえ、仮想DOMという概念は今後Webアプリ界隈でデファクトスタンダードになっていくと思うし、後述するVue+Nuxtでも根本的な考え方(コンポーネントと仮想DOM)は同じだと思っています。

Reactの公式サイトで基本を勉強し、それからReact RouterとReduxを勉強しました。このReduxにも頭を悩ませられました。いくつもの記事を読み漁ってようやくエッセンスくらいは取り込めたかなという感じです。

Reduxの解説記事は以下がわかりやすかったです。図があると理解度が全然違います。Qiitaの記事は難しかった。

イメージで覚えるReact + Redux - ユニファ開発者ブログ

Nextはフレームワークのため、ルーティングといった共通的な部分はすでに組み込まれているので、その点は使いやすいと感じました。ただ、やはりベースのDOMをJSX記法で書いていくということに慣れませんでした。

Vue+Nuxt

React+Nextに高い障壁を感じつつ、次にVue+Nuxtを勉強しました。Vue+NuxtはReact+Nextとは異なり、HTML部分はJavaScriptと切り離して書けるので、凝り固まった私の頭でもすらすらと書ける点が良かったです。

こんなにもゴリゴリとJavaScriptを書いたのは初めてでしたが、事前に予習していたおかげでスラスラと書くことができました。

コールバック関数やメソッドチェーンで階層構造がすごいことになっていくのはJavaScriptを書いている以上、仕方ないことなんでしょうか。自分なりの改行とインデントのルールを決めて慣れていく必要がありそうです。

  likeCard({ commit }, payload) {
    return new Promise((resolve, reject) => {
      cardRef.where('id', '==', payload.card.id).get()
        .then(snapshot => {
          snapshot.forEach(doc => {
            cardRef.doc(doc.id).update({ like: payload.card.like })
              .then(ref => {
                resolve(true)
              })
              .catch(error => {
                console.error('An error occurred in likeCard(): ', error)
                reject(error)
              })
          })
        })
      })
  },

Firebase

機能の紹介で書いたとおり、Firebaseの各機能を存分に活用させてもらいました。まず、デプロイはものすごく楽でした。Firebase CLIを入れて、認証して、初期設定して、後はアップロードするだけ。

Firestoreは一般的なDBとは似て非なるものなので注意しなければなりません。

import firebase from '~/plugins/firebase'

const db = firebase.firestore()
const cardRef = db.collection('cards')

// 下記はエラーになる
cardRef.where('id', '!=', payload.card.id).get().then(snapshot => (...)}

Firestoreでクエリを実行する際、比較演算子に!=<>は使えません。

クエリの制限事項

!= 句が含まれるクエリ。この場合は、「より大きい」クエリと「より小さい」クエリに、クエリを分割する必要があります。たとえば、クエリ句 where("age", "!=", "30") はサポートされませんが、2 つのクエリ(句 where("age", "<", "30") が含まれるクエリと句 where("age", ">", 30) が含まれるクエリ)を結合することで、同じ結果セットが得られます。

Cloud Firestore で単純なクエリと複合クエリを実行する  |  Firebase

フィールドがnumberじゃなかったらどうすればいいんでしょうか? 実際、私はstringのフィールドに対して!=を使いたいシーンがありました。結局、取得したデータに対してsomeifを使って対象データを除外することにしました。

追記

Firebase Storageは管理コンソールからファイルの確認ができました。私の勘違いでしたので、以下の文章はスルーしてください。

Firebase Storageは、Amazon S3やGCPのように、管理コンソールからファイルの確認などができません。間違ってファイルをアップロードしてしまっても削除することはできません。

私は開発中に何度も画像をアップロードしていたので、Firebase Storageには大量のゴミファイルが残ってしまっていました。それでは気持ち悪いので、ファイル一覧を取得し、不要なファイルを削除する簡易的な管理ページを作成しました。

  listFile({ commit }, payload) {
    commit('clearItems')

    return new Promise((resolve, reject) => {
      firestorage.ref('images/').listAll()
        .then(snapshot => {
          snapshot.items.forEach(item => {
            item.getDownloadURL()
              .then(url => {
                commit('addItem', { name: item.name, url: url })
              })
          })
          resolve(true)
        })
        .catch(error => {
          console.log('An error occurred in listFile(): ', error)
          reject(error)
        })
    })
  },
  deleteFile({ commit }, payload) {
    return new Promise((resolve, reject) => {
      firestorage.ref('images/').child(payload.name).delete()
        .then(() => {
          resolve(true)
        })
        .catch(error => {
          console.log('An error occurred in deleteFile(): ', error)
          reject(error)
        })
    })
  }

Firebaseに関しては「なんでこうなってるの?」と思うことがちょくちょくありました。現在はGoogleによって運営されていますが、管理コンソールが重かったりするので、Googleとしてもあまり注力していないサービスなのかもしれません。

まとめ

Vue+Nuxtは開発していて楽しかったので、これからも勉強は続けようと思います。次はTypeScriptの勉強をして、Vue+Nuxt+TypeScriptでなにか作りたいなと思っています。

ここまで読んでいただきありがとうございました。