likes
comments
collection
share

😀Vue3实现React官方示例的井字棋游戏

作者站长头像
站长
· 阅读数 12

昨天用React写了官方示例的井字棋游戏,今天我们来用Vue3重新把它实现一次,感受一下VueReact的开发体验上的区别

今天就直接上手开发了,主要会讲一下相同的概念在两个框架之间的不同点

Square组件

首先是最简单的棋盘格子组件,它接收一个squareContent属性,并且有一个自定义事件fill-square,用于通知父组件往格子中落棋,相较于ReactVuedefineEmits,可以让我们很方便地实现自定义事件

当然,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
评论
请登录