LoginSignup
17
21

More than 3 years have passed since last update.

Firebase CloudStorageを使ってブログのサムネイルは画像を設定する【Vue.js】

Posted at

Cloud Storage

Firebaseの提供する機能の一つである、CloudStorageを利用します。
Cloud Storageは写真や動画など、ユーザーが作成したコンテンツを保管、提供します。
AWSのS3のように使用できます。

Vue CLIアプリ作成

いつもどおりVue CLIによりVueアプリを作成します。

vue create firebase-app

どうせ使うのでRouterVuexも入れておきましょう。

? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features
? Check the features needed for your project: 
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

firebase初期化

FireStoreモジュールをnpmでインストールします。

npm install firebase

src/plugins/firebase.jsファイルを作成して初期化コードを作成して、export defautしてアプリケーションでfirebaseを利用できるようにします。

src/plugins/firebase.js
import firebase from 'firebase/app'

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
      apiKey: process.env.VUE_APP_APIKEY,
      authDomain: process.env.VUE_APP_AUTHDOMAIN,
      databaseURL: process.env.VUE_APP_DATABASEURL,
      projectId: process.env.VUE_APP_PROJECTID,
      storageBucket: process.env.VUE_APP_STORAGEBUCKET,
      messagingSenderId: process.env.VUE_APP_MESSAGINGSENDERID,
      appId: process.env.VUE_APP_APPID,
      measurementId: process.env.VUE_APP_MEASUREMENTID,
    }
  )
}

export default firebase

APIキーはクライアントアプリケーションで利用される前提なので、そのまま書いても構いませんが、なんとなく.env.localから読み込みます。

env.local
VUE_APP_APIKEY=MY_API_KEY
VUE_APP_AUTHDOMAIN=MY_AUTHDOMAIN
VUE_APP_DATABASEURL=MY_DATABASEURL
VUE_APP_PROJECTID=MY_PROJECT_ID
VUE_APP_STORAGEBUCKET=MY_STORAGE
VUE_APP_MESSAGINGSENDERID=MY_MESSAGER
VUE_APP_APPID=MY_APPID
VUE_APP_MEASUREMENTID=MY_MEMEASUREMENTID

Storageの設定を初期化する

storageをモジュールで初期化して利用します。
src/plugins/storage.jsに以下のようにかきます。

src/plugins/storage.js
import firebase from '@/plugins/firebase'

export const storage = firebase.storage()

Cloud Storageを利用する

実際にStorageを利用してみます。
例として、ブログ作成CMSでサムネイルを設定する機能を作成します。

スクリーンショット 20200419 22.44.31.png

機能として、新しい画像を新たにアップロードしてサムネイルを設定するか、ブログ内で使用した画像をサムネイルに設定できるようにします。

画像をアップロードする

まずは、画像をアップロードしてサムネイルを設定する機能を作成します。

<template>は次のようになっています。


<template>
    <v-dialog v-model="dialog" scrollable max-width="600px">
      <template v-slot:activator="{ on }">
        <v-btn color="primary" class="ma-5" v-on="on">
          <v-icon>fas fa-image</v-icon>
          サムネイルの設定
        </v-btn>
      </template>
      <v-card>
        <v-card-title>サムネイルの設定</v-card-title>
        <v-divider></v-divider>
        <v-progress-linear
          v-model="fileLoading"
          stream
        ></v-progress-linear>
        <v-card-text style="height: 600px;">
          <v-card-subtitle>現在のサムネイル</v-card-subtitle>
          <v-row>
            <v-col cols=4>
              <v-img
                :src="thumbnail"
                alr="サムネイル"
                width=200
                height=200
              ></v-img>
            </v-col>
            <v-col cols=8>
              <v-file-input accept="image/*" label="画像をアップロードして設定する。" @change="onFileUpload"></v-file-input>
              <v-btn color="error" @click="deleteThumbnail">サムネイルを削除する</v-btn>
            </v-col>
          </v-row>

注目すべき点は、<v-file-input>によってファイルがアップロードされたら、onFileUploadイベントが発火するところです。
(<v-file-input><input type="file">を提供するVuetifyのコンポーネントです。)

onFileUploadイベントを見ていきます。

onFileUpload(file) {
    const fileType = this.getFileType(file)
      if (!fileType) {
        this['flash/setFlash']({
          message: 'ファイルタイプが不正です。',
          type: 'error'
        })
      }
      const storageRef = storage.ref(`articles/${this.article.id}/thumbnail.${fileType}`)
      const uploadTask = storageRef.put(file)
      uploadTask.on('state_changed', 
          snapshot => {
            const percentage = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
            this.fileLoading = percentage
          },
          err => {
            console.log(err)
            this['flash/setFlash']({
              message: 'ファイルのアップロードに失敗しました。',
              type: 'error'
            })
          },
          () => {
            uploadTask.snapshot.ref.getDownloadURL().then(downloadURL => {
              this.fileLoading = 0
              this.thumbnail = downloadURL
            })
          }
        )
    },

@changeイベントはfile APIを受け取ります。
getFileTypethis['flash/setFlash']は独自で作成したメソッドです。
ファイルの拡張子を取得する機能と、フラッシュメッセージ機能を提供します。

ファイルの参照を作成

ファイルをCloud Storageにアップロードするには、まずファイル名を含むファイルの完全パスへの参照を作成します。

ファイルの参照を作成するために、storage.ref()メソッドを利用します。

const storageRef = storage.ref(`articles/${this.article.id}/thumbnail.${fileType}`)

ファイルをアップロード

参照を作成したら、storageRef.put()メソッドでファイルをアップロードします。

put()は先程取得したfile APIBlob API経由でファイルを取得し、Cloud Storageにアップロードします。

const uploadTask = storageRef.put(file)

ファイルのアップロード状況を監視する

Cloud Storageでファイルをアップロードしている最中には、アップロード状況を監視することができます。

uploadTask.on('state_changed', 
          snapshot => {
            const percentage = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
            this.fileLoading = percentage
          },
          err => {
            console.log(err)
            this['flash/setFlash']({
              message: 'ファイルのアップロードに失敗しました。',
              type: 'error'
            })
          },
          () => {
            uploadTask.snapshot.ref.getDownloadURL().then(downloadURL => {
              this.fileLoading = 0
              this.thumbnail = downloadURL
            })
          }
        )

進行率を取得するには、転送済みのバイト数(snapshot.bytesTransferred)をアップロード予定のバイト数(snapshot.totalBytes)の合計で割って100をかけて算出します。

2つ目の関数では、エラー処理を担当することになります。

最終的にアップロードが完了した場合には、3つ目の関数が呼び出されます。
アップロードが完了したらgetDownloadURLメソッドから、画像へアクセスするためのURLを取得できます。

このURLを新たにサムネイルプロパティに設定したとき、watchイベントが発火します。


watch: {
    thumbnail (newval) {
      this.$emit('onThubnailChanged', newval)
    }
  }

watchはデータの変更を監視します。
thumbnailプロパティが変更したとき、それを親コンポーネントに通知することで、親コンポーネントにサムネイルの変更の永続化を担ってもらいます。(おそらく、FireStoreに保存することになるでしょう)

ブログ内で使用した画像をサムネイルに設定できるようにする。

こんどは、ブログ内で使用した画像を取得して設定するよういします。

次のような<template>です。

          <v-card-subtitle>記事内の画像から設定する。</v-card-subtitle>
          <v-row>
            <v-col cols=12>
              <v-card>
                <.div v-if="loading">
                  画像データの取得中...
                  <v-progress-circular indeterminate color="red"></v-progress-circular>
                </div>
                <div>
                  この記事に画像は使われていません。
                </div>
                <v-container fluid v-else>
                  <v-row>
                    <v-col
                      v-for="(image, index) in images"
                      :key="index"
                      :index="index"
                      class="d-flex child-flex"
                      cols="4"
                    >
                      <v-card flat tile class="d-flex">
                        <v-img
                          :id="index"
                          :src="image"
                          aspect-ratio="1"
                          :class="{ selected: isSelected(index) }"
                          @click="onClick"
                        >
                          <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>

images配列をv-forでループして、使用した画像一覧を表示します。
images配列に画像を渡しましょう。

画像のリストをダウンロードする

はじめに、Cloud Storageのフォルダの構成は次のようになっています。
記事内で使用されたファイル一覧は以下のパスで取得できます。

articles/articleId/files

スクリプト部分は次のようになっています。

import { storage } from '@/plugins/storage'

export default {
  name: 'thumbnail-setting-dialog',
  props: {
    article: {
      type: Object,
      required: true
    },
  },
  data() {
    return {
      thumbnail: this.article.thumbnail,
      loading: true,
      error: false,
      dialog: false,
      images: [],
      selectedImage: '',
      fileLoading: 0,
    }
  },
  async created() {
    try {
      const storageRef = await storage.ref(`articles/${this.article.id}`)
      const res = await storageRef.listAll()
      res.items.forEach(itemRef => {
        itemRef.getDownloadURL().then(url => {
          this.images.push(url)
        })
      })
    } catch(e) {
      this.error = true
      console.log(e)
    } finally {
      this.loading = false
    }
  },

ファイルを取得する機能は、createdメソッドで発動します。createdメソッドはVue.jsのライフサイクルの一つです。インスタンスが生成されたときにフックされるため、APIの通信など一番初めに処理を実行したいときに呼び出します。

ストレージの参照を作成する

まずはじめに、ファイルへアクセスするための参照を作成します。Cloud Storageは、普段使い慣れているファイルシステムと同じように、階層構造になっています。
storage.ref()メソッドを使用して、現在の記事の参照まで移動しましょう。
そのために、次のように参照を作成しました。前提条件のファイル構成を参考にしてください。

storage.ref(`articles/${this.article.id}`)

this.article.idpropsで親から受け取ったデータになります。予想通り、現在編集している記事のIDが入っているので、これで正しく参照を作成することができます。

ディレクトリのファイル一覧を取得する

参照を作成できたので、ディレクトリの中に存在するすべてのファイルを取得しましょう。
listAll()メソッドで取得することができます。

const res = await storageRef.listAll()

取得に成功したファイル一覧は、forEachを利用して処理します。
個々のファイルの情報からは、ファイルをアップロードしたときと同じようにgetDownloadURL()メソッドから画像のURLを取得することができます。

取得したURLはdataプロパティのimages配列に格納しておきます。

res.items.forEach(itemRef => {
   itemRef.getDownloadURL().then(url => {
      this.images.push(url)
    })
  })

取得した画像一覧を表示する

ここまでの処理の中でエラーがなければ、記事に使用されている画像のURLの一覧が取得できたはずです。
このままではなにも表示されないので、画像を表示するHTML部分を実装しましょう。
全体像は次のようになっています。

<v-row>
  <v-col cols=12>
    <v-card>
      <.div v-if="loading">
        画像データの取得中...
        <v-progress-circular indeterminate color="red"></v-progress-circular>
      </div>
      <.div v-else-if="images.length === 0">
        この記事に画像は使われていません。
      </div>
      <v-container fluid v-else>
        <v-row>
          <v-col
            v-for="(image, index) in images"
            :key="index"
            :index="index"
            class="d-flex child-flex"
            cols="4"
          >
            <v-card flat tile class="d-flex">
              <v-img
                :id="index"
                :src="image"
                aspect-ratio="1"
                :class="{ selected: isSelected(index) }"
                @click="onClick"
              >
                <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-ifディレクティブにより、ローディング中にはプログレスサークルが表示されます。
もし、一つも画像が記事内で使用されていないのであれば、その旨を表示してあげるのが丁寧でしょう。

<.div v-if="loading">
  画像データの取得中...
  <v-progress-circular indeterminate color="red"></v-progress-circular>
</div>
<.div v-else-if="images.length === 0">
    この記事に画像は使われていません。
</div>

画像が表示できるのなら、v-forディレクティブによりすべての画像を表示させましょう。
その際に、Vuetifyのグリッドシステムを利用します。
v-containerの中は、12個のカラムに分割されるので、v-colcolsに4を指定すれば、4 + 4 + 4ということで、画像が3つづつに分割されます。

<v-container fluid v-else>
  <v-row>
    <v-col
      v-for="(image, index) in images"
      :key="index"
      :index="index"
      class="d-flex child-flex"
      cols="4"
    >
      <v-card flat tile class="d-flex">
        <v-img
          :id="index"
          :src="image"
          aspect-ratio="1"
          :class="{ selected: isSelected(index) }"
          @click="onClick"
        >
          <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>

グリッドシステムによる画像の配置は公式ドキュメントを参考にしました。

画像URLをv-imgsrcプロップスに渡せば画像を表示してくれます。
v-imgコンポーネントは優れもので、特に気にせずとも画像を遅延読み込みしてくれます。

選択された画像をサムネイルに設定する

画像一覧が表示されたので、画像が選択されたらサムネイルに設定されるようにしましょう。
v-img@clickイベントハンドラを用いてクリックイベントを購読しましょう。

onClick(e) {
  this.selectedImage = +e.target.parentElement.id
  this.thumbnail = this.images[e.target.parentElement.id]
},

v-imgには配列のインデックスをIDとして付与しているので、イベント引数から取得します。(v-imgコンポーネントを利用している都合上、画像自体にIDが不要されていないので、その親要素から取得しています。)

e.terget.parantElement.idの前についている奇妙な+記号は、文字列を数値へ変換するためのものです。
算術演算子の単項プラスはオペランドを評価する際に数値へ変換するので、値を数値へ変換する際に利用されます。
算術演算子 - JavaScript | MDN

後はインデックスから配列の中の選択された画像のURLを取得することができるので、これをthumbnailに設定すればOKです。

選択された画像を強調する

最後に、画像一覧から画像が選択された場合わかりやすいように黄色い枠で囲むようにしてみましょう。こういう感じです。

スクリーンショット 20200426 23.34.47.png

スクリーンショット 20200426 23.35.54.png

ボーダースタイルを定義する

画像を枠で囲むには、borderスタイルを適用します。次のようなCSSを書きます。

<style scoped>
.selected {
  border: medium solid #FFEB3B
}
</style>

このスタイルを、現在選択されている画像に適用すればいいわけです。

そのための機能がVue.jsには用意されています。クラスとスタイルのバインディング

:class(v-bind:class)の省略構文にオブジェクトを渡すと、クラスを動的に切り替えることができます。今回の例をもう一度上げましょう。

<v-img
    :id="index"
    :src="image"
    aspect-ratio="1"
    :class="{ selected: isSelected(index) }"
    @click="onClick"
>

オブジェクトのキーには適用させたいクラス名を渡し、プロパティデータには真偽値を渡します。つまり、isSelected(index)trueを返した場合には、selectedクラスが適用され先程定義されたスタイルが適用されます。

isSelectedは算出プロパティとして定義されています。

computed: {
   isSelected() {
     return index => this.selectedImage === index
  },
},

算出プロパティで引数を受け取るために、関数をreturnしています。
this.selectedImageには画像がクリックされたときにクリックされた画像のindexが入っているのでした。

算出プロパティは依存する値(今回はthis.selectedImage)が変更されるたびに新たな値を返すので、他の画像がクリックされるたび返す値が変わることになります。

これで、選択された画像にスタイルを適用させて強調させることができました。
この例で見たとおり、クラスバインディングは算出プロパティと組み合わせることで強力な効果を発揮します。

おわりに

CloudStorageを利用して、画像をアップロード、ダウンロードする方法を紹介しました。
かなりシンプルに利用することができるので、ぜひ使ってみてください。

17
21
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
17
21