דוגמת ויו: משחק איקס עיגול
לפני כמה שנים כתבתי מדריך על Vue ואחד הדברים שאז לא עבדו מספיק טוב היה מנגנון הריאקטיביות. רוב הזמן זה היה בסדר אבל היה יחסית קל להגיע לקצוות מבלבלים. לאחרונה אני מרענן את החומרים על Vue לקראת קורס שאני מעביר בנושא ושמח לגלות שהריאקטיביות עובדת הרבה יותר טוב היום. הנה דוגמה קצרה לפיתוח משחק איקס עיגול ריאקטיבי ב Vue, עם כל הפינוקים של הפרדה בין קובץ לוגיקה לקומפוננטות.
1. לוגיקת המשחק
הדרך הסטנדרטית ב Vue לתקשר בין לוגיקה לקומפוננטות היא ריאקטיביות. הקסם של Vue (בניגוד לריאקט), הוא שאפשר ליצור משתנים ריאקטיביים בכל מקום ולא רק בתוך קומפוננטות וכך אפשר לבנות לוגיקה שכוללת State מחוץ לקומפוננטות. בדוגמה שלנו זה הקוד של משחק איקס עיגול שמחזיק סטייט ריאקטיבי:
import {ref, Ref} from 'vue';
export default class ReactiveTicTacToe {
board: Ref<Array<Array<'O'|'X'|'.'>>>;
winner: Ref<undefined|'O'|'X'>;
currentPlayer: Ref<'O'|'X'>;
constructor() {
this.board = ref([
['.', '.', '.'],
['.', '.', '.'],
['.', '.', '.']
]);
this.currentPlayer = ref('X');
this.winner = ref();
}
play(row: number, column: number) {
if ((this.winner.value) || (this.board.value[row][column] !== '.')) {
return;
}
this.board.value[row][column] = this.currentPlayer.value;
if (this.checkWinner(this.currentPlayer.value)) {
this.winner.value = this.currentPlayer.value;
}
this.currentPlayer.value = this.nextPlayer();
}
nextPlayer() {
if (this.currentPlayer.value === 'X') {
return 'O'
} else {
return 'X'
}
}
checkWinner(player: 'X' | 'O') {
const winningCoordinates = [
[[0, 0], [0, 1], [0, 2]],
[[1, 0], [1, 1], [1, 2]],
[[2, 0], [2, 1], [2, 2]],
[[0, 0], [1, 0], [2, 0]],
[[0, 1], [1, 1], [2, 1]],
[[0, 2], [1, 2], [2, 2]],
[[0, 0], [1, 1], [2, 2]],
[[0, 2], [1, 1], [2, 0]]
]
if (winningCoordinates.some(triplet =>
triplet.every(([row, column]) => (
this.board.value[row][column] === player
))
)) {
return true;
}
return false;
}
}
זה קוד שקל לעבוד איתו, קל לבדוק אותו בבדיקות יחידה או לשלב אותו יחד עם קבצי לוגיקה אחרים. אפשר להגיד שקוד כזה היה כל מה שתמיד רציתי מ mobx, אבל דברים שם אף פעם לא עבדו מספיק טוב.
2. הקומפוננטה
ויו מעודד כתיבת לוגיקה פשוטה של קומפוננטה בתוך סקריפט האיתחול שלה, אבל כמובן שדברים יותר מורכבים עדיף לכתוב בקובץ נפרד. אחרי שכתבנו את קובץ הלוגיקה נוכל להשתמש בו מתוך קומפוננטה באופן הבא:
<script setup lang="ts">
import ReactiveTicTacToe from '../game/logic';
const {game} = defineProps<{game: ReactiveTicTacToe}>()
</script>
<template>
<div>
<h1 v-if="game.winner.value">Bravo! {{ game.winner }} won</h1>
<h1 v-else>Next player = {{ game.currentPlayer }}</h1>
<div v-for="(_, row) in 3" :style="{display: 'flex'}">
<div v-for="(_, column) in 3" :style="{flex: 1, cursor: 'pointer'}">
<div
@click="game.play(row, column)"
>
{{ game.board.value[row][column] }}
</div>
</div>
</div>
</div>
</template>
ובטח שכיף להשתמש בקיצורי הדרך של vue במיוחד באפשרות של לולאה על מספר בתוך ה v-for ובאפשרות לרשום ב @click
משהו שנראה כמו הפעלה של פונקציה ו vue כבר יהפוך את זה לפונקציה.
לסיום בשביל להשתמש במשחק נוכל להפעיל אותו באופן הבא:
<script setup lang="ts">
import TicTacToeGame from './components/TicTacToeGame.vue';
import ReactiveTicTacToe from './game/logic';
</script>
<template>
<TicTacToeGame :game="new ReactiveTicTacToe()"/>
<TicTacToeGame :game="new ReactiveTicTacToe()"/>
<TicTacToeGame :game="new ReactiveTicTacToe()"/>
</template>
3. עכשיו אתם
ולסיום תרגיל לאלה מכם שרוצים לצלול קצת יותר לעומק הריאקטיביות ב Vue. אני מוסיף את הקוד הבא ל script setup של הקומפוננטה TicTacToeGame.vue:
onUpdated(() => {
console.count(`TicTacToeGame::updated`);
})
ומגלה שכל לחיצה על כל משבצת גורמת לקומפוננטה להתעדכן. בגלל שהקומפוננטה גדולה עדכון שלה אומר להריץ מחדש את שתי הלולאות.
בנו קומפוננטה בשם Square שאחראית על הצגת תוכן משבצת בודדת, וקומפוננטה נוספת בשם Player שאחראית על הצגת השחקן הנוכחי - וגם בשתיהן הוסיפו את קטע הקוד שמקשיב ל updated ומדפיס כשיש עדכון לקומפוננטה.
עכשיו מצאו דרך לעדכן את הקוד כך שלחיצה על משבצת תגרום לעדכון רק של הקומפוננטות Square ו Player אבל לא לעדכון של הקומפוננטה הראשית TicTacToeGame.