現在開発中のVuejsを使ったPWAで、開発当初はログインやデータの保存にDjangoを使っていたけど、ずっと気になっていたFirebaseに変更してみることにした。
2018/8/2 追記
Vue.jsを使ったPWA、yagish 履歴書リリースしました
rirekisho.yagish.jp
追記ここまで
クライアント側のコードはほぼそのまま流用し、機能も少なかったので、1日程度でとりあえず動いた
とりあえず移行した機能は下記。
- Googleログイン(Firebase Authentication)
- ユーザーごとのデータバックアップ+ロード(Cloud Firestore ※まだベータ)
もちろん移行できなかった、しなかった機能もある。
- PDF生成サーバー(LinuxでヘッドレスChromeを使うため)
- アップロードされた画像をリサイズしてBase64で返すもの(Djangoのまま、これは自分のJS力次第でCloud Functionsで行けそう)
印象としては、もうモバイルに限らず、新規WEBアプリはこれでいいんじゃないかという印象。
足りないものはCloud Functionsなり、自分でサーバーを立てる別途GKEで立ててAPIとして使えばほぼ大丈夫そう。
安いし、管理不用で、スケールアウトは実質無限大。
導入のながれとしては、すでにGCPを使っており、プロジェクトもあるのでそのまま進む。
そうするといろいろ設定が表示されるので使う。
webpackでやっているのでnpmでインストール
npm install --save firebase
Vueのプラグインという形でラッパーを作り、どこからでもthis.$firebaseという形で呼び出せるようにした。
やることが増えたら機能ごとに分けたほうが良さそう。
とりあえず動いたって感じなので不用なコピペしたコードが残っており、めっちゃ汚い。
import firebase from 'firebase/app' import 'firebase/auth' import 'firebase/firestore' import VueLocalForage from 'vue-localforage' const _ = require('lodash') // ↓登録後にもらえるやつ const firebaseConfig = { apiKey: 'yourKey', authDomain: 'projectName.firebaseapp.com', databaseURL: 'https://projectName.firebaseio.com', projectId: 'projectName', storageBucket: '', messagingSenderId: '00000000' } firebase.initializeApp(firebaseConfig) // ↓これいれないとなんかエラーでる const storeSettings = { timestampsInSnapshots: true } const saveKeys = ['format_rirekisho', 'basicData', 'yagiProfile', 'recentEdit'] const BACKUP_VERSION = 'v20180511-01' const BACKUP_COLLECTION_KEY = 'backups' const BACKUP_LIMIT_NUM = 10 const FirebaseLib = { install: function (Vue, options) { Vue.use(VueLocalForage) const main = new Vue({ data: function () { return { authProvider: new firebase.auth.GoogleAuthProvider(), db: {}, user: {}, backupList: [] } }, computed: { isAuthenticated: function () { if (this.user && this.user.uid) { return true } else { return false } } }, methods: { signIn: function () { firebase.auth().signInWithRedirect(this.authProvider).then(function (result) { // This gives you a Google Access Token. You can use it to access the Google API. console.log(result) var token = result.credential.accessToken; // The signed-in user info. var user = result.user; app.$message({ message: 'ログインしました:' + user.email, type: 'info' }) // ... }).catch(function (error) { console.log(error) // Handle Errors here. var errorCode = error.code; var errorMessage = error.message; // The email of the user's account used. var email = error.email; // The firebase.auth.AuthCredential type that was used. var credential = error.credential; // ... }) }, signOut: function () { const app = this firebase.auth().signOut().then(function () { // Sign-out successful. app.$message({ message: 'ログアウトしました', type: 'info' }) }).catch(function (error) { app.$message({ message: 'ログアウトに失敗しました', type: 'error' }) console.log(error) }) }, showResultMessage: function (message, type) { this.$message({ message: message, type: type }) }, setupDb: function () { this.db = firebase.firestore() this.db.settings(storeSettings) }, loadBackup: function (id) { this.setupDb() let errors = [] const app = this this.db.collection(BACKUP_COLLECTION_KEY).doc(id).get() .then( function (documentSnapshot) { console.log(documentSnapshot.data()) let data = JSON.parse(documentSnapshot.data().data) _.forEach(saveKeys, function (key) { if (data.hasOwnProperty(key)) { app.$setItem(key, data[key], function (err) { if (err) { console.log(err) errors.push(err) return } console.log('sync-loaded:', key) app.$eventHub.$emit('sync-loaded-' + key, data[key]) }) } }) if (errors.length === 0) { app.$message({ message: 'バックアップを読み込みました' + documentSnapshot.data().label, type: 'success' }) } } ) .catch(function (error) { app.$message({ message: 'バックアップの読み込みに失敗しました', type: 'error' }) console.log(error) }) }, deleteBackup: function(id){ this.db.collection(BACKUP_COLLECTION_KEY).doc(id).delete().then(function() { console.log("Document successfully deleted!"); return true }).catch(function(error) { console.error("Error removing document: ", error); return false }) }, getBackupList: function () { if (!this.isAuthenticated) { return } this.setupDb() const uid = this.user.uid this.backupList = [] const app = this this.db.collection(BACKUP_COLLECTION_KEY) .where('uid', '==', this.user.uid) .limit(20) .orderBy('timestamp', 'desc') .get() .then( function (querySnapshot) { let count = 0 querySnapshot.forEach(function (doc) { // doc.data() is never undefined for query doc snapshots // console.log(doc) if (count < BACKUP_LIMIT_NUM) { app.backupList.push({ id: doc.id, value: doc.data() }) } else { console.log(count, doc.data().label) app.deleteBackup(doc.id) } count++ }); }); }, addBackup: async function (label) { if (!this.isAuthenticated) { this.$message({ message: 'ログインしてください', type: 'error' }) return } let storageData = {} await this.$iterateStorage(function (value, key, iterationNumber) { if (saveKeys.includes(key)) { console.log(key, value) storageData[key] = value } }, function (err) { if (!err) { console.log('Iteration has completed'); } }) if (!storageData) { this.$message({ message: 'データがありません', type: 'error' }) return } var ts = Date.now() this.setupDb() const backupData = { label: label, uid: this.user.uid, timestamp: ts, data: JSON.stringify(storageData), version: BACKUP_VERSION } console.log(backupData) const app = this this.db.collection(BACKUP_COLLECTION_KEY).add(backupData).then( function () { app.$message({ type: 'success', message: '保存成功:' + label }) } ).catch(function (error) { app.$message({ type: 'error', message: '保存失敗:' + label }) }) } } }) firebase.auth().onAuthStateChanged(function (user) { main.user = user }) firebase.auth().getRedirectResult().then(function (result) { console.log('getRedirectResult', result) if (result.credential) { // This gives you a Google Access Token. You can use it to access the Google API. // var token = result.credential.accessToken; main.showResultMessage('ログインしました:' + result.user.email, 'success') // ... } // The signed-in user info. var user = result.user; }).catch(function (error) { // Handle Errors here. var errorCode = error.code; var errorMessage = error.message; // The email of the user's account used. var email = error.email; // The firebase.auth.AuthCredential type that was used. var credential = error.credential; main.showResultMessage('ログインに失敗しました:' + error.message, 'error') }); Vue.prototype.$firebase = main } } export default FirebaseLib
これをmain.jsで読み込む。
import FirebaseLib from './assets/lib/FirebaseLib' Vue.use(FirebaseLib)
はまったところ
Cloud Firestoreの権限
バックアップはログインしたユーザーで、自分のしか見れたり、作れたりしないといけない。
ベータのせいか、あんまりドキュメントがなく、下記のような形でひとまず動いている。
service cloud.firestore { match /databases/{database}/documents { match /backups { allow list: if request.auth.uid != null && request.query.limit <= 20; match /{backupid} { allow read, delete: if request.auth.uid == resource.data.uid; allow write: if request.auth.uid == request.resource.data.uid; } } } }
まずdatabaseとかdocumentsとか、collectionとか独自の概念ではまる。
listを付けないと一覧を取る際にエラーになるところではまる。
次に追加するデータはrequest.resource.data.uid、すでにあるのはresource.data.uidの違いがわからずはまる。
あと更新にしばらく時間がかかる上に、更新サれたかどうかも分からないのでちょっと手が止まる。
と、慣れないことなので大分時間をくったけど、こういう細かい権限管理はRDBではできないのですげー便利だと感じた。
ログイン状態を取るには非同期が必須
ページ読み込み時やvueのテンプレート上、mountedなどで現在のユーザー情報を取れなくて困ったが公式に書いてあった。
Firebase でユーザーを管理する | Firebase
firebase.auth().onAuthStateChanged(function (user) { main.user = user })
これでユーザー状態をVueの監視下にいれておけば便利。
1日程度のちょっとした勉強で強大な力とスピードを得られた気がする。
Vue.js + Firebaseで個人開発もやってみたいと思う。