Vue.jsでカウンターアプリを実装～デプロイまで

#はじめに

Vue.jsの練習のためシンプルなカウンタアプリを作成しました。
作成した内容をまとめておきます。

#作成したもの


### アプリ概要

以下の画面のカウンタアプリを作成していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/9a17769b-4571-9c55-22b3-f6abf8803c8d.png)

画面にはカウント数のラベル、「Count」ボタン、「Reset」ボタンを配置しています。
Countボタンを押すとカウント数が1ずつ増加して、Resetボタンを押すとカウント数が0になります。

### 実装概要

Vue + Typescriptを使って作成しています。
1つのコンポーネントで作成できる程度の小規模なアプリですが、以下のことも取り入れました。
- レスポンシブデザイン：Vuetifyを導入
- AtomicDesign：これも練習のため少し取り入れてます
- デプロイ：firebaseにデプロイしました

### ソースコード
今回作成したものはGithubで公開しています。
コードの詳細を確認したい方は以下URL先からご確認ください。
https://github.com/Ruketa/simple-counter

それでは、実装した内容を紹介していきます。

#開発環境

以下のライブラリとFWを使用しました

- Vue.js
- Typescript
- vuetify
- node.js

以下のツールを使用しています

- Vue CLI
    - Vueで開発するためのツールです。<br> 以下サイトを参考にインストールして下さい。
    - 参考：https://cli.vuejs.org/
- Firebase CLI
    - Firebaseプロジェクトの管理、表示、デプロイを行う様々な機能が使えます。<br>以下サイトを参考にインストールして下さい。
    - 参考：https://firebase.google.com/docs/cli?hl=ja
    - firebaseへデプロイするためには事前にプロジェクトを作成する必要があります<br>
      お持ちでない方は公式サイトからプロジェクト作成をしてください。
        - 私はSparkプラン（無料プラン）を使っています
        - 公式サイト：https://firebase.google.com/?hl=ja  
      
# 実装内容

## プロジェクトの作成

まずvue cliを使って初期プロジェクトを作ります。
適当な場所に移動して以下コマンドでプロジェクトを作成します。
プロジェクト名はお好きな名前を指定してください。

```vue
vue create "プロジェクト名"
```

実行するとDefault設定を使うか問われるのでManually select featuresを選択して詳細設定を行います。
いくつかプロジェクト設定をどうするか問われるので選択していきます。
各項目は下図のように設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/29c5ed36-1cdb-58d4-fab2-8e92fb6bc423.png)

主な設定としては以下の二点です。

- TypeScriptを利用
- class style component syntaxは有効（Yesを選択）

その他の項目はLinterの設定等なのでお好きに設定頂いて構いません

しばらくしたら初期プロジェクトが作成されます。
そして、`npm run serve`を実行して,コンソールに出力されるアドレス（`http://localhost:8080とか`）へアクセスすれば動作確認できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/9aabde5d-a23e-97ba-5d4e-35f2c0bfd32d.png)

こんな画面が表示されます。楽チンですね。

## カウンタ機能の実装
ベースとなるプロジェクトを作れたのでまずは画面から作っていきます。
下図のファイルが作成されていて、編集対象となるのは拡張子が「.vue」のファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/8de673cc-ce84-0e5c-855b-87c9053c0eb7.png)
`App.vue`から始まって`HelloWorld.vue`を呼び出す構成になっているので`HelloWorld.vue`を編集していきます。

`***.vue`ファイルはシングルファイルコンポーネントといい、ファイル内は以下のタグで大きく区分けされています。

- `<template>`
    - HTMLの定義する箇所
- `<script>`
    - Typescript( or javascript ) を実装する箇所
- `<style>`
    - CSSを定義する箇所

詳細な説明は以下のサイト等をご覧ください

https://v3.ja.vuejs.org/guide/single-file-component.html#%E5%89%8D%E6%9B%B8%E3%81%8D

以下の流れで編集していきます。

- HTML、CSSの定義（`<template>`、`<style>`の部分）
- 部品をクリックしたときの動作の実装（`<script>`の部分)

#### HTML、CSSの定義

カウント番号は`<p>`タグ、count up、resetボタンは`<a>`タグで定義し以下のように作成しました。
HTML部分の定義だけではとても質素な見た目なので`<script>`の部分へ`<a>`タグ、`<p>`タグへのスタイルの定義をします。
ここはまだまだ未熟なのと、けっこう量が多くなるのでGithubでソースコードをご確認下さい。
※Githubへアップしているコードはコンポーネントを分割しているので`HwlloWorld.vue`ではなく、`CountButton.vue`、`CounterLabel.vue`、`ResetButton.vue`をご確認下さい。

```html
<template>
  <div>
    <p class="counter_label">{{ count_number }}</p>
    <a>count up</a>
    <a>reset</a>
  </div>
</template>
...
<style>
<!-- ※ここはGithubのソースコードをご参照下さい -->
</style>
```

#### Scriptの定義

続いて`<script>`の部分にボタンを押したときの動作を実装していきます。
以下のような感じです。

```typescript
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
// @ is an alias to /src
@Component
export default class Home extends Vue {
  private counter = 0;
  private c_clicked = false;
  private r_clicked = false;

  mousedown(target:string):void{
    if(target === "countup")
      this.c_clicked = true
    else
      this.r_clicked = true
  }

  mouseup(target:string):void{
    if(target === "countup")
      this.c_clicked = false
    else
      this.r_clicked = false
  }

  countup():void{
    this.counter++;
  }

  reset():void{
    this.counter = 0;
  }
}
</script>
```
#####解説
カウント数を表す値のために変数`counter`を定義しています。
カウント数を変化させるために`countup()`メソッド、`reset()`メソッドを定義します。
`countup()`メソッドはcounterの値を1増やす処理、`reset()`メソッドはcounterの値を0に初期化します。
あと、ボタンを押したときにボタンのスタイルを変更したいので、押下中を表すフラグとして`c_clicked`、`r_clicked`を定義します。
それぞれ、押したときにtrue、離したときにfalseを設定するメソッド`mousedown`、`mouseup`を定義します。
これで、表示用のカウント数とボタンを押したときの処理を実装できました。

##### HTMLとScriptの連結

まだ表示部分と処理部分が繋がっていないので、ボタンを押しても変化はありません。
ですので、次は`<script>`で定義した処理を`<template>`で定義した箇所と繋げていきます。
こんな感じです

```html
<template>
  <div class="home">
    <div class="count-area">
      <a class="counter-label">{{counter}}</a>
    </div>
    <div class="button-area">
      <a 
        v-bind:class="{'base cb-released':!c_clicked, 'base cb-pushed': c_clicked}"
        v-on:mousedown="mousedown('countup')"
        v-on:mouseup="mouseup('countup')"
        v-on:click="countup"
        >count</a>
    </div>
    <div class="button-area">
      <a class="reset" 
        v-bind:class="{'base rb-released':!r_clicked, 'base rb-pushed': r_clicked}"
        v-on:mousedown="mousedown('reset')"
        v-on:mouseup="mouseup('reset')"
        v-on:click="reset"
        >reset</a>
    </div>
  </div>
</template>
```

これでcountボタンを押すとカウント数が1ずつ増えていき、resetボタンを押すと0に初期化されます。

#####解説
カウント数を表す`<a>`タグの部分には変数counterの値を表示したいため`{{counter}}`を追記します。
これで変数counterの値が`<a>`タグによって表示されるようになります。
続いて、countボタン、resetボタンを表す`<a>`タグにそれぞれ`v-bind`、`v-on`を使ってボタンを押したときに表示状態を変更する処理、カウント数を変更する処理を関連付けます。
`v-bind:class=***`の部分でボタンを押しているかを表す`c_clicked`、`r_clicked`の値に応じてスタイルを割り当てています。
また、`v-on`で、ボタン上でマウスを押したとき、上げたときにフラグの値の更新、クリックが発生したときにカウント数の更新処理を呼び出すように関連付けています。

## アトミックデザインを導入

この時点でもカウンタアプリとして動作するのですが1コンポーネントにすべて詰め込まれていて、onmousedownでcountかresetかを見分けるために引数指定していたりでちょっとイケてない感じです。
ですので、アトミックデザインを参考にしてボタンやラベルをコンポーネントとして分割していきます。
アトミックデザインについては以下サイト等をご参照下さい。

https://design.dena.com/design/atomic-design-%E3%82%92%E5%88%86%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%A4%E3%82%82%E3%82%8A%E3%81%AB%E3%81%AA%E3%82%8B

今回は簡単なアプリですので、「atoms」、「molecules」、「organisms」の3つに分解するほどではないのと、「原子」、「分子」とかちょっと想像つきにくいので「component(部品)」と「composition(複合部品)」という名前で2つに分けました。
構成としては以下のようにしました。

``` 構成
src
  ┣ views
  ┃　┗ Home.vue(HelloWOrld.vueからファイル名を変更しました)
  ┗ components
　　┣ component
　　┃　┗ Label.vue
　　┗ composition 
　　　┣ CountButton.vue
　　　┣ CounterLabel.vue
　　　┗ ResetButton.vue
```
Home.vueからcompositionに作成したコンポーネントを使い、compositionの各コンポーネントはconponentのLabel.vueを使っている構成としました。
例えばこんな構図です。Home.vue→CountButton.vue→Label.vue
これで各コンポーネントに実装が分解されて扱いやすくなってきたと思います。

例えば、Label.vueの実装は以下のようにほぼ`<a>`タグのみの単純な感じになりました。

```vue  
<template>
  <a
    v-on:mousedown="$emit('mousedown')"
    v-on:mouseup="$emit('mouseup')"
    v-on:click="$emit('click')"
    >{{ label }}</a
  >
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class Label extends Vue {
  @Prop() public label!: string;
}
</script>
```

コンポーネントを分割したときにハマってしまったのが、プロパティとイベントハンドリングの部分です。
まずプロパティについてです。
各ボタンに対して表示文字を指定しますが、最終的にLabel.vueまで値が渡る必要があります。
ですので、`@Prop()`を使って各コンポーネントでプロパティを公開してHome.vueからの設定値をLabel.vueまで伝えていきます。
親コンポーネントから子コンポーネントにプロパティを渡してやる構図です。
これでCountButton、ResetButtonのラベルが表示されるようになります。

次にイベントハンドリングについてです。
ここが最初は中々わからなかったのですが、、、マウスをClickしたイベント等を最初に受け取るのはHTMLで定義された部品です。
今回の場合だと、Countボタンをクリックしたときにclickイベントを最初に受け取るのはLabel.vueとなります。
ですので、プロパティの時とは逆に子コンポーネントから親コンポーネントに向けてイベントを伝えないといけません。
そのため、子コンポーネントでは`$emit`を使ってイベントを親コンポーネントへ上げています。
この辺りは以下の記事が参考になると思います。

https://qiita.com/d0ne1s/items/f88ecd6aaa90c7bbc5d4

ここまでで、各部品毎のコンポーネントに分割してカウンタアプリが動作するようになりました。

## レスポンシブデザインを導入

もう少し改善を加えます。
カウンタ機能としては機能するようになったのですが、ブラウザの幅を狭くするとボタンが見切れてしまうことがあります。
ですので、レスポンシブデザインを導入してブラウザ幅が狭い時にボタンが縦並びになるようにしました。
レスポンシブデザインというのは、複数の異なる画面サイズをWebサイト表示の判断基準にしてページのレイアウト・デザインを柔軟に調整する方法です。
これによって、PC、タブレット、スマートフォン用の画面を1つのHTMLで表現できます。
レスポンシブデザインについての詳細な説明は以下サイト等が参考になると思います。

https://liginc.co.jp/designer/archives/6154

https://qiita.com/kztmy/items/221cc6b3f0ef5d2b3d3a

今回は`Vutify`を使ってレスポンシブデザインを導入したいと思います。
Vutifyの公式サイトのインストール手順に従って以下コマンドを入力します。

```
vue add vutify
```

https://vuetifyjs.com/ja/getting-started/installation/#vue-cli-306b3088308b30a430f330b930c830fc30eb

これでVutifyがプロジェクトに追加されて機能が使えるようになります。
そして、`Home.vue`の`<template>`内のHTML定義を以下のように修正しました。

```vuejs
<template>
  <v-container fluid>
    <div>
      <v-row >
        <v-col cols="12">
          <CounterLabel :label="count_number" />
        </v-col>
      </v-row>
      <v-row >
        <v-col cols="12" xs="10," sm="6" md="6" lg="6" xl="6">
          <CountButton @click="countup" />
        </v-col>
        <v-col cols="12" xs="10," sm="6" md="6" lg="6" xl="6">
          <ResetButton @click="reset" />
        </v-col>
      </v-row>
    </div>
  </v-container>
</template>
```

##### 解説

vutifyではグリッドシステムを備えており、`<v-container>`、`v-row`、`v-col`を使ってレスポンシブデザインを表現していきます。
`<v-container>`はグリッド全体、`v-row`は一行分、`v-col`は`v-row`内での1つのセルを表しています。
関係を図示すると以下のような形になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/843b7050-d798-988d-340a-ed89220182d9.png)
上記コードの例では、一行（`v-row`）を12セル（`v-col`）に分割してxs～xlまでの各ブラウザ幅サイズにおいて`v-col`が使うセル数をxsの時は10個、それ以外は6個に指定しています。
（各サイズ毎のブラウザ幅については以下リンクを御参考下さい。）

https://vuetifyjs.com/ja/components/grids/

ですので、ブラウザ幅がxs(< 600px)となったときに、`CountButton`、`ResetButton`がそれぞれ一行内でセルを10個使うようにレイアウトされます。
一行内のセル数は12個なので、一行で収まらず改行が入り`CountButton`、`ResetButton`は縦に並ぶ動きとなります。
これで簡単ですがレスポンシブデザインを実現できました。

#デプロイ

最後に作成したウェブアプリをfirebaseへデプロイします。
現状では自分のローカル環境だけでした確認できないので例えばスマホで表示してみることはできません。
ですので、firebaseのようにホスティング機能を提供するサービスへWebアプリをアップロードすることでWebアプリが外部公開されて自分のスマホでも見れるようになります。
以下のような構図です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/b09cd9ce-a3e3-9734-ac3a-4d693552cf04.png)

デプロイするための手順ですが、まずはfirebaseの初期設定を追加します。
以下コマンドを実行することで追加できます。

```
firebase init
```

以下のような感じでいくつか確認されるので選択していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536019/b36cf45f-2ab3-60c0-a1f2-c9b540c1e396.png)

私の設定内容は以下の通りです。

- どのfirebaseの機能を使うか→ Hostingを選択
- public directoryをどこにするか→ お好きな場所へ。デフォルトは./publicとなります。
- Single page applicationとして設定するか→Yes
- Githubを使う設定とするか→私はNoとしました
- public directory以下のindex.htmlを上書きしていいか→既にある場合はNo

これでfirebase設定が出来たので以下のコマンドでデプロイを実行します。

```
firebase deploy
```

プロジェクトが複数ある場合などはデプロイ先を選ぶ質問が出てきますが選択後にしばらく待つと処理が進みデプロイが完了します。
デプロイ先のURLが表示されると思いますので、ブラウザで指定して開くとカウンタアプリが表示されると思います。
これはfirebase上でホスティングされていますので、ご自身のスマホからでもアクセス可能です。
※URLは頑張って入力してください。

これで、ようやくデプロイまで完了できました。

#おわり

Webアプリ開発は初心者ですが取り組んだ内容を記載させて頂きました。
色々書いて長くなってしまいましたが、、、ここまで読んで頂きありがとうございました。
発展的な内容はないかと思いますが、何かお役に立つ情報があれば幸いです。