Help us understand the problem. What is going on with this article?

Vue vuexでfirebaseのログイン保持

More than 1 year has passed since last update.

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 {} }でこれらの状態を保存してます。
ErgoFriend
美好くん
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした