React--井字棋小游戏

安装较新版本的node.js,这里使用的是v15.0.1,用以记录React学习笔记。

1 搭建本地开发环境

在想要创建项目的文件夹下输入cmd
在这里插入图片描述
在这里插入图片描述
回车
在这里插入图片描述
输入命令npx create-react-app my-app等待项目初始化
在这里插入图片描述
等待一段时间,等待时间长短跟网络有关。
出现以下页面本地环境搭建就成功了。
在这里插入图片描述
这只是本地的git仓库,但是在开发中肯定是要团队合作的,所以这里我关联一下远程仓库
首先在GitHub上新建一个仓库
然后拷贝仓库地址 在这里插入图片描述
在新搭建好的本地环境文件夹中右键打开git bash
在这里插入图片描述
执行git remote add origin 你的仓库地址
再执行git push -u origin master将本地仓库推送到GitHub仓库的origin/master分支
在这里插入图片描述
在GitHub master分支上就可以看到本地代码了。
在这里插入图片描述

2 游戏开发

2.1 初始化

删除src目录里的所有文件,
新建index.css index.js
拷贝初始化代码
index.css

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;
  }
  
  .kbd-navigation .square:focus {
    background: #ddd;
  }
  
  .game {
    display: flex;
    flex-direction: row;
  }
  
  .game-info {
    margin-left: 20px;
  }

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Square extends React.Component {
    render() {
      return (
        <button className="square">
          {/* TODO */}
        </button>
      );
    }
  }
  
  class Board extends React.Component {
    renderSquare(i) {
      return <Square />;
    }
  
    render() {
      const status = 'Next player: X';
  
      return (
        <div>
          <div className="status">{status}</div>
          <div className="board-row">
            {this.renderSquare(0)}
            {this.renderSquare(1)}
            {this.renderSquare(2)}
          </div>
          <div className="board-row">
            {this.renderSquare(3)}
            {this.renderSquare(4)}
            {this.renderSquare(5)}
          </div>
          <div className="board-row">
            {this.renderSquare(6)}
            {this.renderSquare(7)}
            {this.renderSquare(8)}
          </div>
        </div>
      );
    }
  }
  
  class Game extends React.Component {
    render() {
      return (
        <div className="game">
          <div className="game-board">
            <Board />
          </div>
          <div className="game-info">
            <div>{/* status */}</div>
            <ol>{/* TODO */}</ol>
          </div>
        </div>
      );
    }
  }
  
  // ========================================
  
  ReactDOM.render(
    <Game />,
    document.getElementById('root')
  );

这里使用的vscode,快捷键ctrl + ~打开终端,执行npm start
在这里插入图片描述
在这里插入图片描述

2.2 使用props传递数据

在Board中的renderSquare方法中给Square组件添加属性value={i}

renderSquare(i) {
      return <Square value={i}/>;
    }

Square组件中render方法button值改为{this.props.value}

render() {
      return (
        <button className="square">
          {this.props.value}
        </button>
      );
    }

效果为:
在这里插入图片描述

2.3 state实现交互功能

Square组件添加构造方法

    constructor(props) {
        super(props);
        this.state = {
            value: null,
        };
    }

render方法中onClick设置state

render() {
      return (
        <button className="square" onClick={() => {
            this.setState({
                value: 'X'
            })
        }}>
          {this.state.value}
        </button>
      );
    }

效果:点击之后出现’X’
在这里插入图片描述

2.3 状态提升

当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。
将Square中初始化提升到Board中
Board中写构造方法constructor,并初始化

    constructor(props) {
        super(props);
        this.state = {
            squares: Array(9).fill(null),
        };
    }

组件Square中,render方法

    render() {
        return (
            <button className="square"
                onClick={() => this.props.onClick()}
            >
                {this.props.value}
            </button>
        );
    }

组件BoardrenderSquare

    renderSquare(i) {
        return <Square
            value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />;
    }

handleClick方法.slice() 方法创建了数组的一个副本,而不是直接修改现有的数组:

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

这里Square组件只有一个render函数,那么完全可以将其重写为一个函数组件(如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写)

function Square(props) {
    return (
        <button className="square"
            onClick={() => props.onClick()}
        >
            {props.value}
        </button>
    )
}

2.4 轮流落子

设置’X’为首先落子方,state中定义一个布尔变量,每次点击设置该变量反转

    constructor(props) {
        super(props);
        this.state = {
            squares: Array(9).fill(null),
            iXNext: true,
        };
    }

点击

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

2.5 决出胜者

定义胜利时棋盘可能存在的情况
判断三个棋子连成线即为胜利

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [0, 4, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 4, 6],
        [2, 5, 8],
        [3, 4, 5],
        [6, 7, 8]
    ];
    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中调用该方法

render() {
        const winner = calculateWinner(this.state.squares);
        let status;
        if (winner) {
            status = "Winner: " + winner;
        } else {
            status = 'Next player: ' + (this.state.iXNext ? 'X' : 'O');
        }
        // const status = 'Next player: ' + (this.state.iXNext ? 'X' : 'O');
        return (
            <div>
                <div className="status">{status}</div>
                <div className="board-row">
                    {this.renderSquare(0)}
                    {this.renderSquare(1)}
                    {this.renderSquare(2)}
                </div>
                <div className="board-row">
                    {this.renderSquare(3)}
                    {this.renderSquare(4)}
                    {this.renderSquare(5)}
                </div>
                <div className="board-row">
                    {this.renderSquare(6)}
                    {this.renderSquare(7)}
                    {this.renderSquare(8)}
                </div>
            </div>
        );
    }

最后修改handleClick函数,如果有胜利者或者玩家点击已经有值的square,则直接return

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

效果图:
在这里插入图片描述

3 时间旅行

3.1 保存历史记录

在以上代码中,使用slice方法保存了数组的副本,这是实现保存历史记录的关键所在。
这里需要再次将代码进行提升,把初始化,点击监听事件,部分render()提升至Game

    constructor(props) {
        super(props);
        this.state = {
            history: [
                {
                    squares: Array(9).fill(null),
                }
            ],
            iXNext: true,
        }
    }
    handleClick(i) {
        const history = this.state.history;
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        // const squares = this.state.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
            return;
        }
        squares[i] = this.state.iXNext ? 'X' : 'O';
        this.setState({
            /**
             * concat方法不会改变原数组, 与push不一样。
             */
            history: history.concat([{
                squares: squares,
            }]),            
            iXNext: !this.state.iXNext,
        });
    }
    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.iXNext ? 'X' : 'O');
        }
        return (
            <div className="game">
                <div className="game-board">
                    <Board
                        squares={current.squares}
                        onClick={(i) => this.handleClick(i)}
                    />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
 	}

Board组件中删除构造方法和handleClick方法

renderSquare(i) {
        return (
            <Square value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)} />
        )
    }
    render() {
        return (
            <div>
                <div className="board-row">
                    {this.renderSquare(0)}
                    {this.renderSquare(1)}
                    {this.renderSquare(2)}
                </div>
                <div className="board-row">
                    {this.renderSquare(3)}
                    {this.renderSquare(4)}
                    {this.renderSquare(5)}
                </div>
                <div className="board-row">
                    {this.renderSquare(6)}
                    {this.renderSquare(7)}
                    {this.renderSquare(8)}
                </div>
            </div>
        );
    }

历史记录就已经保存下来了。

3.2 实现时间旅行

使用数组的map方法,把历史步骤映射为代表按钮的 React 元素,然后可以展示出一个按钮的列表,点击这些按钮,可以“跳转”到对应的历史步骤

const moves = history.map((step, move) => {
            const desc = move ?
                'Go to move #' + move :
                'Go to game start';
            return (
                <li key={move}>
                    <button onClick={() => this.jumpTo(move)}>
                        {desc}
                    </button>
                </li>
            )
        });
return (
            <div className="game">
                <div className="game-board">
                    <Board
                        squares={current.squares}
                        onClick={(i) => this.handleClick(i)}
                    />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <ol>{moves}</ol>
                </div>
            </div>
        );

constructor中添加stepNumber

    constructor(props) {
        super(props);
        this.state = {
            history: [
                {
                    squares: Array(9).fill(null),
                }
            ],
            iXNext: true,
            stepNumber: 0
        }
    }

实现jumpTo方法

    jumpTo(step) {
        this.setState({
            stepNumber: step,
            iXNext: (step % 2) === 0
        })
    }

效果:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值