Vueの基本を復習します

n.yuumi
n.yuumi

はじめに

久しぶりにVueを触ってみたら、色々と記憶がなくなっていたので復習も兼ねてブログを書いて、Vueでよく使うv-ifやv-forディレクティブ、computedやwatch、methods処理などの動きを確認していきます。

Vueで作ってみる

今回は、暇つぶし用のシンプルなゲームを作成したいと思います。
Optionにあるボタンを使って相手を倒します。右側の黒背景のLogsには、Optionで選択した内容を、そして左下にはHPを動的に表示します。

Screen Shot 2022-02-02 at 11.16.04.png

そして、終わったら結果を表示させ、再スタートできるようにしたいと思います。

Screen Shot 2022-02-02 at 14.22.06.png

まずは、jsファイルを用意します。Vueアプリケーションを生成し、そのVueアプリケーションをマウントします。
お決まりの書き方なので、呪文のように唱えて覚えていきます...。
動作確認用にdata属性を設定しました。

const app = Vue.createApp({
    data() {
        return {
            testTxt: 'テストです'
        }
    }
});
app.mount('#test');

次に、HTML側にマウントしたidを持つタグを作成します。
ブラウザにアクセスし{{testTxt}}の部分が「テストです」と表示されれば準備完了です。

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next" defer></script>
    <script src="./app.js" defer></script>
  </head>
  <body>
      <div id="test">
          <p>{{testTxt}}</p>
      </div>
  </body>
</html>

まずは、Option部分から作っていきますが、挙動は大きく分けると3つあります。
Punch,Kick,Hammer,Super PowerボタンはHPを減らし、Potionボタンは自分のHPを増やします。また、Take a breakとDoneは下記のようにポップアップを表示する仕様です。

Screen Shot 2022-02-02 at 11.42.40.png

まず、例としてHPを減らすためのPunchボタンを作成していきます。
ボタンをクリックした時の処理を@clickを使い、引数には攻撃の最大値、最小値、種類をそれぞれ設定しました。

<button class="btn" @click="attckOnFriend(15,5,'punch')">
    Punch<i class="far fa-hand-rock face"></i>
</button>

data属性に自分と相手のHPをそれぞれ100と設定し、Math.random関数で作成されたランダムな数字をそこから引いていきます。1人用のゲームなので、相手に攻撃した時に自分のHPも減るようにしました。処理はmetods属性に書いていきます。
Kick、Hammer、Super Powerは、最大値、最小値の引数を変えてそれぞれ設定してください。

data() {
    return {
        yourHitPoint: 100,
        friendHitPoint: 100,
    }
},
methods: {
    attckOnFriend(max, min, how) {
        let val = this.updateHitPoint(max, min)
        this.friendHitPoint = this.friendHitPoint - val;
        this.attackOnYou();
    },
    attackOnYou() {
        let val = this.updateHitPoint(15, 10)
        this.yourHitPoint = this.yourHitPoint - val;
    },
    updateHitPoint(max, min) {
        let randomNum = Math.floor( Math.random() * (max - min + 1) ) + min ;
        console.log(randomNum);
        return randomNum;
    },
}

Potionも同じ要領で作成していきます。こちらは回復用なので、Math.random関数で生成した数値を足していきます。こちらでも、1人遊びようなので回復した後に自動的に攻撃を受けるようにしてあります。

<button class="btn" @click="getPotion(10,5,'potion')">Potion<i class="fas fa-capsules face"></i></button>
getPotion(max, min, how) {
    let val = this.updateHitPoint(max, min)
    this.yourHitPoint = this.yourHitPoint + val;
    this.attackOnYou();
},

次は、cssのwidthスタイル値を変えてHPの表示を動的に変化させていきたいと思います。

Screen Shot 2022-02-02 at 12.14.49.png

 

算出プロパティのcomputedに処理を追加していきます。data属性に定義したyoutHitPointfriendHitPointの値を確認し、widthに0〜100%を設定されるようにしました。スタイルで色をつけているのでバーの色を動的に変化させることができます。

//自分
<div class="power">
    <div class="power__bar" :style="yourHitPointWidth"></div>
</div>

//相手
<div class="power">
    <div class="power__bar" :style="friendHitPointWidth"></div>
</div>
computed: {
    //自分
    yourHitPointWidth() {
        if(this.yourHitPoint < 0) {
            return { width: '0%'};
        } else {
            return {width: this.yourHitPoint + '%'};
        }
    },
    //相手
    friendHitPointWidth() {
        if(this.friendHitPoint < 0) {
            return { width: '0%'};
        } else {
            return {width: this.friendHitPoint + '%'};
        }
    },
},

次はログを作成していきます。
ここでは、攻撃の種類とダメージがどのくらいあったかを記録したいです。情報を保存しておくための変数と、その情報を追加していく処理をdata属性とmethods属性に追加していきます。

data() {
    return {
        yourHitPoint: 100,
        friendHitPoint: 100,
        //ログ表示用に追加
        logMsgs: [],
    }
},
methods: {
    attckOnFriend(max, min, how) {
        this.counter++;
        let val = this.updateHitPoint(max, min)
        this.friendHitPoint = this.friendHitPoint - val;
        this.attackOnYou();
        //ログ表示用に追加
        this.updateLogs(how,val);
    },
    
    //配列の先頭にデータを追加
    updateLogs(option, number) {
        this.logMsgs.unshift({
            how: option,
            value: number,
        })
    },
}

v-forディレクティブを使い、logMsg変数にセットされた配列のデータをループして描画していきます。攻撃手段と攻撃値をそれぞれhowとvalueに設定したので、logMsg.howlogMsg.valueと指定してください。potionだけ回復用なので、色を変えるためにclassも動的につくようにしました。

Screen Shot 2022-02-02 at 12.51.23.png

<ul>
    <li v-for="logMsg in logMsgs" :class="logMsg.how !== 'potion' ? '' : logMsg.how">
        <span>{{logMsg.how}}</span>
        <span>{{logMsg.value}} point</span>
    </li>
</ul>

このままだと永久に自分が勝ち続けてしまいそうなので、PotionボタンとSuper Powerボタンが使えるタイミングを指定していきます。これらのボタンにdisabledが動的に設定され選択できないようにしたいと思います。
counter変数をdata属性に追加し、Optionボタンで攻撃、回復した時にcounter++していきます。今回は、5回に1度だけdisabled属性が外れるように指定しました。

<button class="btn" :disabled="isDisabled" @click="getPotion(10,5,'potion')">Potion<i class="fas fa-capsules face"></i></button>
data() {
    return {
        yourHitPoint: 100,
        friendHitPoint: 100,
        //追加
        counter: 0,
        logMsgs: [],
    }
},

computed: {
    isDisabled() {
        return (this.counter % 5 !== 0);
    },
},

それでは、自分と相手どちらが勝ったかを表示させていきます。

Screen Shot 2022-02-02 at 13.07.54.png

結果が出た時だけ表示したいので、v-ifディレクトリと監視プロパティを使って描画したいと思います。
自分と相手のHPの値を監視し、どちらかもしくは両方が0になったらwhoIsWinner変数に値をセットします。whoIsWinnerはdata属性に追加してください。初期値をnullに設定し、Draw、Friend、Youどれかの文字列がセットされたらポップアップが表示されるようにしました。

<div class="modal" v-if="whoIsWinner">
    <div class="popup">
        <div class="popup__txt">
            <p v-if="whoIsWinner === 'Draw'">{{whoIsWinner}}</p>
            <p v-else>{{whoIsWinner}} won</p>
            <button class="btn btn-lg" @click="restartGame">Restart</button>
        </div>
    </div>
</div>
data() {
    return {
        yourHitPoint: 100,
        friendHitPoint: 100,
        //追加
        whoIsWinner: null,
        counter: 0,
        logMsgs: [],
    }
},
watch: {
    yourHitPoint(hpValue) {
        if(hpValue <= 0 && this.friendHitPoint <= 0) {
            this.whoIsWinner = 'Draw';
        } else if(hpValue <= 0) {
            this.whoIsWinner = 'Friend';
        }
    },
    friendHitPoint(hpValue) {
        if(hpValue <= 0 && this.friendHitPoint <= 0) {
            this.whoIsWinner = 'Draw';
        } else if(hpValue <= 0) {
            this.whoIsWinner = 'You';
        }
    },
},

全体のコードは以下のようになりました。

<body>
  <div style="max-width: 1000px; margin: 10px auto;">
    <header>
        <h1>Vue Learning</h1>
        <p></p>
    </header>
    <main id="test">
        <div class="option-log">
            <div class="option">
                <h2>Option</h2>
                <div>
                    <button class="btn" @click="attckOnFriend(15,5,'punch')">Punch<i class="far fa-hand-rock face"></i></button>
                    <button class="btn" @click="attckOnFriend(20,5,'kick')">Kick<i class="fas fa-shoe-prints face"></i></button>
                    <button class="btn" @click="attckOnFriend(25,5,'hammer')">Hammer<i class="fas fa-hammer face"></i></button>
                    <button class="btn" :disabled="isDisabled" @click="attckOnFriend(30,5,'super power')">Super Power<i class="far fa-star face"></i></button>
                    <button class="btn" :disabled="isDisabled" @click="getPotion(10,5,'potion')">Potion<i class="fas fa-capsules face"></i></button>
                    <button class="btn" @click="takeABreak">Take a break<i class="fas fa-utensils face"></i></button>
                    <button class="btn" @click="done">Done<i class="far fa-dizzy face"></i></button>
                </div>
            </div>
            <div class="log">
                <h2>Logs</h2>
                <ul>
                    <li v-for="logMsg in logMsgs" :class="logMsg.how !== 'potion' ? '' : logMsg.how">
                        <span>{{logMsg.how}}</span>
                        <span>{{logMsg.value}} point</span>
                    </li>
                </ul>
            </div>
        </div>
        <div>
            <div class="power-wrap">
                <h2>You<i class="far fa-smile face"></i> HP: {{yourHitPoint}}</h2>
                <div class="power">
                    <div class="power__bar" :style="yourHitPointWidth"></div>
                </div>
            </div>
            <div class="power-wrap">
                <h2>Friend<i class="fas fa-grimace face"></i> HP: {{friendHitPoint}}</h2>
                <div class="power">
                    <div class="power__bar" :style="friendHitPointWidth"></div>
                </div>
            </div>
        </div>
        <div class="modal" v-if="whoIsWinner">
            <div class="popup">
                <div class="popup__txt">
                    <p v-if="whoIsWinner === 'Draw'">{{whoIsWinner}}</p>
                    <p v-else>{{whoIsWinner}} won</p>
                    <button class="btn btn-lg" @click="restartGame">Restart</button>
                </div>
            </div>
        </div>
        <div class="modal" v-if="isBreak">
            <div class="popup">
                <div class="popup__txt">
                    <button class="btn btn-lg" @click="continueGame">Start where I stopped</button>
                </div>
            </div>
        </div>
    </main>
</div>
</body>
const app = Vue.createApp( {
    data() {
        return {
            yourHitPoint: 100,
            friendHitPoint: 100,
            whoIsWinner: null,
            counter: 0,
            logMsgs: [],
            isBreak: false,
        }
    },
    computed: {
        yourHitPointWidth() {
            if(this.yourHitPoint < 0) {
                return { width: '0%'};
            } else {
                return {width: this.yourHitPoint + '%'};
            }
        },
        friendHitPointWidth() {
            if(this.friendHitPoint < 0) {
                return { width: '0%'};
            } else {
                return {width: this.friendHitPoint + '%'};
            }
        },
        isDisabled() {
            return (this.counter % 5 !== 0);
        },
    },
    watch: {
        yourHitPoint(hpValue) {
            if(hpValue <= 0 && this.friendHitPoint <= 0) {
                this.whoIsWinner = 'Draw';
            } else if(hpValue <= 0) {
                this.whoIsWinner = 'Friend';
            }
        },
        friendHitPoint(hpValue) {
            if(hpValue <= 0 && this.friendHitPoint <= 0) {
                this.whoIsWinner = 'Draw';
            } else if(hpValue <= 0) {
                this.whoIsWinner = 'You';
            }
        },
    },
    methods: {
        attckOnFriend(max, min, how) {
            this.counter++;
            let val = this.updateHitPoint(max, min)
            this.friendHitPoint = this.friendHitPoint - val;
            this.attackOnYou();

            this.updateLogs(how,val);
        },
        attackOnYou() {
            let val = this.updateHitPoint(15, 10)
            this.yourHitPoint = this.yourHitPoint - val;
        },
        getPotion(max, min, how) {
            this.counter++;
            let val = this.updateHitPoint(max, min)
            this.yourHitPoint = this.yourHitPoint + val;
            this.attackOnYou();

            this.updateLogs(how,val);
        },
        takeABreak() {
            this.isBreak = true;
        },
        updateHitPoint(max, min) {
            let randomNum = Math.floor( Math.random() * (max - min + 1) ) + min ;
            console.log(randomNum);
            return randomNum;
        },
        updateLogs(option, number) {
            this.logMsgs.unshift({
                how: option,
                value: number,
            })
        },
        done() {
            this.whoIsWinner = 'Friend'
        },
        restartGame() {
            this.yourHitPoint = 100;
            this.friendHitPoint = 100;
            this.whoIsWinner = null;
            this.counter = 0;
            this.logMsgs = [];
            this.isBreak = false;
        },
        continueGame() {
            this.isBreak = false;
        },
    },
});

app.mount('#test');

最後に

少しVueの記憶を取り戻せたので、忘れないようにまた何か作ってみようと思います。

こちらのブログにもVueに関して書いているので、チェックしてみてください。

Vue RouterでURLを動的に制御する

Vueコンポーネントの受け渡しについて