表題の通りです。
Vue.jsとGoogleのFirebaseを利用して画像を保存するモノを作ってる時に、全然うまくいかなかったので、
その備忘録として残しておきます。
あとで思い返したら単純に書き方が間違ってただけなんですが。。
初心者なので暖かめの目で多めにみて頂けたらと思います。
おかしいところとかあればぜひご指摘ください。
環境
Mac OS X Catalina 10.15.4
Vue.js 2.6.11
Vue CLI 4.3.1
AWS Cloud9 Firebase
やりたい事
買ったコーヒーをメモるためのアプリを作ろうとしてました。
Firebaseに画像を保存し、画面読み込みor更新した際に画面に表示させたい。
その最低限の準備です。
保存の実装
事前にVue CLIにFirebaseを読み込ませます。
yarn add firebase --save
もしくはこちらで
npm install firebase --save
Firebaseの設定はいろいろありますが、こちらの記事を参考に爆速で構築させて頂きました。
Vue.js + Firebase を使って爆速でユーザ認証を実装する
トップ画面に、firebaseに保存した画像を表示させるため、まずは保存します。
<template>
<div class="back">
<div class="register">
<h2>Coffee Register</h2>
<!--名前とか買った店、写真を保存する-->
<input type="text" placeholder="CoffeeName" v-model="coffeeName">
<input type="text" placeholder="ShopName" v-model="shopName">
<input type="file" v-on:change="onFileChange" id="coffee-image" accept="image/jpeg,image/png,image/gif">
<!--保存ボタン 後述のrecordメソッドで保存-->
<button v-on:click="record" class="recordbtn">Record</button>
<!--保存せずに戻るボタン-->
<button v-on:click="toIndex" class="returnbtn">Return</button>
</div>
</div>
</template>
<script>
import firebase from 'firebase' //firebase読み込み
export default {
name: 'CoffeeRegister',
data: function(){
return {
coffeeName: '',
shopName: '',
file: null, // 選択した画像を持っておく変数
coffeeImageLocation: null, //画像を保存する場所のURLを保存する変数
};
},
methods: {
toIndex: function() { //トップページに戻るメソッド
this.$emit('return-click-register', (false));
},
// ここから画像ファイルを保存するメソッド
onFileChange: function(e) {
const image = e.target.files; //選択された画像ファイルを選択
this.file = image[0]; //画像ファイルを1つだけ選択
// Firebase storageに保存するパスを決める
this.coffeeImageLocation = `coffee-images/${this.coffeeName}`;
},
// firebase storageに保存するメソッド
record: function() {
//画像をfirebase storageに保存
firebase
.storage()
.ref(this.coffeeImageLocation) //さっき決めたパスを参照して、
.put(this.file) //保存する
.then(() => {
//保存が成功したら、保存した画像ファイルの場所とともにfirebase databaseに保存する準備
const coffeeData = {
name: this.coffeeName,
shop: this.shopName,
coffeeImageLocation: this.coffeeImageLocation,
createdAt: firebase.database.ServerValue.TIMESTAMP,
};
// ここでfirebase databaseに保存する
firebase
.database()
.ref('beans') //保存する場所を参照して、
.push(coffeeData) //追加で保存 setメソッドを使うと上書きされる
.then(() => {
console.log('saved coffee data.');
alert('data is registration.');
this.$emit('success-coffee-registration', false); //親コンポーネントに伝達
})
.catch((error) => {
console.error('cannnot saved.', error);
});
})
.catch((error) => {
console.error('image uproad error.', error);
});
}
}
}
</script>
firebaseには独特のメソッドがあるし、JSとかと似てるからちょっと困る時がある。
メインページで保存したデータ読み込み
次にトップ画面で読み込みを実装します。
<template>
..省略..
<div class="coffee-item">
<div class="coffee-image-wrapper">
<img class="coffee-item-image" alt="" v-bind:src="image">
</div>
<div class="coffee-detail">
<div class="coffee-item-name">コーヒー</div>
<div class="coffee-item-delete-wrapper">
<button class="btn btn-danger coffee-item-delete">削除</button>
</div>
</div>
</div>
..省略..
</template>
<script>
..省略..
import CoffeeRegister from '@/components/CoffeeRegister.vue';
export default {
...
name: 'Index',
components: {
...
CoffeeRegister,
},
data: function () {
return {
...
lct: '',
image: '',
};
},
created: function() {
...
this.loadCoffeeView();
this.downloadCoffeeImages(this.lct);
},
methods: {
// firebase databaseからコーヒーデータをダウンロード
loadCoffeeView: function() {
const coffeeRef = firebase
.database()
.ref('beans') //firebase database の beans に保存したデータを参照
.orderByChild('createdAt'); //並び替え
// 過去に登録したイベントハンドラを削除
coffeeRef.off('child_added');
// ここで保存データを抜き取り
coffeeRef.on('child_added',(coffeeSnapshot) => {
const coffeeId = coffeeSnapshot.key;
const coffeeData = coffeeSnapshot.val();
this.lct = coffeeData.coffeeImageLocation; //ここでURLが抜き取られる
});
},
//firebase storageからコーヒーの画像データをダウンロード
downloadCoffeeImages: function(coffeeImageLocation) {
firebase
.storage()
.ref(coffeeImageLocation)
.getDownloadURL()
.then((data) => {
console.log(data);
this.image = data;
})
.catch((error) => {
console.error('cannot download coffee images.', error);
});
},
};
};
</script>
全体像としては上記なのですが、要はライフサイクルフックのcreatedで画像やらデータを読み込んで表示させたかったんです。
data: function () {
return {
...
lct: '',
image: '',
};
created: function() {
...
// ここのメソッドで画像URLを[ lct ]に格納して、
this.loadCoffeeView();
// ここで画像を抜き取って[ image ]に格納して、v-bindで表示させる予定
this.downloadCoffeeImages(this.lct);
},
エラー内容
めっちゃ抽象的で困りました。
createdフックでエラーなのはわかります。
試した事
じゃcreatedじゃなかったらいいの?と思ったので、
mountedで記述したんですが、全く同じエラー。
単体で確認しようと思い、v-on:clickディレクティブ使って確認したらちゃんと動いてました。
なんで単体で動くのにライフサイクルフックで動かないの?
と思って、データ抽出メソッドをcreatedにして、URL抽出メソッドをmountedにしてみたら、
mountedで同様のエラーが出ました。
ここのどこかが悪いってわかったので、いろいろ試してみます。
//firebase storageからコーヒーの画像データをダウンロード
downloadCoffeeImages: function(coffeeImageLocation) {
firebase
.storage()
.ref(coffeeImageLocation)
.getDownloadURL()
.then((data) => {
console.log(data);
this.image = data;
})
.catch((error) => {
console.error('cannot download coffee images.', error);
});
},
いろいろ試したら、.ref(coffeeImageLocation)
のところで何かがおかしいみたい。
typeof
で、このメソッドに渡される予定の変数がどうなってるか確認したら、undefined。
結果
結論からいうと、前述の画像URLを取得して、変数に格納する前に画像参照するメソッドが動いてたから表示されてなかったみたいです。
<script>
...
// firebase databaseからコーヒーデータをダウンロード
loadCoffeeView: function() {
const coffeeRef = firebase
.database()
.ref('beans')
.orderByChild('createdAt');
// 過去に登録したイベントハンドラを削除
coffeeRef.off('child_added');
coffeeRef.on('child_added',(coffeeSnapshot) => {
const coffeeId = coffeeSnapshot.key;
const coffeeData = coffeeSnapshot.val();
console.log(coffeeData);
const location = coffeeData.coffeeImageLocation;
console.log('coffee data is:', typeof location);
console.log(location);
//firebase storageからコーヒーの画像データをダウンロード
firebase
.storage()
.ref(location)
.getDownloadURL()
.then((data) => {
console.log('data is: ', data);
this.image = data;
})
.catch((error) => {
console.error('cannot download coffee images.', error);
});
});
},
...
</script>
最終的に全部同じメソッドに記述したらちゃんと表示されました。
変にdataオブジェクトに格納しない方がいいんですね。
このへんの事調べてもよく分からなかったので、ご存知の方がいましたら
ぜひ教えて頂けませんか?