Vue2 と Firebase v8 を CDN 版でやる (vue-router と Vuetify2 と Firebase Emulator もやる）

# 主旨

表題の通りです。

firebase は v8 です。vue.js は 2 です。

# 参考

- [npm不要どころかHTML1つだけでVueとVuetifyが試せるテンプレート](https://qiita.com/10size/items/21fc5b0c3af22fbeb7c5)
- [VueのCDN版を使おう](https://scrapbox.io/vue-yawaraka/Vue%E3%81%AECDN%E7%89%88%E3%82%92%E4%BD%BF%E3%81%8A%E3%81%86)
- [CDNを使ってVue.jsをする方法](https://qiita.com/hi-scorp-d/items/43c6b702f72da90c2d81)


# 準備

## npm と node のインストール

ググってインストールします。すでにインストール済みなら何もしなくてもOKです。
なお、この記事で使ってるバージョンは、以下です。

```
$ npm -v
8.3.1
$ node -v
v16.14.0
```

エミュレータの起動には java の version 1.8 (openjdk-8-jre/java-1.8.0-openjdk) 以上のインストールが必要です。下記から JDK の最新版をインストールすれば、多分いけます。

- [How to download and install prebuilt OpenJDK packages](https://openjdk.java.net/install/)

参考: [Local Emulator Suite のインストール、構成、統合](https://firebase.google.com/docs/emulator-suite/install_and_configure?hl=ja)


## firebase-tools

```
% npm i -g firebase-tools
$ firebase --version
10.2.2
```

firebase-tools は頻繁にバージョンアップするので、多少バージョンが違っても多分問題ないです。

## firebase のプロジェクト作成

下記ページなどを参考に、プロジェクトをひとつ作成します。

- [Firebaseプロジェクトの作成手順](https://jeannie-blog.online/create-firebase-project/)


# firebase の初期化

以下は Firestore, functions, Hosting, Storage, Emulators を使う例です。

```
$ firebase init
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/a86a9574-6c3b-f3ba-b89e-7b0cf491ae95.png)

- functions を使う場合は、blaze プラン（有料プラン）にする必要があります。blaze プランは従量制ですが、無料枠があります。emulator だけを使う限りは（何も deploy しなければ）、課金されることはないはずです。

- authentication については、この記事の下のほうにあるコードを動かすためには、firebase コンソールでメール認証を ON にしておく必要があります。emulator で動かしている限りは、ON にしなくてもいいはずなんですが、何かの拍子にダメといわれることがあるので、一応 ON にしときます。

- storage, realtime databse. firestore を使うためには、前述のように firebase コンソール側で、storage や realtime databse, firestore をそれぞれ初期化しておく必要があります。具体的には、コンソールにいって プロジェクトの storage の項目の中で「使用を開始」（realtime databse なら「データベースを作成」）を押しておきます。

- storage や firebase, real time databse の初期化時に「本番環境」と「テスト環境」を選択しますが、特に問題がなければ「本番環境」にしておきます。必要があれば、あとで変更できます。emulator の動作には、ここでの選択は影響しません。

- ロケーションの選択についても、特に問題がなければデフォルトにしておきます。この選択は後で変更できませんが、ローカルでの開発テストをするだけであれば、ここでの選択は影響しません。（プロジェクト内部で他のGCPのサービスを使用する場合は、ロケーションを揃えたほうが良いです）
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/83f06a5c-776c-564c-6402-fba0dd593ca9.png)

作成済みのプロジェクトを使うので、ここでは use an existing project を選びます。すると、自分の作成済みプロジェクト一覧が表示されるので、使用するプロジェクトを選びます。

```
=== Project Setup
? What file should be used for Firestore Rules? firestore.rules
=== Firestore Setup
? 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? public
? Configure as a single-page app (rewrite all urls to /index.html)? No
? Set up automatic builds and deploys with GitHub? No
✔  Wrote public/404.html
✔  Wrote public/index.html
=== Storage Setup
? What file should be used for Storage Rules? storage.rules
✔  Wrote storage.rules
```

このあたりは全てデフォルトで問題ないです。コンソールで初期化がすんでいないサービスを選んでいると、コンソール側で初期化する必要があるというエラーが出て、初期化が中断されます（多分）。

Spark プラン（無料プラン）で functions を選ぶと、プロジェクトのプランを変更する必要があるというエラーが出ます（多分）。

```
=== 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 Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. A
uthentication Emulator, Functions Emulator, Firestore Emulator, Hosting 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 hosting emulator? 5000
? 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

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!
```

emulators は、databse と pub/sub 以外を使います。あとから `firebase init emulators` として、使用するエミュレータを変更することもできます。ポートの設定はすべてデフォルトで問題ないです。

ちなみに、emulator をつかってローカルでお試し開発をするだけなら（deployしないなら）`firebase init emulators` だけでもいけるぽいです。


# Vue.js + CDN + Firebase のコードを書く

プロジェクトの public/ 以下に index.html を置きます。（CDN の参照先が微妙にバラバラで気持ち悪いですが・・・いつか直します）

inde.xhtml 中の `const firebaseConfig` の部分は、firebase のプロジェクトの apiKey を設定します。apiKey の取得方法については、下記記事の「apiKey 等の情報を取得する」の項目を参照のこと。

- [Firebase の web アプリにメールアドレスでユーザ登録をさせて確認メールを送る](https://qiita.com/mml/items/5e325bb19ba532ca56b7)

```public/index.html
<!DOCTYPE html>
  <head>
    <meta charset="UTF-8" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@500&display=swap" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">

    <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-storage.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-firestore.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-functions.js"></script>
    
    <script src="https://www.gstatic.com/firebasejs/ui/4.8.1/firebase-ui-auth.js"></script>
    <link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/4.8.1/firebase-ui-auth.css" />
  
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
    <script src="https://unpkg.com/vue-router@3.5.2/dist/vue-router.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
    
    <script src="https://unpkg.com/http-vue-loader"></script>

    <title>Firebase Auth/Storage/Functions on Vue/Vuetify/vue-router</title>
  </head>
  <body>
    <div id="app">
      <v-app>
      <v-app-bar
        app
        color="primary"
        dark
      >
      <v-toolbar-title class="text-h6">
          <span>Firebase Storage Test</span>
      </v-toolbar-title>
      </v-app-bar>
    
        <v-main fluid>
          <router-view></router-view>
        </v-main>
      </v-app>
    </div>
  </body>
  <script>
    const firebaseConfig = {
      apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      authDomain: "xxxxxxxxxxxxxx.firebaseapp.com",
      projectId: "xxxxxxxxxxxxxx",
      storageBucket: "xxxxxxxxxxxxxx.appspot.com",
      messagingSenderId: "11111111111111",
      appId: "1:0000000000000000:web:xxxxxxxxxxxxxxxxxxxxxxxx"
    };

    firebase.initializeApp(firebaseConfig);
    const isEmulating = window.location.hostname === "localhost";
    if (isEmulating) {
        firebase.auth().useEmulator("http://localhost:9099");
        firebase.storage().useEmulator('localhost', 9199);
        firebase.functions().useEmulator('localhost', 5001);
    }

    const routes = [
      { path: '/', component: httpVueLoader('./components/HomeView.vue') },
      { path: '/login', component: httpVueLoader('./components/LoginPage.vue') },
    ];

    new Vue({
      el: '#app',
      router: new VueRouter({routes}),
      vuetify: new Vuetify(),
    });
  </script>
</html>
```

各 vue ファイルは public/components/ 以下に置きます。（index.html 内のパスを書き換えれば、どこに置いても良いです）

```public/components/HomeView.vue
<template>
  <v-container>

    <div id="firebaseui-auth-container"></div>
    <router-link v-if="login" to="/login">To Test page</router-link>
  </v-container>
</template>
<script>
module.exports = {
  data: function() {
    return {
      login: false,
      text: 'Home Component'
    }
  },
  mounted(){
      const uiConfig = {
          signInSuccessUrl: '/#/login', // ログインに成功したときに飛ぶページ
          signInOptions: [
            {
              provider: firebase.auth.EmailAuthProvider.PROVIDER_ID 
              //メール認証のみ使用する。google 認証などを使う場合はさらに羅列する。
            }
          ],

          tosUrl: '/kiyaku.html',           // 規約とかを描いたページ
          privacyPolicyUrl: '/policy.html'  // プライバシーポリシーを描いたページ
      };
  
      // firebase auth ui を使用、ログインしているときは表示しない。
      firebase.auth().onAuthStateChanged( user => {
          if ( user != null ){
            this.login = true;
          }else{
            this.login = false;
            let ui = new firebaseui.auth.AuthUI(firebase.auth());
            ui.start('#firebaseui-auth-container', uiConfig);
          }
      });
  }
}
</script>
```

```public/components/LoginPage.vue
<template>
    <div>
        <b>{{ loginMessage }}</b>
    </div>
</template>
<script>
module.exports = {
    data: () => ({
        currentUser: null,
        loginMessage: 'Loading...'
    }),
    mounted(){
        console.log( "login" );
        firebase.auth().onAuthStateChanged( user => {
            if ( user != null ){
                this.loginMessage = "ログインしました";
                this.currentUser = user;
            }else{
                this.logingMessage = 'ログインに失敗しました。';
            }
        });
    }
}
</script>
```

ファイルやディレクトリの構成は、下記のような感じになります。

```
.
├── firebase-debug.log
├── firebase.json
├── firestore-debug.log
├── firestore.indexes.json
├── firestore.rules
├── functions
│   ├── index.js
│   ├── node_modules
│   ├── package-lock.json
│   └── package.json
├── public
│   ├── 404.html
│   ├── components
│   │   ├── HomeView.vue
│   │   └── LoginPage.vue
│   └── index.html
├── storage.rules
└── ui-debug.log
```

# Firebase Emulator を起動する

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

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

こうなれば、エミュレータは起動できています。初回の起動時は、エミュレータをダウンロードが行われます。

## ブラウザで読み込む

エミュレータが起動している状態で、`http://localhost:5000/` をブラウザで開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/d88294d2-09e4-b204-ca34-02cdbe8f2b5e.png)

こんな感じのページが出たら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/243e6210-1218-e99c-6a74-9098ce20e6b3.png)

適当なメールアドレスを入れると、新規ユーザ登録できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/d9593ff4-bc12-5069-92ea-82798d0b7894.png)

SAVE を押して、正常に登録ができると、この表示になるはずで。ログインできましたと表示されていれば、正常に認証ができています。

## Emulator UI 

`http://localhost:4000/` をブラウザで開くと、エミュレータの状態を確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c7058260-2f22-80e9-156a-66084ef13b05.png)

Authentication のタブを開くと、登録済みのユーザが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/ce0f8cb1-0b01-2080-6bfc-9d9f1e92285a.png)

このページで、新たなユーザの追加や、既存のユーザの削除などの操作ができます。

## 注意点

Emulator を control-c などで落とすと、認証情報や Storage, Firestore などに入っているデータはすべて消えます。

データの保存方法や復元方法については、下記を参照してください。

- [Firebase の Local Emulator Suite を使う](https://qiita.com/mml/items/33633f50db0218c97a82)

ブラウザで開くときは、必ず `http://localhost:5000` を開きます。`http://127.0.0.1:5000` を開くと、内部で使用している Auth や Storage などはオンライン側が使われます。`http://127.0.0.1:5000` を開いてエミュレータ動作させたい場合は、index.html の下記を書き換えます。

```.js
const isEmulating = window.location.hostname === "127.0.0.1";
```