移行しました
最新のVue3で書き直しました!Zenn Vue3でFirebaseログイン
以下は2年前の情報です。
Vueチュートリアル vuexでfirebaseのログイン保持
Vue.jsでfirebaseのログイン実装の記事はよく見かけますが、単一コンポーネント内での処理ばかりでした。
なので全体でログイン状態を共有できるようvuexを導入したいと思います。
vuexにデータを入れると、どのコンポーネントからでもアクセスできます。
Vueでプロジェクトの作成からやりたいと思っています。
DEMOページ
・ログイン及びログアウトボタンを表示するコンポーネント
・ユーザー情報を表示するコンポーネント
以上の2つで、vuexのデータへアクセスしています。
環境
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
を実行します。
↑ 最初にProject Managerが表示されるのでCreateから好きな場所で下のCreateボタンを押して進めていくとプロジェクト名のフォルダが作られます。
↑ 下に設定がありますが、NEXTします。
↑ ManualでNEXT
↑ BabelとLinterをオフ、RouterとVuexの2つをオンにしてNEXT。DEMOにはPWAも入ってますが必要ないのでオフ。
↑ このままRouterを使うとURLに#が含まれるので、Historyモードをオンにします。
Createボタンをクリック!プリセットの名前を聞かれますのでtutorial-vuex-firebase-auth
とかにしときます。
↑ 右上のInstall dependency
からMain dependenciesで@firebase/app
・@firebase/auth
・buefy
の3つをインストールします。
buefyはheaderとかのスタイルに使います。レスポンシブしてくれます。
↑ Run taskを押して、Open appでローカルサーバーのページを開けます。
作っていきます
作業するのはtutorial-makina/src
です。3つの必要なファイルを作っていきます。
構造は以下のようになります。
│ 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つを保持する場所を作ります。
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 プロジェクトに追加する]
(https://firebase.google.com/docs/web/setup)
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)
: 認証状態の永続性を決めます。認証状態の永続性 -
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を指定します。 - 複数の認証プロバイダ
-
onAuth()
が呼ばれます : Promiseでログインが完了した後に、ユーザー情報をvuexのstoreへ保存します。 -
var user = firebase.auth().currentUser
: ユーザー情報を取得できます。Firebase でユーザーを管理する
logout()
: ログアウトしてvuexからも削除
-
firebase.auth().signOut()
: ログインしてたら、これだけでログアウトできます。 - Google ログインと JavaScript を使用して認証する
-
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
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が描画されます。
<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
トップ画面です。
<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でアクセスできるようにしています。
<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
に登録した関数を実行します。
-
import Firebase from "./../firebase"
: 最初に作ったfirebase.js
をFirebaseで読み込みます。 -
doLogin() { Firebase.login() }
:firebase.js
の関数を再度名前を付けます。 -
methods
で関数を作ることで@click
からのログイン・ログアウトを可能にしています。
<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 />
でボタンを配置しています。
<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 {} }
でこれらの状態を保存してます。