react学写棋类游戏

安装

使用Create React App,最好的方式,但是必须node>6的版本
全局安装

npm install -g create-react-app
create-react-app my-app
cd my-app
npm start
  1. 删除掉生成项目中 src/ 文件夹下的所有文件。

  2. src/ 文件夹下新建一个名为 index.css 的文件并拷贝 这里的 CSS 代码 到文件中。

  3. src/ 文件夹下新建一个名为 index.js 的文件并拷贝 这里的 JS 代码 到文件中, 并在此文件的最开头加上下面几行代码:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

接下来通过命令行在你的项目目录下运行 npm start 命令并在浏览器中打开 http://localhost:3000 你就能够看到空的井字棋棋盘了

开始编码


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';


class Square extends React.Component {
    render() {
        return (
            
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return ;
    }

    render() {
        const status = 'Next player: X';

        return (
            
{status}
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); } } class Game extends React.Component { render() { return (
{/* status */}
    {/* TODO */}
); } } // ======================================== ReactDOM.render( , document.getElementById('root') );

讲得更具体一点,我们现在有3个组件:

Square
Board
Game
Square 组件代表一个单独的 ); } }

react学写棋类游戏_第1张图片
image.png

给组件添加交互功能

接下来我们试着让棋盘的每一个格子在点击之后能落下一颗 “X” 作为棋子。我们试着把 render() 方法修改为如下内容:
现在你试着点击一下某个格子,在浏览器里就会弹出一个警示框。
在 React 组件的构造方法 constructor 当中,你可以通过 this.state 为该组件设置自身的状态数据。我们来试着把棋盘格子变化的数据储存在组件的 state 当中吧:

首先,我们为组件添加构造函数并初始化 state:

lass Square extends React.Component {
    constructor(){
        super()
        this.state = {
            value:null,
        }
    },
    render() {
        return (
            
        );
    }
}

现在我们试着通过点击事件触发 state 的改变来更新棋盘格子显示的内容:

); } }

每当 this.setState 方法被触发时,组件都会开始准备更新,React 通过比较状态的变化来更新组件当中跟随数据改变了的内容。当组件重新渲染时,this.state.value 会变成 'X' ,所以你也就能够在格子里看到 X 的字样。

现在你试着点击任何一个格子,都能够看到 X 出现在格子当中。

开发工具

在 Chrome 或 Firefox 上安装 React 开发者工具可以让你在浏览器的开发控制台里看到 React 渲染出来的组件树。
你同样可以在开发工具中观察到各个组件的 props 和 state.
安装好开发工具之后,你可以在任意页面元素上面右键选择 “审查元素”,之后在弹出的控制台选项卡最右边会看到名为 React 的选项卡。

状态提升

我们现在已经编写好了井字棋游戏最基本的可以落子的棋盘。但是现在应用的状态是独立保存在棋盘上每个格子的 Square 组件当中的。想要编写出来一个真正能玩的游戏,我们还需要判断哪个玩家获胜,并在 X 或 O 两方之间交替落子。想要检查某个玩家是否获胜,需要获取所有9个格子上面的棋子分布的数据,现在这些数据分散在各个格子当中显然是很麻烦的。
最好的解决方式是直接将所有的 state 状态数据存储在 Board 组件当中。之后 Board 组件可以将这些数据传递给各个 Square 组件
当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中的状态数据就能够更方便地交流共享了
像这种提升组件状态的情形在重构 React 组件时经常会遇到。我们趁现在也就来实践一下,在 Board 组件的构造函数中初始化一个包含9个空值的数组作为状态数据,并将这个数组中的9个元素分别传递到对应的9个 Square 组件当中。

 constructor(){
        super()
        this.state = {
            squartes: Array(9).fill(null)
        }
    }

现在传入的都是空数据,井字棋游戏进行会把数组填充成类似下面这样:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

我们在 value 属性中传递对应 state 数组元素的值。

    renderSquare(i) {
        return ;
    }

现在我们需要修改当某个格子被点击时触发的事件处理函数。现在每个格子当中的数据是存储在整个棋盘当中的,所以我们就需要通过一些方法,让格子组件能够修改整个棋盘组件数据的内容。因为每个组件的 state 都是它私有的,所以我们不可以直接在格子组件当中进行修改。

惯例的做法是,我们再通过 props 传递一个父组件当中的事件处理函数到子组件当中。也就是从 Board 组件里传递一个事件处理函数到 Square 当中,我们来把 renderSquare 方法改成下面这样


    renderSquare(i) {
        return this.handleClick(i)}
                />;
    }

注意到我们在写代码的时候,在各个属性直接换了行,这样可以改善我们代码的可读性。并且我们在 JSX 元素的最外层套上了一小括号,以防止 JavaScript 代码在解析时自动在换行处添加分号。

现在我们从 Board 组件向 Square 组件中传递两个 props 参数:value 和 onClick. onClick 里传递的是一个之后在 Square 组件中能够触发的方法函数。我们动手来修改代码吧:

将 Square 组件的 render 方法中的 this.state.value 替换为 this.props.value 。
将 Square 组件的 render 方法中的 this.setState() 替换为 this.props.onClick() 。
删掉 Square 组件中的 构造函数 constructor ,因为它现在已经不需要保存 state 了。
进行如上修改之后,代码会变成下面这样:

class Square extends React.Component {
  render() {
    return (
      
    );
  }
}

现在每次格子被点击时就会触发传入的 onClick 方法。我们来捋一下这其中发生了什么:

1.添加 onClick 属性到内置的 DOM 元素 ); }

记得把所有的 this.props 替换成参数 props. 我们应用中的大部分简单组件都可以通过函数定义的方式来编写,并且 React 在将来还会对函数定义组件做出更多优化。

另外一部分简化的内容则是事件处理函数的写法,这里我们把 onClick={() => props.onClick()} 直接修改为 onClick={props.onClick} , 注意不能写成 onClick={props.onClick()} 否则 props.onClick 方法会在 Square 组件渲染时被直接触发而不是等到 Board 组件渲染完成时通过点击触发,又因为此时 Board 组件正在渲染中(即 Board 组件的 render() 方法正在调用),又触发 handleClick(i) 方法调用 setState() 会再次调用 render() 方法导致死循环。

轮流落子

很明显现在我们点击棋盘只后落子的只有 X 。 下面我们要开发出 X 和 O 轮流落子的功能。

我们将 X 默认设置为先手棋:

class Board extends React.Component {
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

接下来,我们每走一步棋,都需要切换 xIsNext 的值以此来实现轮流落子的功能,接下来在 handleClick 方法中添加修改 xIsNext 的语句

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

到这里我们就实现了 X 和 O 轮流落子的效果了。我们再到 render 方法里添加一点内容来显示当前执子的一方

render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      // the rest has not changed

判断赢家

接下来我们来编写判断游戏获胜方的代码,首先在你的代码里添加下面这个判断获胜方的算法函数

 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];
    }
  }
  return null;
}

然后你就可以在 Board 组件的 render 方法里调用它,来检查是否有人获胜并根据判断显示出 “Winner: [X/O]” 来表示获胜方。

将 render 中的 status 替换为如下内容:

render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // the rest has not changed

继续完善游戏规则,我们在 handleClick 里添加当前方格内已经落子/有一方获胜就就无法继续落子的判断逻辑

 handleClick(i) {
       const squares = this.state.squares.slice()
        if(this.calculateWinner(squares) || squares[i]) return
        squares[i] = this.state.xIsNext ? 'X' : 'O'
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext,
        })
    }

保存历史记录

接下来我们一起来实现保存棋局每一步的历史记录的功能。在现有的代码逻辑中,我们已经是在每走一步棋之后就返回一个新的 squares 数组了,所以想要保存历史记录也非常简单。

我们计划通过一个数组对象来保存每一步的状态数据

我们期望在顶层的 Game 组件中展示一个链接每一步历史记录的列表。所以就像我们之前将 state 从 Square 组件提升到 Board 中一样,现在我们把 Board 中的状态数据再提升到 Game 组件中来。

首先在 Game 组件的构造函数中初始化我们需要的状态数据

class Game extends React.Component {
    constructor(){
        super()
        this.state = {
            history: [{
                squares: Array(9).fill(null),
            }],
            xIsNext: true,
        };
    }

    render() {
        return (
            
{/* status */}
    {/* TODO */}
); } }

接下来,就好像我们之前对 Square 组件的操作一样。我们将 Board 中的状态数据全都移动到 Game 组件当中。Board 现在通过 props 获取从 Game 传递下来的数据和事件处理函数。

1.删除 Board 的构造方法 constructor 。
2.把 Board 的 renderSquare 方法中的 this.state.squares[i] 替换为 this.props.squares[i] 。
3.把 Board 的 renderSquare 方法中的 this.handleClick(i) 替换为 this.props.onClick(i) 。

将calculateWinner提到外面公用的

Game 组件的 render 方法现在则要负责获取最近一步的历史记录(当前棋局状态),以及计算出游戏进行的状态(是否有人获胜)。

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      
this.handleClick(i)} />
{status}
    {/* TODO */}
); }

既然现在由 Game 组件负责渲染游戏状态,我们可以直接把 Board 组件的 render 方法里的

{status}
删掉:

  render() {
    return (
      
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); }

之后,我们需要将 Board 组件里的 handleClick 移动到 Game 组件当中。你可以直接把它剪切粘贴过来。

不过为了实现我们新的历史记录的功能,还需要稍微修改一下我们的代码,让 handleClick 在每次触发时,添加当前的棋局状态数据到 histroy 当中

 handleClick(i) {
    const history = this.state.history;
    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
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

代码编写到这一步,Board 组件当中现在应该只有 renderSquare 和 render 两个方法;应用状态 state 以及事件处理函数现在都定义在 Game 组件当中。
完成代码


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';


function Square(props) {
    return (
        
    )
}

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];
        }
    }
    return null;
}

class Board extends React.Component {

    renderSquare(i) {
        return this.props.onClick(i)}
                />;
    }

    render() {
        return (
            
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); } } class Game extends React.Component { constructor(props){ super(props) this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; } handleClick(i) { const history = this.state.history 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 }]), xIsNext: !this.state.xIsNext }) } render() { const history = this.state.history const current = history[history.length - 1] const winner = calculateWinner(current.squares) let status if(winner) { status = 'Winner: ' + winner }else { status = 'Nest player: ' + (this.state.xIsNext ? 'X' : 'O') } return (
this.handleClick(i)} />
{ status }
    {/* TODO */}
); } } ReactDOM.render( , document.getElementById('root') );

展示每步历史记录链接

现在我们来试着展示每一步棋的历史记录链接。在教程的开始我们提到过,React 元素事实上都是 JS 当中的对象,我们可以把元素当作参数或定义到变量中使用。在 React 当中渲染多个重复的项目时,我们一般都以数组的方式传递 React 元素。最基本的方法是使用数组的 map 方法,我们试着来修改 Game 组件的 render 方法吧:

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Move #' + move :
        'Game start';
      return (
        
  • this.jumpTo(move)}>{desc}
  • ); }); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return (
    this.handleClick(i)} />
    {status}
      {moves}
    ); }

    对于每一步的历史记录,我们都创建了一个带 链接的

  • 列表项。目前链接还没指向任何地方,别着急我们后面会继续实现切换至对应棋步的功能。有了我们现有的代码,已经能渲染出一个列表了,不过你留心的话,就会在控制台看到警告:

    Keys

    当你在 React 当中渲染列表项时,React 都会试着存储对应每个单独项的相关信息。如果你的组件包含 state 状态数据,那么这些状态数据必须被排序,不管你组件是怎么编写实现的。

    当你想要更新这些列表项时,React 必须能够知道是那一项改变了。这样你才能够在列表中增删改查项目。

    比方说下面这个例子,从前一个表单

  • Alexa: 7 tasks left
  • Ben: 5 tasks left
  • 变成下面这个表单

  • Ben: 9 tasks left
  • Claudia: 8 tasks left
  • Alexa: 5 tasks left
  • 你用肉眼可以很轻易地分辨,Alexa 被移到了最后,多出来一个 Claudia。可是 React 只是电脑里运行地程序,它无从知晓这些改变。所以我们必须为列表中的每一项添加一个 key 作为唯一的标识符。标识符必须是唯一的,比方说刚才这个例子中的 alexa, ben, claudia 就可以用来做标识符。更普遍的一种情况,假如我们的数据是从数据库获取的话,表单每一项的 ID 就很适合当作它的 key :

  • {user.name}: {user.taskCount} tasks left
  • 强烈建议你在渲染列表项时添加 keys 值。 假如你没有现成可以作为唯一 key 值的数据使用的话,你可能需要考虑重新组织设计你的数据了。

    实现时间旅行

    在我们的棋步的列表中,已经有了现成的唯一 key 值,也就是每一次 move 的记录值。我们通过

  • 来添加一下。

     const moves = history.map((step, move) => {
          const desc = move ?
            'Move #' + move :
            'Game start';
          return (
            
  • this.jumpTo(move)}>{desc}
  • ); });

    在上面的代码中,我们同样为每一个 添加了一个 jumpTo 方法,用来将棋盘的状态切换至对应的棋步时的状态。接下来我们来着手实现这个方法:

    首先在 Game 组件的初始状态中多设置一项 stepNumber: 0

    class Game extends React.Component {
      constructor() {
        super();
        this.state = {
          history: [{
            squares: Array(9).fill(null),
          }],
          stepNumber: 0,
          xIsNext: true,
        };
      }
    

    接下来,我们正是编写 jumpTo 来切换 stepNumber 的值。根据游戏的逻辑,与此同时我们还需要修改 xIsNext 来保证对应棋步时,执子的一方是能对应上的。我们可以根据棋步计算出是谁在执子。

    我们把 jumpTo 编写在 Game 组件中:

      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: (step % 2) ? false : true,
        });
      }
    

    接下来,我们在 handleClick 方法中对 stepNumber 进行更新,添加 stepNumber: history.length 保证每走一步 stepNumber 会跟着改变:

     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
          }]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext,
        });
      }
    

    现在你可以直接在 Game 组件的 render 方法里根据当前的棋步获取对应的棋局状态了:

    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);
    

    查看此步完整代码示例。

    现在你试着点击每一步棋记录的列表中的一项,棋盘会自动更新到对应项时的棋局状态

    总结,招帮官网文档来的,算是自我学习的一种方式

  • 你可能感兴趣的:(react学写棋类游戏)