初心者でもできるVueとFirebaseでSPAなスマホでできる子ども用足し算練習webサービスをリリースする方法

# 完成形

https://edu-keisan.web.app/

# 準備

vue cliを使う。
https://cli.vuejs.org/
ここでつまづいたら、インストール方法に関する記事はこの辺
https://qiita.com/kamitomo/items/34451b11caaf51bd2498

```
$ vue --version
3.10.0
```

（コマンド実行する所と結果を見分けるため、コマンド実行している所は先頭に```$```を付ける）

準備は以上。

# vueに必要なファイルを自動生成してもらう

```
$ vue create ui
```
uiはフォルダ名。好きに書き換えて良い。

下記のように質問来るけど、defaultでenterを押す

```
Vue CLI v3.10.0
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features 
```

これでvueに必要なファイルの作成は完了。

下記のようなコマンド実行すると、localhostでサーバーが起動する。

```
 $ cd ui
 $ yarn serve
```

```
$ yarn serve
 DONE  Compiled successfully in 4303ms                                                                                                                                     9:59:38 AM
                                                                                                                                                                                     
  App running at:
  - Local:   http://localhost:8082/ 

  Note that the development build is not optimized.
  To create a production build, run yarn build.
```

ポート番号は空いている所が自動的に。
（ちなみに、vue cli 3.10.0からは、```--port```がサポートされたようで）

ブラウザで見ると、こんな感じに

<img width="300"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/1e876c80-553a-eaff-fd77-95d014839907.png" style=":1px">

# トップページを書き換える

トップページはsrc/App.vueというファイルになる。

書き換え前はこんな感じ

```vue:src/App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

書き換え後

```vue:src/App.vue
<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'app',
}
</script>

<style>
</style>
```

１行```<router-view></router-view>```を追加したので注意。
ここにページが自動的に埋まるようになる。
それ以外の所は装飾して遊んでみてください。

# vue routerを使ってみる

これを使うと複数のページをURLに従って切り替えしてくれる。
先ほどの```<router-view></router-view>```が切り替わる。
今回の範囲は１ページなので使わなくていいけど、拡張できるように使ってみる。

先程作成したuiフォルダで

```
$ yarn add vue-router
```

これで、vue routerが使えるようになったので、vue routerを使うjsファイルを作成する。
ファイル名は何でも良い。

```js:src/router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router);

function loadView(view) {
    return () => import(`@/views/${view}.vue`)
}

const router = new Router({
    mode: 'history',
    routes: [
        {
            path: "/",
            name: "check",
            component: loadView('Check'),
        },
    ]
})

export default router
```

ブラウザで```/```のパスの場合に表示するページがCheck.vueになるという設定で。
だいたいこんな感じで動く。

# router.jsを使う設定

main.jsが本当に中心となるファイルで、ここに書くと実行してもらえるようにできている。
App.vueもここから呼び出されている。
ここで```router```を読み込んで、vueに使ってもらうように設定する。
router.jsを読み込むコードは```import router from './router'```

```js:src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
```

./routerのように```.js```は省略（書いてもいいけど）
importで```'vue'```みたいに先頭に```./```書いていないやつはライブラリ。
ライブラリは先ほどのように```yarn add```みたいな感じでもらってくる。
importで```@/```って書いてあったらは```src/```の事。今回は入ってない。
という知識を持っていたら色々なサンプルコードが読めるようになる。

# Check.vueの作成

src/views/Check.vueというファイルを作成する

```vue:src/views/Check.vue
<template>
    <div>check</div>
</template>

<script>
    export default {
        name: "Check"
    }
</script>

<style scoped>

</style>
```

これは、先程の```router.js```から呼び出すファイルとなる。

ここまでの実装が完了すると、ブラウザで開いた場合は、こんな感じ

<img width="300"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/631f3cef-5bd1-da4c-fa77-8a0d9f61e1ce.png">

あとは、ここの実装を増やしていくだけで終わり。

# v-if, data, methodsを使ってみる

計算中か計算中以外の表示を切り替える。
ボタンを押したら切り替わるようになる。

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/e36831cd-f087-d33e-a0bc-a02bf0a83a03.png">

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/c45b75ea-67c9-a28e-aa7c-9ed9a74cf299.png">


```vue:Check.vue
<template>
    <div>
        <div v-if="isChecking">
            <button @click="checkAns">こたえあわせ</button>
        </div>
        <div v-else>
            <button @click="start">スタート</button>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Check",
        data: () => ({
            isChecking: false,
        }),
        methods: {
            checkAns() {
                this.isChecking = false
            },
            start() {
                this.isChecking = true
            }
        },
    }
</script>

<style scoped>
    button {
        font-size: 24px;
    }
    input {
        font-size: 24px;
        width: 60px;
    }
    div {
        font-size: 24px;
    }
</style>
```

画面切り替えはこれで完成。
あとはロジックを増やしていくだけ。
styleは好みで設定。
タグの中に```<div style="font-size: 24px">```でもよい。

# {{{xxxx}}}とv-modelを使ってみる

* 計算できるように、ランダムの数字を表示する
* 入力できるようにする

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/2a7597e1-4227-e54d-9298-c825fcf06d13.png">

```vue:Check.vue
...変更なし
        <div v-if="isChecking">
            <br>
            {{leftNum}} + {{rightNum}} =
            <input type="number" v-model="ans" style="font-size: 24px; width: 60px">
            <br>
            <button @click="checkAns" style="font-size: 24px">こたえあわせ</button>
        </div>
...変更なし
<script>
    export default {
        name: "Check",
        data: () => ({
            leftNum: 0,
            rightNum: 0,
            ans: "",
            isChecking: false,
        }),
        methods: {
            checkAns() {
                this.isChecking = false
            },
            next() {
                const l = Math.floor(Math.random() * 9) + 1
                const r = Math.floor(Math.random() * 9) + 1
                this.leftNum = l
                this.rightNum = r
                this.ans = ""
            },
            start() {
                this.isChecking = true
                this.next()
            }
        },
    }
</script>
```
* {{{変数名}}}で、dataの値が表示できる
* v-model="変数名"で、inputのデータをセットすることができる

ほぼ出来上がった感がある。

# watchを使ってみる

* 計算にかかった時間を計測する。
* isCheckingがtrue中の時間を計測すればよい
* 単にwatchを使いたいだけで、実際は使わなくてもいいけど・・・

<img width="250"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/3c47e10f-7bdd-3b71-3d85-f8de8e2997c0.png">

```vue:Check.js

        <div v-else>
            <br>
            <button @click="start" style="font-size: 24px">スタート</button>
            <br>
            <div>
                じかん：{{elapsedTime}}びょう
            </div>
        </div>
...       
        data: () => ({
            leftNum: 0,
            rightNum: 0,
            ans: "",
            isChecking: false,
            startTime: 0,
            elapsedTime: 0,
        }),
        methods:{
         ....
        },
        watch: {
            isChecking(v) { // isCheckingが変更されるたびに呼ばれる
                if (v) { // isCheckingがtrueに変更
                    this.startTime = Date.now()
                } else { // isCheckingがfalseに変更
                    this.elapsedTime = Math.floor((Date.now() - this.startTime) / 1000)
                }
            }
        },
```

# UXを向上させる

このままだと入力が難しい！！！

* スマホブラウザで電卓キーボードに切り替える
* オートフォーカス（Vue.nextTickを使ってみる）

## 電卓キーボードに

スマホでみた時に、入力しやすさを考えて電卓キーボードに変更したい

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/b9d9012e-a4e7-3bb0-8dfe-93434a311cc8.jpeg">

```vue:Check.js
  <input type="number" v-model="ans" pattern="\d*" id="ans">
```

## オートフォーカス（Vue.nextTickを使ってみる）

* こたえあわせを押す毎にキーボードが閉じられるので、入力枠にオートフォーカスする必要がある
* オートフォーカスは画面の描写が終わった後にしかできない難しさがあるので、技を使う必要がある

```
...
<script>
    import Vue from 'vue'

            next() {
                const l = Math.floor(Math.random() * 9) + 1
                const r = Math.floor(Math.random() * 9) + 1
                this.leftNum = l
                this.rightNum = r
                this.ans = ""
                Vue.nextTick(function () {
                    document.getElementById('ans').focus();
                })
            },
```

この書き方なら応用がきくと思うので、ここではこんな感じで書いてみた。

# リリース用のファイルを作成する

```
$ yarn build
```

これでdistフォルダが自動的に作成され、その中のファイルが公開用のファイル

# Webサービスとして全世界に公開

## Firebase Hostingの設定

* firebaseのhostingを使うと先程作ったSPAを簡単に公開できる
* https://firebase.google.com/?hl=ja
* firebaseにアクセスして```プロジェクトを追加```をクリック

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/87cfacf2-d315-9498-f465-8fb9be6d04db.png">

プロジェクト名は好きな名前で（ここで付けた名前がURLになる）
<img width="300"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/1f28ad29-3624-6ad7-f9f4-3964596d3a07.png">

「今は必要ない」を選択して「プロジェクトを作成」をクリックする。
A/Bテスト・・・とかなんとか色々できそうな雰囲気が伝わってくるが、webサービスでは使えないものが並んでいる。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/92a3636b-195a-2664-8a67-b82cd1930647.png">

<img width="290"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/4c601262-702a-fe3e-aea6-4474dfa0bd47.png">

ここで```Hosting```をクリックする
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/323891f3-0629-1bc7-bade-dea781b516f7.png">

ここで```始める```をクリック
<img width="300"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/48de66be-6657-af59-0ec6-e61d8ba846ef.png">

表示される順番でコマンドを実行する。
注意点があるとすると、```firebase login```で```ログイン済み```みたいな感じになったら、```firebase logout```を実行して、再度実行すると、後に問題が出ない。
<img width="300"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37624/80bc36df-3884-ca90-27a3-92eca3dd0018.png">

一番最初に作ったuiフォルダ内でfirebase initを実行する
上下キーでHostingを選択して、スペースキーで選択して、enter

```
$ cd ui
$ firebase init
....
 ◯ 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
```

先程作成したプロジェクト名を選択してenter

```
? Select a default Firebase project for this directory: 
  [don't setup a default project] 
❯ test97668183 (test97668183) 
  [create a new project] 
```

先程```yarn build```作成した```dist``` と入力してenterすると、distフォルダがアップロードできる

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

親切に、```シングルページアプリケーションか？```と聞いてくるのでyを入力

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

index.htmlがあるけど上書きするか？と聞いてくるので、N

```
? File dist/index.html already exists. Overwrite? (y/N) 
```

distフォルダをサーバー(firebase hosting)にアップロード

```
$ firebase deploy
```

完成。

# URLはfirebaseapp.comだけではない

デプロイ後にURLはfirebaseapp.comのほうを案内されるけど、web.appでもアクセスできる。

https://edu-keisan.firebaseapp.com/
https://edu-keisan.web.app/

アクセス先は同じ。

# 補足：xxxx.mapは削除してみたい

yarn build の時に使われる設定例としてやってみる。

```js:ui/vue.config.js
module.exports = {
    productionSourceMap: false,
}
```

このファイルを作成して、```yarn build```すると、mapファイルが作成されていない。
あとは、これを```firebase deploy```とするとアップロードされる。

# 補足：ルート以外のパスで動かしたい

URLのパスでルート以外で動作させたいという気になった場合は、publicPathを設定する。

```js:ui/vue.config.js
module.exports = {
    publicPath: 'calc',
    productionSourceMap: false,
}
```

とかとか、あとは好きに検索して遊んでみて。

# 最後に

せいかい、まちがいカウントに関しては、ここまで来ると自力で作れると思うので、あとは自力でやってみてください。
ここにあるコードでは出てきません。