【react】官方tutorial简单实现的井字过三关+拓展题

545 阅读4分钟

简单归纳一下,基本上都是按着教程写的代码。

  1. this.state 用来存储本地数据,等价于 Vue 中的 data
  2. this.props 用来使用接收到的参数,等价于 Vue 中的 props
  3. render() 类似 Vue 中较少用到的 Render 函数,使用时记得自己写的是 JS 代码而不是 HTML 模板。条件控制、循环、分支等写法都只需要写 JS 代码(用大括号包起来)即可。

拓展题:

  1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。 直接使用一个getStepPosition的转换函数实现。
  1. 在历史记录列表中加粗显示当前选择的项目。

增加一个判断语句,如果 move 等于 this.state.stepNumber 就加上一个内联样式style={{fontWeight: "bold"}},关键点:双大括号(外层表示 JS 代码,内层表示 JS 对象),驼峰命名法(因为是 JS 中命名,底层会转换成 CSS 样式的)

  1. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。

在 Board 中 state 定义一个底数 radix 大小取3,然后用this.state.radix.map(cb)来实现循环和渲染出对应的内容。

  1. 添加一个可以升序或降序显示历史记录的按钮。

添加一个按钮和一个判断是否顺序排序的 state 取名为 asc。

  1. 每当有人获胜时,高亮显示连成一线的 3 颗棋子。

新增一个内部变量 hightlights 用来保存当前出现 winner 时,该胜利序列的三个 square 的位置,通过父到子的通信传输方向 Game -> Board -> Square,最后在 Square 上新增一个类用来添加样式。

  1. 当无人获胜时,显示一个平局的消息。

增加一个判断,满足就修改输出的消息 status 即可。

附录:代码文件

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Square extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: null,
        }
    }
    render() {
      if(this.props.highlights && this.props.highlights.includes(this.props.i)) {
        return (
          <button 
              className="square highlights" 
              onClick={ () => {this.props.onClick() }}>
            { this.props.value }
          </button>
        )
      }
      return (
        <button 
            className="square" 
            onClick={ () => {this.props.onClick() }}>
          { this.props.value }
        </button>
      );
    }
  }

  /** 将上面的 Square 组件类重写为 函数组件 */
  // eslint-disable-next-line
  function SquareFun(props) {
      return (
          <button className="square" onClick={props.onClick}>
              {props.value}
          </button>
      )
  }
  
  class Board extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        radix: Array(3).fill(null),
      }
    }
    renderSquare(i) {
        return <Square 
            value={this.props.squares[i]}
            onClick={()=> this.props.onClick(i)}
            key={i}
            highlights={this.props.highlights}
            i={i}
            />;
    }

    // 3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
    renderRow(i) {
      return <div className="border-row" key={i}>
        { 
          this.state.radix.map((square, index) => this.renderSquare(i * 3 + index)) 
        }
      </div>
    }
    render() {
      return (
        <div>
          {
            this.state.radix.map((row, index) => this.renderRow(index))
          }
        </div>
      );
    }
  }
  
  class Game extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        history: [{
          squares: Array(9).fill(null),
          stepPosition: [null, null],
        }],
        asc: true,
        stepNumber: 0,
        xIsNext: true,
      }
    }
    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,
            stepPosition: getStepPostion(i),
          }]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext,
      });
    }
    jumpTo(step) {
      this.setState({
        stepNumber: step,
        xIsNext: (step % 2) === 0,
      })
    }
    render() {
      const history = this.state.history;
      const current = history[this.state.stepNumber];
      const winner = calculateWinner(current.squares);
      const moves = history.map((step, move, arr) => {
        move = this.state.asc ? move : history.length - move - 1;
        const desc = move ?
          'Go to move #' + move + ' at (' + arr[move].stepPosition[0] + ', ' + arr[move].stepPosition[1] + ')':
          'Go to game start';
        
        if(move === this.state.stepNumber) {
          return (
            <li key={move}>
              {/* 3. 加粗显示 */}
              <button className="histBtn" style={{fontWeight: "bold"}} onClick={() => {
                this.jumpTo(move);
                }}>{desc}</button>
            </li>
          )
        }
        return (
          <li key={move}>
            <button className="histBtn" onClick={() => {
              this.jumpTo(move);
              }}>{desc}</button>
          </li>
        )
      })

      let status;
      let highlights;
      if(winner) {
          status = 'Winner is ' + winner[0];
          highlights = winner[1];
      } else {
          status = 'Next player is ' + (this.state.xIsNext ? 'X' : 'O');
      }
      if(!winner && this.state.stepNumber === 9) {
        status = 'Draw'
      }
      return (
        <div className="game">
          <div className="game-board">
            <Board
              squares={current.squares}
              onClick={(i) => this.handleClick(i)}
              highlights={highlights} />
          </div>
          <div className="game-info">
            <div className="status">{status}</div>
            <div className="btns">
              {/* 4. 添加一个可以升序或降序显示历史记录的按钮。 */}
              <button className="btn" onClick={() => this.setState({asc: !this.state.asc})}>Reverse</button>
              <button className="btn" onClick={() => this.state.stepNumber - 1 >= 0 && this.jumpTo(this.state.stepNumber - 1)}>Step back</button>
            </div>
            <ol>{moves}</ol>
          </div>
        </div>
      );
    }
  }
  
  // ========================================
  
  ReactDOM.render(
    <Game />,
    document.getElementById('root')
  );  
  
  // 1.在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
  function getStepPostion(i) {
    const position = [
      [1, 1],
      [2, 1],
      [3, 1],
      [1, 2],
      [2, 2],
      [3, 2],
      [1, 3],
      [2, 3],
      [3, 3]
    ];
    return position[i];
  }
  function calculateWinner(squares) {
    const lines = [
      [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 < lines.length; i++) {
      const [a, b, c] = lines[i];
      if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
        return [squares[a], [a, b, c]];
      }
    }
    return null;
  }

index.css

body {
    font: 14px "Century Gothic", Futura, sans-serif;
    margin: 20px;
  }
  
  ol, ul {
    padding-left: 30px;
  }
  
  .board-row:after {
    clear: both;
    content: "";
    display: table;
  }
  
  .status {
    margin-bottom: 10px;
  }
  
  .square {
    background: #fff;
    border: 1px solid #999;
    float: left;
    font-size: 24px;
    font-weight: bold;
    line-height: 34px;
    height: 34px;
    margin-right: -1px;
    margin-top: -1px;
    padding: 0;
    text-align: center;
    width: 34px;
  }
  
  .square:focus {
    outline: none;
  }

  .square.highlights {
    outline: none;
    color: aqua;
  }
  
  .btns {
    display: flex;
    width: 160px;
    line-height: 90px;
  }

  .btns .btn {
    margin: 10px 5px;
    font-family: Arial, Helvetica, sans-serif;
    width: 70px;
    height: 50px;
    border-radius: 12px;
  }
  .btns .btn:nth-child(2) {
    background-color: greenyellow;
  }

  .kbd-navigation .square:focus {
    background: #ddd;
  }
  
  .game {
    display: flex;
    flex-direction: row;
  }
  
  .game-info {
    display: flex;
    width: 220px;
    flex-wrap: wrap;
    margin-left: 20px;
  }

  .status {
    width: 220px;
  }

  .histBtn {
    border: none;
    margin: 4px auto;
  }