Edited at

Vue vuexでfirebaseのログイン保持


Vueチュートリアル vuexでfirebaseのログイン保持

Vue.jsでfirebaseのログイン実装の記事はよく見かけますが、単一コンポーネント内での処理ばかりでした。

なので全体でログイン状態を共有できるようvuexを導入したいと思います。

vuexにデータを入れると、どのコンポーネントからでもアクセスできます。

Vueでプロジェクトの作成からやりたいと思っています。

DEMOページ

・ログイン及びログアウトボタンを表示するコンポーネント

・ユーザー情報を表示するコンポーネント

以上の2つで、vuexのデータへアクセスしています。

image.png


環境

node 8.11.4

npm 5.6.0
vue 3.0.1

npm install -g @vue/cli
npm install -g @vue/cli-service-global

すぐに手元で動かしたい場合は、https://github.com/ErgoFriend/makinaからクローンします。

git clone https://github.com/ErgoFriend/makina.git

cd makina
npm install
vue ui


プロジェクト作成

vue uiが使えるようになったので使っていきます。

環境構築が完了したら、コマンド:vue uiを実行します。

スクリーンショット (49).png

↑ 最初にProject Managerが表示されるのでCreateから好きな場所で下のCreateボタンを押して進めていくとプロジェクト名のフォルダが作られます。

スクリーンショット (50).png

↑ 下に設定がありますが、NEXTします。

スクリーンショット (51).png

↑ ManualでNEXT

スクリーンショット (52).png

↑ BabelとLinterをオフ、RouterとVuexの2つをオンにしてNEXT。DEMOにはPWAも入ってますが必要ないのでオフ。

スクリーンショット (54).png

↑ このままRouterを使うとURLに#が含まれるので、Historyモードをオンにします。

Createボタンをクリック!プリセットの名前を聞かれますのでtutorial-vuex-firebase-authとかにしときます。

スクリーンショット (56).png

↑ 右上のInstall dependencyからMain dependenciesで@firebase/app@firebase/authbuefyの3つをインストールします。

buefyはheaderとかのスタイルに使います。レスポンシブしてくれます。

スクリーンショット (57).png

↑ Run taskを押して、Open appでローカルサーバーのページを開けます。


作っていきます

作業するのはtutorial-makina/srcです。3つの必要なファイルを作っていきます。

構造は以下のようになります。


tutorial-makina/src

│  App.vue

│ main.js
│ router.js
│ store.js
| firebase.js //追加
├─assets
│ logo.png
├─components
│ // HelloWorld.vue を削除します
| Authentication.vue //追加
| Navigation.vue //追加
└─views
About.vue
Home.vue


Vuex+Firebase 初期化と認証とvuex


Vuex store.js

 store.jsにログイン情報、ログイン状態の2つを保持する場所を作ります。


/src/store.js

import Vue from 'vue';

import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
state: {
user: {},
status: false
},
mutations: {
onAuthStateChanged(state, user) {
state.user = user; //firebaseが返したユーザー情報
},
onUserStatusChanged(state, status) {
state.status = status; //ログインしてるかどうか true or false
}
},
getters: {
user(state) {
return state.user;
},
isSignedIn(state) {
return state.status;
}
}
});


vueコンポーネントでif (user)を行うと常にtrueになってしまいます。

state.userは存在しますが、中身が空っぽです。なので、例えばstate.user.nameを取ろうとするとfalseになってくれます。

アドバイスを頂けたので、参考にしてログイン状態だけをもつstatusをつくりました。

初期化がfalseでログインするとtrueになります。


Firebase firebase.js

vueファイルから関数を使用するために、export default{}で囲っています。

export default {

init() {},
login() {},
logout() {},
onAuth() {}
};

configにはfirebase consoleからプロジェクトを作成して入手してください。Firebase を JavaScript プロジェクトに追加する


/src/firebase.js

import firebase from "@firebase/app";

import "@firebase/auth";
import store from "./store";

const config = {
apiKey: "ほげほげ",
authDomain: "ほげほg..."
data.... //省略します!
};

export default {
init() {
firebase.initializeApp(config);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
},
login() {
const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
},
logout() {
firebase.auth().signOut()
},
onAuth() {
firebase.auth().onAuthStateChanged(user => {
user = user ? user : {};
store.commit('onAuthStateChanged', user);
store.commit('onUserStatusChanged', user.uid ? true : false);
});
}
};




  • store.commit('onAuthStateChanged', user)で、store.jsonAuthStateChangedを呼び出してユーザー情報を渡します


  • store.commit('onUserStatusChanged', user.uid ? true : false)store.jsonUserStatusChangedを呼び出してログインしてるかどうかのbool値を渡します

user.uid ? true : falseは分かりやすいとは言えないので、オブジェクトのlengthを使うやり方なども検討してみたいと思います。


init() : おなじみの初期化です。



  • firebase.initializeApp(config) : configを読み込んで初期化します。


  • firebase.auth().setPersistence(xxxxx) : 認証状態の永続性を決めます。認証状態の永続性



    • firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION) : 今回は、タブを閉じるまで。更新しても保持されます。


    • firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE) : 更新したら消えます。デフォルトっぽい?


    • firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL) : ユーザーがログアウトするまで残ります。




login() : SNS認証のGoogleログインを使ってログイン。



  • firebase.auth().signInWithPopup(provider) : プロバイダを指定してポップアップウィンドウでログインさせます。




  • onAuth()が呼ばれます : Promiseでログインが完了した後に、ユーザー情報をvuexのstoreへ保存します。




logout() : ログアウトしてvuexからも削除


onAuth() : コンポーネントでは最初にこの関数が呼ばれます。



  • firebase.auth().signIn...firebase.auth().signOut()が成功して認証状態が変化するとonAuthStateChangedに引っ掛かります。


  • import store from "./store"しているので、store.commitでmutationsの関数を呼び出せる。


  • firebase.auth().onAuthStateChanged(user => {})は呼ばれた時のユーザー情報をuserで返します。



    • user = user ? user : {} : ログアウトしたときはuserは何も入ってないのでfalseとなり、{}が保存されます。


    • user.uid ? true : falseは、uidがログインした時だけ含まれるのを利用してます。




Vue 見た目と挙動

見た目とログインの挙動を作っていきます。src/main.jsでbuefyの読み込みとfirebaseの初期化を行います。


main.js


/src/main.js

import Vue from "vue";

import App from "./App.vue";
import router from "./router";
import store from "./store";

import Buefy from "buefy";
import "buefy/dist/buefy.min.css";
Vue.use(Buefy);

import Firebase from "./firebase";
Firebase.init();

Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");


import "buefy/lib/buefy.css"でエラー2週間前とファイルの配置が変わってました。

プロジェクトトップにあるnode_modules/buefyでcssを探します。

無事発見 buefy/dist/buefy.cssにありました。


App.vue

<Navigation />にヘッダー、<router-view/>にHomeもしくはAboutが描画されます。


App.vue

<template>

<div id="app">
<Navigation />
<router-view/>
</div>
</template>
<script>
import Navigation from "@/components/Navigation.vue";
export default {
name: "home",
components: {
Navigation
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>


Home.vue

トップ画面です。


Home.vue

<template>

<div class="home">
<h1>hello</h1>
</div>
</template>
<script>
export default {
name: "home",
components: {}
};
</script>
<style scoped>
.home {
height: 90vh;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-image: url("https://pbs.twimg.com/media/DgOcLUnU8AEgN56.jpg");
}
</style>


About.vue

ログインユーザーの名前とメールアドレス、uidを表示します。

computedでvuexのstateに保存したユーザー情報を返し、tempalteでアクセスできるようにしています。


About.vue

<template>

<div class="about">
<div class="box">
<div class="media">
<figure class="image is-64x64 media-left">
<img class="is-rounded" :src="user.photoURL">
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>{{ user.displayName }} </strong> <small>{{ user.email }} </small>
<br>
{{ user.uid }}
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item">
<span class="icon is-small"><i class="fas fa-reply"></i></span>
</a>
<a class="level-item">
<span class="icon is-small"><i class="fas fa-retweet"></i></span>
</a>
<a class="level-item">
<span class="icon is-small"><i class="fas fa-heart"></i></span>
</a>
</div>
</nav>
</div>
</div>
</div>
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong>Makina</strong> by <a href="https://twitter.com/ergofriend">ergofriend</a>. The source code is licensed
<a href="http://opensource.org/licenses/mit-license.php">MIT</a>. The website content
is licensed <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY NC SA 4.0</a>.
</p>
</div>
</footer>
</div>
</template>
<script>
export default {
name: "about",
computed: {
user() {
return this.$store.getters.user;
}
},
components: {}
};
</script>
<style scoped>
.box {
margin: 5% 10%;
}
</style>


Authentication.vue

これはログインとログアウト機能をまとめたコンポーネントです。ヘッダーのログイン・ログアウトはこのコンポーネントを使用しています。



  • createdでインスタンス作成後に認証状態の監視を開始します。Vue インスタンス


  • computedでvuexのstatusにログインしているかどうかを保持し、v-ifで分岐できるようにしています。


  • methods: { func() } : ログイン・ログアウトボタンが押されたら@clickに登録した関数を実行します。



    1. import Firebase from "./../firebase" : 最初に作ったfirebase.jsをFirebaseで読み込みます。


    2. doLogin() { Firebase.login() } : firebase.jsの関数を再度名前を付けます。


    3. methodsで関数を作ることで@clickからのログイン・ログアウトを可能にしています。




Authentication.vue

<template>

<div class="authentication">
<!-- ログイン時にはフォームとログアウトボタンを表示 -->
<div v-if="userStatus" key="login" class="navbar-item">
<p class="navbar-item">{{ user.displayName }}</p>
<button type="button" class="button is-small is-info is-outlined" @click="doLogout">
Sign out
</button>
</div>
<!-- 未ログイン時にはログインボタンを表示 -->
<div v-else key="logout">
<button type="button" class="google-button" @click="doLogin">
<span class="google-button__icon">
<svg viewBox="0 0 366 372" xmlns="http://www.w3.org/2000/svg"><path d="M125.9 10.2c40.2-13.9 85.3-13.6 125.3 1.1 22.2 8.2 42.5 21 59.9 37.1-5.8 6.3-12.1 12.2-18.1 18.3l-34.2 34.2c-11.3-10.8-25.1-19-40.1-23.6-17.6-5.3-36.6-6.1-54.6-2.2-21 4.5-40.5 15.5-55.6 30.9-12.2 12.3-21.4 27.5-27 43.9-20.3-15.8-40.6-31.5-61-47.3 21.5-43 60.1-76.9 105.4-92.4z" id="Shape" fill="#EA4335"/><path d="M20.6 102.4c20.3 15.8 40.6 31.5 61 47.3-8 23.3-8 49.2 0 72.4-20.3 15.8-40.6 31.6-60.9 47.3C1.9 232.7-3.8 189.6 4.4 149.2c3.3-16.2 8.7-32 16.2-46.8z" id="Shape" fill="#FBBC05"/><path d="M361.7 151.1c5.8 32.7 4.5 66.8-4.7 98.8-8.5 29.3-24.6 56.5-47.1 77.2l-59.1-45.9c19.5-13.1 33.3-34.3 37.2-57.5H186.6c.1-24.2.1-48.4.1-72.6h175z" id="Shape" fill="#4285F4"/><path d="M81.4 222.2c7.8 22.9 22.8 43.2 42.6 57.1 12.4 8.7 26.6 14.9 41.4 17.9 14.6 3 29.7 2.6 44.4.1 14.6-2.6 28.7-7.9 41-16.2l59.1 45.9c-21.3 19.7-48 33.1-76.2 39.6-31.2 7.1-64.2 7.3-95.2-1-24.6-6.5-47.7-18.2-67.6-34.1-20.9-16.6-38.3-38-50.4-62 20.3-15.7 40.6-31.5 60.9-47.3z" fill="#34A853"/></svg>
</span>
<span class="google-button__text">Sign in with Google</span>
</button>
</div>
</div>
</template>
<script>
import Firebase from './../firebase';
export default {
name: 'authentication',
created: function() {
Firebase.onAuth();
},
computed: {
user() {
return this.$store.getters.user;
},
userStatus() {
// ログインするとtrue
return this.$store.getters.isSignedIn;
}
},
methods: {
// ログイン処理
doLogin() {
Firebase.login();
},
// ログアウト処理
doLogout() {
Firebase.logout();
}
}
};
</script>
<style scoped>
.authentication {
display: inline-block;
}
.google-button {
height: 40px;
border-width: 0;
background: white;
color: #737373;
border-radius: 5px;
white-space: nowrap;
box-shadow: 1px 1px 0px 1px rgba(0, 0, 0, 0.05);
transition-property: background-color, box-shadow;
transition-duration: 150ms;
transition-timing-function: ease-in-out;
padding: 0;
}
.google-button:focus,
.google-button:hover {
box-shadow: 1px 4px 5px 1px rgba(0, 0, 0, 0.1);
}
.google-button:active {
background-color: #e5e5e5;
box-shadow: none;
transition-duration: 10ms;
}
.google-button__icon {
display: inline-block;
vertical-align: middle;
margin: 8px 0 8px 8px;
width: 18px;
height: 18px;
box-sizing: border-box;
}
.google-button__icon--plus {
width: 27px;
}
.google-button__text {
display: inline-block;
vertical-align: middle;
padding: 0 24px;
font-size: 14px;
font-weight: bold;
font-family: "Roboto", arial, sans-serif;
}
html,
body {
height: 100%;
}
body {
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
</style>


Navigation.vue

<Authentication />でボタンを配置しています。


Navigation.vue

<template>

<div id="nav">
<nav class="navbar is-transparent" v-bind:class="{'is-fixed-bottom':menuPosition}">
<div class="navbar-brand">
<router-link class="navbar-item" to="/">
<img src="https://bulma.io/images/bulma-logo.png" alt="Bulma: a modern CSS framework based on Flexbox" width="112" height="28">
</router-link>
<div class="navbar-burger burger " v-bind:class="{ 'is-active': menuActive }" v-on:click="menuToggle()" data-target="navbarExampleTransparentExample">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarExampleTransparentExample" class="navbar-menu" v-bind:class="{ 'is-active': menuActive }">
<div class="navbar-start">
<router-link class="navbar-item" to="/">Home</router-link>
<router-link class="navbar-item" to="/about">About</router-link>
</div>
<div class="navbar-end">
<Authentication />
</div>
</div>
</nav>
</div>
</template>
<script>
import Authentication from "@/components/Authentication.vue";
export default {
name: "navigation",
components: {
Authentication
},
created() {
if (window.innerWidth < 920) {
this.menuPosition = !this.menuPosition;
}
},
data() {
return {
menuActive: false,
menuPosition: false
};
},
methods: {
menuToggle() {
this.menuActive = !this.menuActive;
}
}
};
</script>
<style scoped>
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #555;
}
#nav a.router-link-exact-active {
color: #3273dc;
}
</style>


補足

bulmaのNavbar burgerメニューをvue(nuxt)に組み込む際のテンプレートの書き方


  • methodsのmenuToggle()は、レスポンシブでヘッダーが折りたたまれた時に表示されるメニューボタンのアクションです。開いたり閉じたり。

  • created()のif (window.innerWidth < 920)は、スマホで表示した時にヘッダーを画面下に固定しています。



    • data() { return {} }でこれらの状態を保存してます。