Vue.jsとfirebaseでログイン認証付き画像投稿サービスをシンプルに作りたい


目的(purpose)

Vue.jsとfirebaseでなにか簡単なアプリを勉強がてら作ってみようと思って今回まとめました。
リアルタイムでやりとりするアプリに関してチャットアプリを考えたのですが結構やられているので今回は画像をリアルタイムで共有するアプリを作成しようと思います。
また、せっかくなのでログイン認証も追加しちゃおうというのが今回のまとめです。

firebaseのアカウントが既に用意してあることが前提となります。
追加の仕方は以下のリンクに載せています。


開発環境(Development environment)

OS:

  • Windows10 Home

ツール:

  • @vue/cli 4.2.3
  • firebase 8.10
  • 扱うfirestore

f:id:fetchkun:20200518190113p:plain

  • 扱うstorage

f:id:fetchkun:20200518190127p:plain


ディレクトリ構成(Directory structure)

project/
     ├ src/
     │   ├ components/
     │   │      ├ js/
     │   │      │ └ picture.js
     │   │      │
     │   │      ├ Picture.vue
     │   │      └ Signin.vue
     │   │
     │   ├ plugins/
     │   │   └ firebase.js
     │   │
     │   ├ router/
     │   │    └ index.js
     │   │
     │   └ App.vue
     │
     └ .env

※今回使用するファイルのみ表示しています。


firebaseの設定(firebase setting)

firebaseからapikey等を取得してvueとfirebaseを連携させます。
ただ、apikeyなどはenvファイルに管理してjavascriptファイル内で環境変数を扱っていきます。


src/plugins/firebase.js

import firebase from 'firebase/app'
import 'firebase/firestore'
import 'firebase/auth'
import 'firebase/storage'

const config = {
  apiKey: process.env.VUE_APP_API_KEY,
  authDomain: process.env.VUE_APP_AUTH_DOMAIN,
  databaseURL: process.env.VUE_APP_DATABASE_URL,
  projectId: process.env.VUE_APP_PROJECT_ID,
  storageBucket: process.env.VUE_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.VUE_APP_MESSAGING_SENDER_ID,
  appId: process.env.VUE_APP_APP_ID,
  measurementId: process.env.VUE_APP_MEASUREMENT_ID
}

const firebaseApp = firebase.initializeApp(config)

export const fb = firebase

export const db = firebaseApp.firestore()

export const auth = firebaseApp.auth()

export const storage = firebaseApp.storage().ref()


.env

VUE_APP_API_KEY = 'firebaseのapiキー'
VUE_APP_AUTH_DOMAIN = 'firebaseのauth domain'
VUE_APP_DATABASE_URL = 'firebaseのdatabaseのurl'
VUE_APP_PROJECT_ID = 'firebaseのプロジェクトID'
VUE_APP_STORAGE_BUCKET = 'firebaseのストレージバケット'
VUE_APP_MESSAGING_SENDER_ID = 'firebaseのid'
VUE_APP_APP_ID = 'firebaseのapp id'
VUE_APP_MEASUREMENT_ID = 'firebaseのid'


表示画面(Display screen)

せっかくなので画面を作成するのに簡単に小奇麗したいのでVuetifyを導入します。
vuetifyjs.com

$ vue add vuetify

上記のコマンドを実行します。

メイン画面(main)

src/App.vue

<template>
  <v-app>
    <router-link to="/picture">picture</router-link>
    <router-view/>
  </v-app>
</template>

<script>

export default {
  name: 'App',
};
</script>

http://localhost:8080/にアクセスしたときに最初に表示される画面です。
vuetifyを入れたときにいろいろなコードが記載されていたと思いますが、
すべて消してpicture画面に遷移するリンクのみ表示させます。

サインイン画面(Signin)

src/components/Signin.vue

<template>
  <div>
    <h2>サインイン</h2>
     <v-form ref="form">
       <v-text-field
        v-model="email"
        label="email"
      ></v-text-field>
      <v-text-field
        v-model="password"
        type="password"
        label="password"
      ></v-text-field>
      <v-btn @click="signin">
        Signin
      </v-btn>
    </v-form>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'signin',
  data () {
    return {
      email: '',
      password: ''
    }
  },
  methods: {
    signin: function () {
      firebase.auth().signInWithEmailAndPassword(this.email, this.password)  // 1
      .then(user => {
        console.log(user)
        this.$router.push('/picture')
      })
    }
  },
}
</script>

  • 1: サインインのフォームにメールアドレスとパスワードを入力し実行すると、
    firebaseの認証方法でメール/パスワードを有効にしていればサインインすることができます。

画像画面(Picture)

src/components/Picture.vue

<template>
  <div>
    <v-row>
      <v-col cols="12" sm="6" offset-sm="3">
        <v-card>
          <v-container fluid>
            <v-row>
              <v-col
                v-for="(img, idx) in imgs"
                :key="idx"
                class="d-flex child-flex"
                cols="4"
              >
                <v-card flat tile class="d-flex">
                  <v-img
                    :src="img"
                    aspect-ratio="1"
                    class="grey lighten-2"
                  >
                    <template v-slot:placeholder>
                      <v-row
                        class="fill-height ma-0"
                        align="center"
                        justify="center"
                      >
                        <v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
                      </v-row>
                    </template>
                  </v-img>
                </v-card>
              </v-col>
            </v-row>
          </v-container>
        </v-card>
      </v-col>
    </v-row>
    <v-form ref="form">
      <input type="file" v-on:change="selectImg"/>  // 2
      <v-btn @click="addImgs">投稿する</v-btn>
    </v-form>
    <v-btn @click="signout">サインアウト</v-btn>
  </div>
</template>

<script>
import {auth} from '../plugins/firebase'
import {getSnapShot, uploadImgs} from './js/picture'

export default {
  name: "Picture",
  data: () => ({
    imgs: null,
    imgsSelected: null,
    uid: '',
  }),
  methods: {
    signout: function () {
      auth.signOut().then(() => {
        this.$router.push('/signin')
      })
    },
    selectImg: function (img) {
      this.imgsSelected = img.target.files  // 3
    },
    addImgs: function () {
      uploadImgs(this.uid, this.imgsSelected)  // 4
    }
  },
  async mounted () {
    this.uid = auth.currentUser.uid
    await getSnapShot(this.uid, (data) => {   //5
      this.imgs = data
    })
  }
}
</script>

<v-row>~</v-row>内に記述しているコードはVuetifyのImagesタブのgridのコードをほぼそのまま使用しています。
Image component — Vuetify.js

  • 2: selectImgは何か画像ファイルを選択したとき、呼び出される関数です。
  • 3: 2で呼び出されたとき選択した画像を一時的にdataに保存
  • 4: この関数が呼び出されたときにdata内のimgsSelectedに保存してある画像ファイルをfirebaseのstorageに保存します。
    (実際に保存する処理を実装しているのは別ファイルのsrc/components/js/picture.jsに書いています。)
  • 5: 画像を表示する画面を開いたときfirebase上に保存してある画像を表示するようにします。

画像データの保存(save image data)

src/components/js/picture.js

import { fb, db, storage } from '../../plugins/firebase'

export const getSnapShot = (id, callback) => {  // 6
  db.collection('pictures').doc(id)
  .onSnapshot(query => {
    if (query.exists) {
      callback(query.data().img)
    } else {
      callback([])
    }
  })
}

export const uploadImgs = async (id, imgList) => {
  imgList.forEach(async (img) => {
    await storage.child(`${id}/${img.name}`).put(img)  // 7
    const url = await storage.child(`${id}/${img.name}`).getDownloadURL()  // 8
    await db.collection('pictures').doc(id).update({
      img: fb.firestore.FieldValue.arrayUnion(url)  // 9
    })
  })
}

  • 6: 先ほどの画像を表示する画面を開いたときに設置するスナップショットを準備します。
  • 7: firebaseのstorageに保存する部分です。
  • 8: 画像を表示するためにstorageに保存してある画像のURLをここで取得
  • 9: 画像のURLをfirestoreのimg配列に追加する部分です。


routerの設定(router setting)


src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Picture from '@/components/Picture'
import Signin from '@/components/Signin'
import { auth } from '@/plugins/firebase'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Main',
  },
  {
    path: '/picture',
    name: 'Picture',
    meta: {
      requiresAuth: true,  // 1
    },
    component: Picture
  },
  {
    path: '/signin',
    name: 'Signin',
    component: Signin
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

router.beforeEach((to, from, next) => {
  const { requiresAuth } = to.meta  // 2
  if (requiresAuth) {
    auth.onAuthStateChanged(function (user) {  // 3
      if (user) {
        next()
      } else {
        next({ path: '/signin' })
      }
    })
  } else {
    next()
  }
})

export default router

  • 1: Vue Routerではルートの定義をする際にmeta情報をつけることができます。
    これを利用することでサインインしていないユーザをはじき、signinページに遷移させることができます。
  • 2: beforeEach関数の引数のtoには1で設定したmeta情報が含まれています。
    metaが設定されていないページ(signinページ等)または既にサインインしているユーザーのみページを閲覧できるという処理が可能です。
  • 3: ここで既にサインインしているユーザはそのまま閲覧でき、サインインしていないユーザはsigninページに遷移させられるようにしています。

以上でhttp://localhost:8080/からpictureリンクを押すとサインイン画面に飛ばされサインインすると画像が表示されていない画面になります。
適当な画像ファイルを選択し、投稿すると下画像のように画像を上げることができます。

f:id:fetchkun:20200518190225p:plain


最後に(Finally)

今回はとりあえず、vue.jsとfirebaseを使ってログイン認証付きのリアルタイム画像投稿サービスみたいなのを簡単に作成してみました。
一部実用的ではないコードもあるかもしれませんが参考になればうれしいです。
ここまで読んでいただきありがとうございます。