Firebase v9 + Vue3 + Vite + Vuetify3 beta で firebase auth のメール認証でログインしてみる

# 主旨

Linux/Windows WSL2 (Ubuntu 20.04LTS) でやります。各種ツールのバージョンは以下の通り。

```
$ npm -v
8.7.0
$ yarn -v
1.22.11
$ vue -V
@vue/cli 5.0.4
$ node -v
v16.14.0
$ firebase --version
10.7.1
```

# 準備

## vue + vuetify アプリケーションの作成

`vue-app` の部分は、自分の作りたいプロジェクトの名前に置き換えてください。

```
$ vue create vue-app
Vue CLI v5.0.4
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 3] babel, eslint)
  Default ([Vue 2] babel, eslint)
  Manually select features
...
$ cd vue-app
$ vue add vuetify
? Choose a preset:
  Configure (advanced)
  Default (recommended)
❯ Vite Preview (Vuetify 3 + Vite)
  Prototype (rapid development)
  Vuetify 3 Preview (Vuetify 3)
...
$ vue add router
 WARN  There are uncommitted changes in the current repository, it's recommended to commit or stash them first.
? Still proceed? Yes
...
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
...

```

vscode の拡張機能は volar を使うことが推奨されているようです。

冒頭の `vue create vue-app` は下記のようにしてもいけます。

```
$ npm init vite@latest vue-app
```

typescript にしたい場合は、下記の方法でプロジェクトを作成しないと、うまくいかないようです（`vue create` ではダメ）。

```
$ npm init vite@latest vue-app --template vue-ts
```

Typescript を使った場合で、`vue add router` したときに Error: Cannot find module '@vue/cli-service/generator/template/src/App.vue' ... というエラーが出た場合は、先に下記コマンドを実行します。 

```
$ npm install --save-dev @vue/cli-service
```

## 起動テスト

```
$ yarn dev
```

これで http://localhost:3000 にアクセスすると下記の画面が出るはず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/5127c656-b7cb-3042-599b-5c1fda234527.png)

もし下記のようなエラーが出たら、jsconfig.json の `target` を `es6` にする。

```
    vite.config.js:5:0:
      5 │ const path = require('path')
        ╵ ~~~~~

  The target environment was set to "es5" here:

    jsconfig.json:3:14:
      3 │     "target": "es5",
        ╵               ~~~~~
```

このエラーがでたら、 

```jsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    ...
}
```

こうします。(vuetify3.0.0 beta 3 からは、このエラーは出なくなった気がします）

## router/indes.js で process is not defined のエラーが出る

`history: createWebHistory(process.env.BASE_URL),` を `history: createWebHistory(import.meta.env.BASE_URL),` に書きかえます。

```router/index.js
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})
```

下記参照のこと。

- [Vueの環境変数が設定できず悩んだ件vite使用](https://qiita.com/takezone/items/c2a964b1eb2795cf5641)

これで動けば、vue まわりの準備は完了です。


## firebase 関連の初期化

firebase コンソールでひとつプロジェクトを作って、apiKey を取得しておきます。具体的には下記を参照してください。

- [Firebase Authentication + FirebaseUI + Vuetify でメール認証でログインする](https://qiita.com/mml/items/ffdf7c228fb8d0868525)

firebase init では必要なサービスをカーソルキーとスペースキーで選びます。最低限 hosting は必要です。下記では Hosting (github) 以外すべてを選んでいます。

```
$ firebase init
...
? Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confir
m your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◉ Realtime Database: Configure a security rules file for Realtime Database and (optionally) provision default instance
 ◉ Firestore: Configure security rules and indexes files for Firestore
 ◉ Functions: Configure a Cloud Functions directory and its files
❯◉ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
 ◯ Hosting: Set up GitHub Action deploys
 ◉ Storage: Configure a security rules file for Cloud Storage
 ◉ Emulators: Set up local emulators for Firebase products
(Move up and down to reveal more choices)

? 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

=== Database Setup
? What file should be used for Realtime Database Security Rules? database.rules.json

=== Storage Setup
? What file should be used for Storage Rules? storage.rules

=== Firestore Setup
? What file should be used for Firestore Rules? firestore.rules
? What file should be used for Firestore indexes? firestore.indexes.json

=== Functions Setup
? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
✔  Wrote functions/package.json
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes

=== Hosting Setup
? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? No
? Set up automatic builds and deploys with GitHub? No
✔  Wrote dist/404.html
✔  Wrote dist/index.html
...
```

プロジェクトは、firebase のコンソール側で作成したものを選びます。Hostig の public directory は `dist` にします。

```
=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. (
Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◉ Authentication Emulator
 ◉ Functions Emulator
 ◉ Firestore Emulator
 ◉ Database Emulator
 ◉ Hosting Emulator
 ◉ Pub/Sub Emulator
❯◉ Storage Emulator
? Which port do you want to use for the auth emulator? 9099
? Which port do you want to use for the functions emulator? 5001
? Which port do you want to use for the firestore emulator? 8080
? Which port do you want to use for the database emulator? 9000
? Which port do you want to use for the hosting emulator? 5000
? Which port do you want to use for the pubsub emulator? 8085
? Which port do you want to use for the storage emulator? 9199
? Would you like to enable the Emulator UI? Yes
? Which port do you want to use for the Emulator UI (leave empty to use any available port)?
? Would you like to download the emulators now? No
```

Emulator は全部チェックしておけばOKです。storage のエミュレータには問題があるようなので、以下のサンプルでは使いませんが、とりあえず有効にだけしておきます。

エミュレータのポート番号はすべてデフォルトで問題ありません。

## 動作テスト

```
$ yarn build
$ firebase emulators:start

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Database       │ 127.0.0.1:9000 │ http://127.0.0.1:4000/database  │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Pub/Sub        │ 127.0.0.1:8085 │ n/a                             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Storage        │ 127.0.0.1:9199 │ http://127.0.0.1:4000/storage   │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500

```

上のような感じになったら、正常に起動しています。http://localhost:5000/ にブラウザでアクセスすると、`yarn dev` のときと同じ画面が表示されるはずです。

emulator の起動の途中でエラーが出る場合は、

- firebase init での初期化が足りていない
- functions 以下のフォルダで `npm install` の操作がされていない
- functions の言語を Typescript にした時に `npm run build` がされていない
- 他の firebase emulator が動作している（生き残りプロセスがある）

などの理由が考えられます。最後のケースでは `ps -aef | grep firebase` などとして emulator のプロセスを探して kill します。

## firebase の初期化コードを追加する

下記の内容で `src/firebase.js` というファイルを作ります。

```firebase.js
import { initializeApp } from 'firebase/app';
const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
};
const firebase = initializeApp(firebaseConfig);
export const firebaseApp = () => { return firebase };

export default firebase;
```

`firebaseConfig` の部分は、firebase のコンソールで取得した apiKey を設定します。

emulator を使用する場合は、さらに下記のようにします (auth, firestore, functions, storage を使用する場合）。

```firebase.js
import { initializeApp } from 'firebase/app';
import { getFunctions, connectFunctionsEmulator } from "firebase/functions";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";

import { getStorage, connectStorageEmulator } from "firebase/storage";
import { getAuth, connectAuthEmulator } from "firebase/auth";

const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
};
const firebase = initializeApp(firebaseConfig);
const isEmulating = window.location.hostname === "localhost";
if (isEmulating) {

    const auth = getAuth();
    connectAuthEmulator(auth, "http://localhost:9099");

    const storage = getStorage();
    connectStorageEmulator(storage, "localhost", 9199);

    const db = getFirestore(firebase);
    connectFirestoreEmulator(db, 'localhost', 8080); 

    const functions = getFunctions(firebase);
    connectFunctionsEmulator(functions, "localhost", 5001);
}

export const firebaseApp = () => { return firebase };

export default firebase;
```

## firebase.js を .gitignore に追加しておく（オプション）

girhub などに公開状態で置くときは、.gitignore に firebase.js を追加して、firebase.js が公開されないようにしておきます。

```.gitignore
.DS_Store
node_modules
/dist

firebase.js

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
```

# firebase の動作テスト

## firebase 関連のインストール

最初に以下のコマンドを実行します。

```
$ yarn add firebase
```

これは忘れやすい。

## firebase のコンソールでメール認証を on にする

下記を参照してください。

- [Firebase Authentication + FirebaseUI + Vuetify でメール認証でログインする](https://qiita.com/mml/items/ffdf7c228fb8d0868525)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/23bb3a73-6dfd-507d-b539-486a0ebc8f6a.png)

## App.vue などの編集

src/App.vue は下記の内容だけにします。

```App.vue
<template>
  <v-app>
    <router-view/>
  </v-app>
</template>
<script>
import { firebaseApp } from './firebase';
</script>
```

`src/views/HomeView.vue` を以下のように編集します。

```HomeView.vue
<template>
    <v-container>
      <div v-if="currentUser == null">
        <v-card width="400px" class="mx-auto mt-5">
          <v-card-actions>
            <v-col>
                <v-text-field 
                    v-model="emailText"
                    label="E-MAIL"
                >
                </v-text-field>
                <v-text-field 
                    v-model="passwordText"
                    label="PASSWORD"
                    type="password"
                >
                </v-text-field>
              <v-btn
                @click="signin"
                color="primary"
              >
                E-Mail SIgn In
              </v-btn>
              <v-btn
                color="primary"
                @click="createAccount"
              >
                Sign Up
              </v-btn>
            </v-col>
          </v-card-actions>
        </v-card>
      </div>
      <div v-else>
        <h2>
          Success to sign in with firebase auth!
        </h2>
        <v-btn @click="signout">
          Sign Out
        </v-btn>
      </div>
  </v-container>    
</template>
<script>
import { getAuth, signOut, onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword } from "firebase/auth";

export default {
    name: 'loginpage',
    data: () => ({
        emailText: "",
        passwordText: "",
        currentUser: null,
    }),
    mounted()
    {
      const auth = getAuth();
      onAuthStateChanged(auth, (user) => {
        if ( user != null ){
          this.currentUser = user;
        }else{
          this.currentUser = null;
        }
      });
    },
    methods: {
        signin()
        {
            if ( this.emailText == "" || this.passwordText == "" ) return;
            const auth = getAuth();
            signInWithEmailAndPassword(auth, this.emailText, this.passwordText)
            .then((userCredential) => {
                // Sign In
                const user = userCredential.user;
                console.log( user );
            })
            .catch((error) => {
                const errorCode = error.code;
                const errorMessage = error.message;
                console.log( errorCode, errorMessage );
            });
        },
        createAccount()
        {
            const auth = getAuth();
            createUserWithEmailAndPassword(auth, this.emailText, this.passwordText)
            .then((userCredential) => {
                // Create Account
                const user = userCredential.user;
                console.log( user );
            })
            .catch((error) => {
                const errorCode = error.code;
                const errorMessage = error.message;
                console.log( errorCode, errorMessage );
            });
        },
        signout()
        {
          const auth = getAuth();
          signOut(auth).then(() => {
            this.currentUser = null;
          }).catch((error) => {
            console.log( error );
          });
        }
    }
}
</script>
```

## 動作テスト

```
$ firebase emulators:start
$ yarn dev
```

上のコマンドは、別々のターミナルで実行する必要があります。これで http://localhost:3000 をブラウザで開くと、下記のような画面が出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/f3cbfa5f-e9f0-b311-c379-7c6c8112e413.png)

email と password を入力して SIGN UP を押すと、アカウントが作成されてログインした状態になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c8b48fe0-66c2-6f97-4b5d-7894e3043f09.png)

たとえばこんな感じです。

ログインした状態になると、下記の画面になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/fbecf134-4702-d554-eef9-09a8e9ef2e0b.png)

こ画面で、SIGN OUT を押すと、ログアウトできます。

次にログインするときは、メールアドレスとパスワードを入れて E-MAIL SIGN IN を押します。

この状態で http://localhost:4000/auth をブラウザで開くと、エミュレータ上にアカウントが作成されているのが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/0471f8aa-8239-50c5-c229-b1b65e8d60eb.png)

## Deploy

```
$ yarn build
$ firebase deploy
```

# いろいろ

全部のページで firebase を使うなら、src/firebase.js を書いた上で App.vue で下記のように書いておくと、個別のページで import しなくてよくなります。

```App.vue
<script>
import { firebaseApp } from './firebase';
</script>
```

一部のページだけで使うなら、都度 import したほうがよい、のかなあ？

ログインした後、別のページに飛ばしたければ、

```.js
this.$router.push('/home');
```

などとして飛ばします。

storage のエミュレータで、サイズが大きなファイルを扱おうとすると、エミュレータが落ちることがあるようです。storage はオンライン側を使ったほうがいいかもしれません。その場合は、firebase.js のエミュレータ関連の部分は下記のようにコメントアウトします。

```firebase.js
const firebase = initializeApp(firebaseConfig);
const isEmulating = window.location.hostname === "localhost";
if (isEmulating) {

    // const auth = getAuth();
    // connectAuthEmulator(auth, "http://localhost:9099");

    //const storage = getStorage();
    //connectStorageEmulator(storage, "localhost", 9199);

    const db = getFirestore(firebase);
    connectFirestoreEmulator(db, 'localhost', 8080); 

    const functions = getFunctions(firebase);
    connectFunctionsEmulator(functions, "localhost", 5001);
}
```

storage をオンライン側で使う場合、auth もオンライン側を使用しないと、認証時に失敗します。

# おまけ

Windows で WSL を使っている場合で、レポジトリのファイルが Windows File System ↑にあると、HMR （オートリロード）が実行されません。

vite.config.js に下記を追加すると、機能が有効になります。

```vite.config.js
  server: {
    watch: {
      usePolling: true
    }
  },
```