Vue vuexでfirebaseのログイン保持


## <font color="red">移行しました</font>
最新のVue3で書き直しました！[Zenn Vue3でFirebaseログイン](https://zenn.dev/kasu/articles/70a77bd2d1ee01)

<font color="red">以下は2年前の情報です。</font>
# Vueチュートリアル vuexでfirebaseのログイン保持

Vue.jsでfirebaseのログイン実装の記事はよく見かけますが、単一コンポーネント内での処理ばかりでした。
なので全体でログイン状態を共有できるようvuexを導入したいと思います。
vuexにデータを入れると、どのコンポーネントからでもアクセスできます。
Vueでプロジェクトの作成からやりたいと思っています。
[DEMOページ](https://makina.netlify.com/)
・ログイン及びログアウトボタンを表示するコンポーネント
・ユーザー情報を表示するコンポーネント
以上の2つで、vuexのデータへアクセスしています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/222473/5598db21-4fcd-f163-1a9a-e10a8951b1e4.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](https://github.com/ErgoFriend/makina)からクローンします。

```terminal
git clone https://github.com/ErgoFriend/makina.git
cd makina
npm install
vue ui
```

## プロジェクト作成
```vue ui```が使えるようになったので使っていきます。
環境構築が完了したら、コマンド:```vue ui```を実行します。
![スクリーンショット (49).png](https://qiita-image-store.s3.amazonaws.com/0/222473/5cdac1eb-7653-5af1-57b3-f43ab849f14b.png)
↑ 最初にProject Managerが表示されるのでCreateから好きな場所で下のCreateボタンを押して進めていくとプロジェクト名のフォルダが作られます。
![スクリーンショット (50).png](https://qiita-image-store.s3.amazonaws.com/0/222473/715b81e9-dd78-7e6a-3308-1e22e6b6e14b.png)
↑ 下に設定がありますが、NEXTします。
![スクリーンショット (51).png](https://qiita-image-store.s3.amazonaws.com/0/222473/a43c749e-27ce-e92c-2f7e-5fb2ee4891c6.png)
↑ ManualでNEXT
![スクリーンショット (52).png](https://qiita-image-store.s3.amazonaws.com/0/222473/020e4e7b-4091-fc80-02c8-5252dc0164dd.png)
↑ BabelとLinterをオフ、RouterとVuexの2つをオンにしてNEXT。DEMOにはPWAも入ってますが必要ないのでオフ。
![スクリーンショット (54).png](https://qiita-image-store.s3.amazonaws.com/0/222473/21507a8e-0bb7-414b-1c81-d9585e10f44a.png)
↑ このままRouterを使うとURLに#が含まれるので、Historyモードをオンにします。
Createボタンをクリック！プリセットの名前を聞かれますので```tutorial-vuex-firebase-auth```とかにしときます。
![スクリーンショット (56).png](https://qiita-image-store.s3.amazonaws.com/0/222473/524bce4d-97bd-15d8-478f-f42514319cfc.png)
↑ 右上の```Install dependency```からMain dependenciesで```@firebase/app```・```@firebase/auth```・```buefy```の3つをインストールします。
buefyはheaderとかのスタイルに使います。レスポンシブしてくれます。
![スクリーンショット (57).png](https://qiita-image-store.s3.amazonaws.com/0/222473/520ff232-92a4-a6b0-0459-4a244b231e2a.png)
↑ Run taskを押して、Open appでローカルサーバーのページを開けます。

## 作っていきます
作業するのは```tutorial-makina/src```です。3つの必要なファイルを作っていきます。
構造は以下のようになります。

```terminal: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```にログイン情報、ログイン状態の２つを保持する場所を作ります。

```javascript:/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{}```で囲っています。

```javascript
export default {
  init() {},
  login() {},
  logout() {},
  onAuth() {}
};
```

configにはfirebase consoleからプロジェクトを作成して入手してください。[Firebase を JavaScript プロジェクトに追加する]
(https://firebase.google.com/docs/web/setup)


```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.js`の`onAuthStateChanged`を呼び出してユーザー情報を渡します
- `store.commit('onUserStatusChanged', user.uid ? true : false)`で`store.js`の`onUserStatusChanged`を呼び出してログインしてるかどうかのbool値を渡します

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

##### ```init()``` : おなじみの初期化です。
- ```firebase.initializeApp(config)``` : configを読み込んで初期化します。
- ```firebase.auth().setPersistence(xxxxx)``` : 認証状態の永続性を決めます。[認証状態の永続性](https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ja)
 - ```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)``` : プロバイダを指定してポップアップウィンドウでログインさせます。
 - ```const provider = new firebase.auth.GoogleAuthProvider()``` : プロバイダにGoogleを指定します。
 - [複数の認証プロバイダ](https://firebase.google.com/docs/auth/web/account-linking?hl=ja)
- ```onAuth()```が呼ばれます : Promiseでログインが完了した後に、ユーザー情報をvuexのstoreへ保存します。
 - ```var user = firebase.auth().currentUser``` : ユーザー情報を取得できます。[Firebase でユーザーを管理する](https://firebase.google.com/docs/auth/web/manage-users)

##### ```logout()``` : ログアウトしてvuexからも削除
- ```firebase.auth().signOut()``` : ログインしてたら、これだけでログアウトできます。
 - [Google ログインと JavaScript を使用して認証する](https://firebase.google.com/docs/auth/web/google-signin?hl=ja)
- ```onAuth()```が呼ばれます : ログアウトした後、最初に入れたようにuserへ```{}```を保存します。

##### ```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

```javascript:/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が描画されます。

```vue: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
トップ画面です。

```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でアクセスできるようにしています。

```vue: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 インスタンス](https://jp.vuejs.org/v2/guide/instance.html)
- ```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```からのログイン・ログアウトを可能にしています。

```vue: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 />```でボタンを配置しています。

```vue: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)に組み込む際のテンプレートの書き方](https://qiita.com/reopa_sharkun/items/32df1feccbbf252fc276)

- methodsの```menuToggle()```は、レスポンシブでヘッダーが折りたたまれた時に表示されるメニューボタンのアクションです。開いたり閉じたり。
- created()の```if (window.innerWidth < 920)```は、スマホで表示した時にヘッダーを画面下に固定しています。
 - ```data() { return {} }```でこれらの状態を保存してます。