React 官方教程井字棋小游戏的后续功能

546 阅读3分钟

React 官方教程: zh-hans.reactjs.org/tutorial/tu…

github地址:github.com/ShigureRain…

预览地址:shigurerain.github.io/react-tic-t…

使用 React 实现井字棋小游戏,前面步骤可以查看 React 官方教程,这里实现最后的改进

  1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
  2. 在历史记录列表中加粗显示当前选择的项目。
  3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
  4. 添加一个可以升序或降序显示历史记录的按钮。
  5. 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
  6. 当无人获胜时,显示一个平局的消息。

记录坐标

分析需求:

  • 我们需要一个 坐标hash 来记录每一次点击的坐标
  • 既然要回退历史记录,我们仍然需要 history 来记录我们所做的每一步操作

获取坐标

声明一个查找坐标函数,作用是传进点击的格子参数,获取它的位置

const coordinate = (i) => {
  const findCoordinate = [
    [1, 1],
    [1, 2],
    [1, 3],
    [2, 1],
    [2, 2],
    [2, 3],
    [3, 1],
    [3, 2],
    [3, 3],
  ]
  return findCoordinate[i]
}

history 添加记忆数据

  • 在 Game 组件的构造函数内添加 history 的参数
  constructor(props) {
    super(props)
    this.state = {
      history: [{
        squares: Array(9).fill(null),
        coordinates: Array(2).fill(null),  //坐标参数
        xIsNext: true  //用于展示这一步是谁下的
      }],
      stepNumber: 0,
      xIsNext: true
    }
  }
  • click 事件的 history 参数
 handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1)
    const current = history[history.length - 1]
    const squares = current.squares.slice()

    if (calculateWinner(squares) || squares[i]) {return}
    squares[i] = this.state.xIsNext ? 'X' : 'O'
    this.setState({
      history: history.concat([{
        squares: squares,
        coordinates: coordinate(i),  //记录点击时的坐标
        xIsNext: this.state.xIsNext,  //记录这一步的棋子
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    })
  }

渲染页面

render(){
  ...
   const moves = history.map((step, move) => {
      const desc = move ?
        '#' + move + '\n' + (step.xIsNext ? 'X' : 'O') + '\n' + step.coordinates :
        'Go to game start'
      return (
        <li key={move}>
          <button onClick={() => {this.jumpTo(move)}}>{desc}</button>
        </li>
      )
    })
  ...
}

加粗当前选择

分析需求:遍历history,找到步骤的一项添加className

Class 组件的 render 函数

 render() {
 	...
    const moves = history.map((step, move) => {
	...
      return (
        <li key={move}>
          //添加了className
          <button className={move === this.state.stepNumber ? 'current-step' : ''}
                  onClick={() => {this.jumpTo(move)}}>{desc}</button>
        </li>
      )
    })
	...
}

循环渲染棋子

分析需求:使用数组的 map 方法来做出双重循环

const Board = (props) => {
...

  const row = [1, 2, 3]
  let column = 0

  return (
    <div>
      {
        row.map((key) => {
          return (
            <div key={key} className="board-row">
              {
                row.map(() => {
                  return renderSquare(column++)
                })
              }
            </div>
          )
        })
      }
    </div>
  )
}

可升序降序显示历史记录

分析需求:

  • 添加一个点击按钮,可反转 history 数据
  • 识别列表是否为反转状态,用于点击棋盘时数据改变逻辑
  • stepNumber 需要重新计算为之前点击的一步

sort按钮

...
sort() {
    this.setState({
      history: this.state.history.slice().reverse(),
      reverse: !this.state.reverse,
      stepNumber: this.state.history.length - this.state.stepNumber - 1
    })
  }
...
render(){
  ...
    return (
      ...
        <div className="game-info">
          <div>{status}</div>
          <button onClick={() => {this.sort()}}>Reverse</button>
          <ol>{moves}</ol>
        </div>
      ...
    )
  ...
}

识别是否反转状态,改变操作逻辑

  1. 在构造函数添加 reverse
  constructor(props) {
    super(props)
    this.state = {
			...
      reverse: false
    }
  }
  1. 根据是否反转,处理点击事件,反转时:
  • 需要从头部插入数据
  • 插入最新的 stepNumber 为第一条
  • 点击后的 history 记录从上开始删除
  • 当前的 stepNumber 需要重新计算
  handleClick(i) {
    const history = this.state.reverse ? this.state.history.slice(this.state.stepNumber) :
      this.state.history.slice(0, this.state.stepNumber + 1)  //删除点击的那一项以外多余的记录
    const current = this.state.reverse ? history[0] : history[history.length - 1]
    const squares = current.squares.slice()

    if (calculateWinner(squares) || squares[i]) {return}
    squares[i] = this.state.xIsNext ? 'X' : 'O'

    if (this.state.reverse) {  //如果是反转模式,需要从头部插入数据
      this.setState({
        history: ([{
          squares: squares,
          coordinates: coordinate(i),
          xIsNext: this.state.xIsNext,
        }]).concat(history),
      })
    } else {
      this.setState({
        history: history.concat([{
          squares: squares,
          coordinates: coordinate(i),
          xIsNext: this.state.xIsNext,
        }]),
      })
    }

    this.setState({
      stepNumber: this.state.reverse ? 0 : history.length,  //插入最新的 stepNumber 为第一条
      xIsNext: !this.state.xIsNext
    })
  }

render(){
  ...
    const moves = history.map((step, move) => {
    	//
      const nowMove = this.state.reverse
        ? history.length - move - 1 : move  //当前的 stepNumber 需要重新计算

      const desc = nowMove ?
        '#' + nowMove + '\n' + (step.xIsNext ? 'X' : 'O') + '\n' + step.coordinates :
        'Go to game start'
      //

      return (
       ...
      )
    })
  ...
}

高亮显示获胜的棋子

分析需求:找到获胜棋子的位置改变 className

  • 先添加css样式
.highlight-winner {
  color: red;
}
  • 修改一下计算获胜棋子的方法,改为返回坐标
const calculateWinner = (squares) => {
...
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return lines[i]  //返回坐标
    }
  }
  return null
}
  • 在 Game 组件的 render 函数内获取坐标,并传参给 Board
render(){
    let winner
    if (calculateWinner(current.squares)) { //这里替换之前的winner参数
      winner = current.squares[calculateWinner(current.squares)[0]]
    }
    const winnerCoordinate = calculateWinner(current.squares)
  
renturn
  ...
   <Board winner={winnerCoordinate} squares={current.squares} onClick={(i) => {this.handleClick(i)}}/>
	...
}
  • 在 Board 组件传参给 Square
  const renderSquare = (i) => {
    const className = props.winner ? props.winner.includes(i) ? 'highlight-winner' : '' : ''

    return (
      <Square
        key={i}
        winnerClass={className}
        value={props.squares[i]}
        onclick={() => {props.onClick(i)}}/>
    )
  }
  • Square 组件中使用 className
    <button className={`square ${props.winnerClass}`} onClick={props.onclick}>
      {props.value}
    </button>

无人获胜时,显示平局

分析需求:当棋盘满的时候,winner也为空时,展示平局消息

  • 可以添加一个加粗样式
.font-bold{
  font-weight: bold;
}
  • Game 的 render 函数内做平局的条件判断
    let status
    let classDraw
    if (winner) {
      status = 'winner: ' + winner
    } else {
      if (this.state.history.length === 10 && this.state.reverse ? this.state.stepNumber === 0 : this.state.stepNumber === 9) {
        status = 'Game draw!'
        classDraw = 'font-bold'
      } else {
        status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O')
      }
    }

...
   <div className={classDraw}>{status}</div>