😀Vue3实现React官方示例的井字棋游戏
昨天用React
写了官方示例的井字棋游戏,今天我们来用Vue3
重新把它实现一次,感受一下Vue
和React
的开发体验上的区别
今天就直接上手开发了,主要会讲一下相同的概念在两个框架之间的不同点
Square组件
首先是最简单的棋盘格子组件,它接收一个squareContent
属性,并且有一个自定义事件fill-square
,用于通知父组件往格子中落棋,相较于React
,Vue
有defineEmits
,可以让我们很方便地实现自定义事件
当然,Vue
也可以像React
那样,将自定义事件作为props
,由父组件传入回调,子组件在合适的时候调用这个回调,实现和自定义事件一样的子组件通知父组件的效果,但我认为这样的写法可读性会低一些,所以我更倾向于使用自定义事件的方式去实现
此外,得益于Vue
单文件组件的设计,我们可以将组件对应的样式写在同一个文件中,而不用像React
那样,每个组件要写样式的话还得新建一个对应的样式表文件,比较麻烦
<script setup lang="ts">
interface Props {
squareContent: string | null
}
defineProps<Props>()
defineEmits(['fill-square'])
</script>
<template>
<div class="square">
{{ squareContent }}
</div>
</template>
<style scoped lang="scss">
.square {
display: flex;
justify-content: center;
align-items: center;
font-size: 64px;
width: 100px;
height: 100px;
background-color: $square-background-color;
cursor: pointer;
border: 1px solid $text-color;
}
</style>
Board组件
和React
不一样的是,Vue
中我们使用的是模板语法,可以声明式地渲染元素,而不用想React
那用写一个渲染方法去渲染棋盘中每一行,可以直接使用v-for
去渲染
当然,这并不是说React
的写法就不好,只是说简单场景下,声明式语法的开发体验更好,但复杂场景下,React
的方式更加灵活,不过Vue
当中也是可以写tsx
和渲染函数的,所以也算弥补了哈哈
<script setup lang="ts">
import type { Props as SquareProps } from './Square.vue'
import Square from './Square.vue'
// 棋盘中的所有格子类型
export type Squares = SquareProps[]
interface Props {
squares: Squares
boardInfo: string
shape: [number, number]
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'fill-square', squareIdx: number): void
}>()
const handleFillSquare = (squareIdx: number) => {
emit('fill-square', squareIdx)
}
// 根据行索引和列索引计算格子的下标作为格子 id
const getSquareId = (rowIdx: number, colIdx: number): number => {
return rowIdx * props.shape[1] + colIdx
}
</script>
<template>
<div class="board">
<div class="board-info">
<span class="info">{{ boardInfo }}</span>
</div>
<div class="board-container">
<div v-for="(_, rowIdx) in shape[0]" :key="rowIdx" class="board-row">
<Square
v-for="(_, colIdx) in shape[1]"
:key="colIdx"
:square-content="squares[getSquareId(rowIdx, colIdx)].squareContent"
@fill-square="handleFillSquare(getSquareId(rowIdx, colIdx))" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.board {
display: flex;
flex-direction: column;
align-items: center;
gap: $info-gap;
.board-container {
display: flex;
flex-direction: column;
.board-row {
display: flex;
}
}
}
</style>
Game组件
<script setup lang="ts">
import { computed, reactive, ref, shallowReactive, toRefs, watch } from 'vue'
import Board, { Squares } from './Board.vue'
import Divider from './Divider.vue'
import History from './History.vue'
import type { Props as BoardProps } from './Board.vue'
type BoardPropsFiltered = Omit<BoardProps, 'shape'>
type HistoryType = BoardPropsFiltered[]
type Player = 'X' | 'O'
interface State {
history: HistoryType
currentHistoryIdx: number
nextPlayer: Player
}
const shape: [number, number] = [3, 3]
// 初始状态
const state = shallowReactive<State>({
history: [
{
squares: new Array(shape[0] * shape[1]).fill(0).map(() => ({
squareContent: null,
})),
boardInfo: `Next player is X`,
},
],
currentHistoryIdx: 0,
nextPlayer: 'X',
})
// 将状态解构为 ref
const { history, currentHistoryIdx, nextPlayer } = toRefs(state)
const currentHistory = computed(() => history.value[currentHistoryIdx.value])
// 落棋
const handleFillSquare = (squareIdx: number) => {
let winner = calcWinner(currentHistory.value.squares)
if (
winner ||
currentHistory.value.squares[squareIdx].squareContent !== null
) {
return
}
// 落棋
const clonedSquares = currentHistory.value.squares.slice(0)
clonedSquares[squareIdx].squareContent = nextPlayer.value
// 落棋之后产生赢家 -- 修改对局信息
winner = calcWinner(currentHistory.value.squares)
let newBoardInfo = `Next player is ${nextPlayer.value}`
if (winner) {
newBoardInfo = `Winner is ${winner}`
} else {
// 更新 nextPlayer
nextPlayer.value = (currentHistoryIdx.value + 1) % 2 === 0 ? 'X' : 'O'
// 计算新的棋盘对局信息
newBoardInfo = `Next player is ${nextPlayer.value}`
}
// 更新历史记录
history.value = history.value.concat([
{
squares: clonedSquares,
boardInfo: newBoardInfo,
},
])
// 更新 currentHistoryIdx
currentHistoryIdx.value = history.value.length - 1
}
// 历史记录点击跳转
const handleJumpTo = (historyIdx: number) => {
currentHistoryIdx.value = historyIdx
}
const calcWinner = (squares: Squares): Player | null => {
// 赢的时候的棋局情况
const winnerCase = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
]
for (let i = 0; i < winnerCase.length; i++) {
const [a, b, c] = winnerCase[i]
const contentA = squares[a].squareContent
const contentB = squares[b].squareContent
const contentC = squares[c].squareContent
if (contentA && contentA === contentB && contentA === contentC) {
return contentA as Player
}
}
return null
}
</script>
<template>
<div class="game">
<Board
:shape="shape"
:squares="currentHistory.squares"
:board-info="currentHistory.boardInfo"
@fill-square="squareIdx => handleFillSquare(squareIdx)" />
<Divider />
<History
:history="history"
@jump-to="historyIdx => handleJumpTo(historyIdx)" />
</div>
</template>
<style scoped lang="scss">
.game {
display: flex;
gap: $divider-gap;
}
</style>
转载自:https://juejin.cn/post/7128377343105564709