Nuxt.js＋Firebaseで２回目の新しいプロジェクトを作る

Nuxt.js+Firebaseを勉強しています。

プロジェクトを作成し、Webサービスをデプロイすることができました。本記事は、もう一つプロジェクトを作成してSNS認証を実装するところまでのメモです。

# 環境

- Firebase 8.0.1
- Vue 2.6.11
- Nuxt 2.10.2
- 開発環境 WindowsPC

# 実施手順

## [Firebaseコンソール](https://console.firebase.google.com/) で新規プロジェクトを作成

- 「このプロジェクトで Google アナリティクスを有効にする」を有効
	- 「Google アナリティクスの構成」はデフォルトを選択

作成が完了し、プロジェクトのコンソールを見れるようになった。

## Firebaseコンソールでウェブアプリを作成

プロジェクトのコンソールで開始するアプリ（ウェブ）を追加する。

- 「アプリのニックネーム」は自由に決める。
- Firebase Hostingの設定は後からでもできるとのことなので、このタイミングではしない。

「アプリを登録」ボタンを押下する。

## Nuxtでプロジェクトを作成する

Nuxtでプロジェクトを作成するコマンド
```npx create-nuxt-app （プロジェクト名）```

コマンドを実行したディレクトリに指定したプロジェクト名のディレクトリが作られる。

Vuetifyで簡単に作りたかったので、設定はこんな感じ。

```
- ? Project name: (プロジェクト名)
	- デフォルトでいいので未入力で Enter
- ? Programming language: (Use arrow keys)
	- > JavaScript
	-   TypeScript
- ? Package manager: (Use arrow keys)
	-   Yarn
	- > Npm
- ? UI framework: (Use arrow keys)
	-   None
	-   Ant Design Vue
	-   Bootstrap Vue
	-   Buefy
	-   Bulma
	-   Chakra UI
	-   Element
	-   Framevuerk
	-   iView
	-   Tachyons
	-   Tailwind CSS
	-   Vuesax
	- > Vuetify.js
- ? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
	- >( ) Axios
	-  ( ) Progressive Web App (PWA)
	-  ( ) Content
	- ※ 選択無しでEnter
- ? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
	- >( ) ESLint
	-  ( ) Prettier
	-  ( ) Lint staged files
	-  ( ) StyleLint
	-  ( ) Commitlint
	- ※ 選択無しでEnter
- ? Testing framework: (Use arrow keys)
	- > None
	-   Jest
	-   AVA
	-   WebdriverIO
- ? Rendering mode: (Use arrow keys)
	-   Universal (SSR / SSG)
	- > Single Page App
- ? Deployment target: (Use arrow keys)
	- > Server (Node.js hosting)
	-   Static (Static/JAMStack hosting)
- ? Development tools: (Press <space> to select, <a> to toggle all, <i> to invertselection)
	- >( ) jsconfig.json (Recommended for VS Code if you're not using typescript)
	-  ( ) Semantic Pull Requests
	-  ( ) Dependabot (For auto-updating dependencies, GitHub only)
	- ※ 選択無しでEnter
- ? What is your GitHub username? 
	- GitHubのユーザー名を入力する。
- ? Version control system: (Use arrow keys)
	-   Git
	- > None
```

## ビルド実行

```
>npm run build

（中略）

? Are you interested in participation? (Y/n)
```

`n` にした。

## Firebase SDKのインストールとプロジェクトへの紐付け

作成したプロジェクトにFirebase SDKをインストールする。
今回はnpmで--saveでインストールする。（-gではなく）

```
cd （プロジェクト名のフォルダ）
npm install firebase --save
```

インストールしたfirebaseのバージョンを確認する。

```
>npm list firebase
`-- firebase@8.0.1
```

## テスト環境で動かしてみよう

実行コマンドは次の通り。

```
npm run dev
```

`http://localhost:3000/` にアクセスすると、Vuetifyの画面が出る。


## プロジェクトをFirebase初期設定する

### １．Firebaseコンソールでリージョンを設定

[Firebaseコンソール](https://console.firebase.google.com/) にログインし、当該プロジェクトのSettingsを開く。
[全般] 中程の リソース ロケーション を任意のロケーションに指定する。
今回は東京リージョン **asia-northeast1** を選択する。

### ２．Cloud Firestore の作成

Cloud Firestoreのページで、「データベースの作成」ボタンを押下する。


### ３． `firebase init` を実行

NuxtでFirebaseを使いたいので、create-nuxt-appで作成したNuxtプロジェクトディレクトリのトップでコマンド `firebase init` を実行する。

```
 to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)
>( ) Database: Deploy Firebase Realtime Database Rules
 (*) Firestore: Deploy rules and create indexes for Firestore
 ( ) Functions: Configure and deploy Cloud Functions
 (*) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features
```

```
? Please select an option: (Use arrow keys)
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project
```

Firestore Rulesについてはデフォルトのまま Enter

```
? What file should be used for Firestore Rules? (firestore.rules)
```

Firestore indexesについてもデフォルトのまま Enter

```
? What file should be used for Firestore indexes? (firestore.indexes.json)
```

```
? What do you want to use as your public directory? (public)
```

SPAについての設定も y で。

```
? Configure as a single-page app (rewrite all urls to /index.html)? (y/N)
```

## firebase-tools のアップデート

```
   ╭───────────────────────────────────────────╮
   │                                           │
   │     Update available 8.4.3 → 8.15.1       │
   │   Run npm i -g firebase-tools to update   │
   │                                           │
   ╰───────────────────────────────────────────╯
```

```
>npm i -g firebase-tools
```

## firebase.config の作成

firebase.configファイルをプロジェクトのディレクトリに作成して、 `Firebase SDK snippet` を書き込む。
`Firebase SDK snippet` は、Firebaseコンソールの「設定」-「全般」を参照する。

```
export default {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
}
```

firebase.configファイルは、gitignoreに設定する。

```
# others
firebase.config
```

## vuex（ビューエックス）をインストール

vuexを利用するので、インストールする。

```
npm install vuex --save
```

## firebaseuiをインストール

```
npm install firebaseui --save
```

## FirebaseUIのcssを読み込ませる

FirebaseUIのcssは `nuxt.config.js` で読み込んでおく。

```:~/nuxt.config.js
  css: [
    "firebaseui/dist/firebaseui.css",
  ],
```

## Firebaseの初期化処理のコーディング

Firebaseの初期化処理をpluginsに書く。firebase.configの読み込み、SNS認証の初期設定、フィルターの読み込み。
pluginsフォルダで実装したjsは、「root Vue.jsアプリケーションがマウント」される前に実行される。

フィルターは、金額をカンマ区切りで表示したい場合等に利用する。
`~/filter/filters.js` にファイルを作成し、pluginsで読み込ませることで、どのページでも利用できるようになる。

```:~/plugins/firebase.js
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import config from '~/firebase.config';
import Vue from 'vue';
import * as filters from '~/filter/filters';

if (!firebase.apps.length) {
  firebase.initializeApp(config);
  firebase.analytics();
}

// initialize authorization
export const authProviders = {
  // 使うものだけ定義しておきましょう
  Email: firebase.auth.EmailAuthProvider.PROVIDER_ID,
  Google: firebase.auth.GoogleAuthProvider.PROVIDER_ID,
  Facebook: firebase.auth.FacebookAuthProvider.PROVIDER_ID,
  Twitter: firebase.auth.TwitterAuthProvider.PROVIDER_ID,
  Github: firebase.auth.GithubAuthProvider.PROVIDER_ID
};

export const auth = firebase.auth();

// initialize Firestore
const db = firebase.firestore();
export { db };

// import filters
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key]);
});
```

```:~/filter/filters.js
// 数字の3桁ごとにカンマ区切りをつけるフィルタ
export function number_format (value) {
  if (value === 0) { return 0; }
  if (value === "0") { return 0; }
  if (! value) { return 0; }
  return value.toString().replace( /([0-9]+?)(?=(?:[0-9]{3})+$)/g , '$1,' );
}

// yyyymm文字列を"yyyy年mm月"文字列にするフィルタ
export function yyyymm_format (value) {
  if (! value) { return false; }
  return value.substr( 0, 4 ) + "年" + value.substr(4) + "月";
}
```

プラグインをNuxt.jsで使えるように設定ファイル `nuxt.config.js` を編集する。

```:~/nuxt.config.js
  plugins: [
    '~/plugins/firebase',
  ],
```

## SNS認証のログイン/ログアウトのステータスを保存する

```:~/store/auth.js
import Vue from 'vue';
import { auth } from '~/plugins/firebase';

export const state = () => ({
        user: {},
        status: ""
    });

export const mutations = {
        setUser(state, user) {
            state.status = "loggedIn";
            state.user = user;
        },
        logout(state) {
            state.status = "loggedOut";
            state.user = {};
        }
    };

export const getters = {
        isLoggedIn: (state) => {
          return state.status === "loggedIn";
        },
        getUsername: (state) => state.user.displayName,
        getUid: (state) => state.user.uid
    };

export const actions = {
        gotUser({ commit }, user) {
            commit("setUser", user);
        },
        logout({ commit }) {
            auth.signOut().then(() => {
                commit("logout");
            })
        },
    };
```

## ログイン/ログアウトの認証情報による処理の追加

middlewareはページがレンダリングされる前に実行される。
（すべての画面遷移の前に実行される）

middlewareに、ログイン/ログアウトなどの認証情報の変化で発火するonAuthStateChangedを設定しておく。

さらに、 `firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);` を追加する。
これによって、認証情報をローカルストレージに保存することができるようになる。

```:~/middleware/authenticated.js
import firebase from 'firebase';
import { auth } from '~/plugins/firebase';

export default function ({ route, store, redirect }) {
  auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);
  auth.onAuthStateChanged((user) => {
    if (user) {
      store.dispatch("auth/gotUser", user);
    } else {
      // ログアウトせずにタブを消して、ブラウザを閉じずにまたURLから画面を開いたとき、
      // firebase.authのuserがnullなのに、ローカルストレージの情報が残っていてstoreのauthはログイン状態になっている。
      // そこで、ここで一度ログアウトを実行しておく。
      store.dispatch("auth/logout");
      // もしログインしていなかったらログインページにリダイレクト
      if(route.name !== "login") redirect("/login");
    }
  });
}
```

```:~/middleware/notAuthenticated.js
export default function ({ store, redirect }) {
  if (store.getters["auth/isLoggedIn"]) {
    return redirect('/');
  }
}
```

## ログインボタンを表示するUI部分を作成

下記はPUGを利用したHTML記述方法になっている。
firebaseuiのimportはエラーが出たため、requireを利用することにした。

```:~/components/FirebaseAuth.vue
<template>
  <div id="firebaseui-auth-container"></div>
</template>

<script>
import { auth, authProviders } from '~/plugins/firebase';

export default {
  name: 'FirebaseAuth',
  mounted() {
    auth.onAuthStateChanged(user => {
      if (!user) {
        const firebaseui = require('firebaseui');

        const ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(auth);

        const config = {
          signInOptions: [
            // authProviders.Email,
            authProviders.Google,
            // authProviders.Facebook,
          ],
          callbacks: {
          },
          signInSuccessUrl: '/',
          signInFlow: 'popup', // ログインフロー設定。Nuxtのローカルサーバーで起こるCORSエラーがあるのでpopupがオススメです。
        };

        ui.start('#firebaseui-auth-container', config);
      }
    });
  }
}
</script>
```

signInFlowで設定しているログインフロー設定とは、サインアップフローをポップアップにするか、リダイレクトにするかなどを設定するためのものです。

## ログインページの作成

pagesディレクトリに、login.vueを作る。とりあえず中身はない。

```
~/pages/login.vue

<template>
  <div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";

export default {
  middleware: 'notAuthenticated',
  name: 'Login',
}
</script>
```
## ヘッダーの作成

ログイン/ログアウトをヘッダーに実装する。

```
~/components/Header.vue

<template>
  <v-app-bar
    :clipped-left="clipped"
    fixed
    app
  >
    <v-app-bar-nav-icon v-if="isLoggedIn" @click.stop="drawer = !drawer" />
    <v-btn
      icon
      v-if="isLoggedIn"
      @click.stop="miniVariant = !miniVariant"
    >
      <v-icon>mdi-{{ `chevron-${miniVariant ? 'right' : 'left'}` }}</v-icon>
    </v-btn>
    <v-toolbar-title></v-toolbar-title>
    <v-spacer />
    <v-btn text='' v-if="isLoggedIn" @click="logout"> Logout </v-btn>
    <FirebaseAuth/>
  </v-app-bar>
</template>

<script>
import FirebaseAuth from '@/components/FirebaseAuth.vue';
import { mapGetters, mapActions } from "vuex";

export default {
  components: {
    FirebaseAuth
  },
  computed: {
    ...mapGetters("auth", [
      "isLoggedIn",
      "getUsername",
      "getUid"
    ]),
  },
  methods: {
    ...mapActions("auth", ["logout"])
  }
}
</script>
```


## フッターの作成

```
~/components/Footer.vue

<template>
  <v-footer>
    <div class="flex-grow-1"></div>
    <div v-if="getUid"><p class="grey--text" style="font-size: small;">
      UID: {{getUid}}
    </p></div>
  </v-footer>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters("auth", [
      "getUid"
    ]),
  },
}
</script>
```

## FirebaseコンソールでSNS認証の利用を開始

- [Firebaseコンソール](https://console.firebase.google.com/) の「Authentication」で「始める」ボタンを押下する。
- 「Sign-in method」で `Google` を有効にする。

## Firebase Hostingの初期設定

以下のコマンドを実行する。

```
firebase init hosting
```

publicディレクトリは `dist` を指定する。

```
? What do you want to use as your public directory? dist
```

```
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
```

```
? Set up automatic builds and deploys with GitHub? No
```

```
? File dist/index.html already exists. Overwrite? No
```


## ローカル環境でテスト

- `npm run build`
- `firebase serve --only hosting`
- http://localhost:5000/ ※ローカルテスト環境

これでSNS認証によるログインができるようになった。。はず