Cloud Storage
Firebaseの提供する機能の一つである、CloudStorageを利用します。
Cloud Storage
は写真や動画など、ユーザーが作成したコンテンツを保管、提供します。
AWSのS3のように使用できます。
Vue CLIアプリ作成
いつもどおりVue CLI
によりVueアプリを作成します。
vue create firebase-app
どうせ使うのでRouter
とVuex
も入れておきましょう。
? 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
を利用できるようにします。
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
から読み込みます。
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
に以下のようにかきます。
import firebase from '@/plugins/firebase'
export const storage = firebase.storage()
Cloud Storageを利用する
実際にStorageを利用してみます。
例として、ブログ作成CMSでサムネイルを設定する機能を作成します。
機能として、新しい画像を新たにアップロードしてサムネイルを設定するか、ブログ内で使用した画像をサムネイルに設定できるようにします。
画像をアップロードする
まずは、画像をアップロードしてサムネイルを設定する機能を作成します。
<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を受け取ります。
getFileType
とthis['flash/setFlash']
は独自で作成したメソッドです。
ファイルの拡張子を取得する機能と、フラッシュメッセージ機能を提供します。
ファイルの参照を作成
ファイルをCloud Storage
にアップロードするには、まずファイル名を含むファイルの完全パスへの参照を作成します。
ファイルの参照を作成するために、storage.ref()
メソッドを利用します。
const storageRef = storage.ref(`articles/${this.article.id}/thumbnail.${fileType}`)
ファイルをアップロード
参照を作成したら、storageRef.put()
メソッドでファイルをアップロードします。
put()
は先程取得したfile APIやBlob 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.id
はprops
で親から受け取ったデータになります。予想通り、現在編集している記事の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-col
のcols
に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-img
のsrc
プロップスに渡せば画像を表示してくれます。
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です。
選択された画像を強調する
最後に、画像一覧から画像が選択された場合わかりやすいように黄色い枠で囲むようにしてみましょう。こういう感じです。
ボーダースタイルを定義する
画像を枠で囲むには、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
を利用して、画像をアップロード、ダウンロードする方法を紹介しました。
かなりシンプルに利用することができるので、ぜひ使ってみてください。