项目题全解:井字棋—上-灵析社区

懒人学前端

Step 1:用类组件实现井字棋游戏

Step 1.1:搭建本地开发环境

基础知识:

1、安装 Node.js。

2、使用下面的指令创建一个新的项目

npx create-react-app my-app

3、删除掉新项目中 src/ 文件夹下的所有文件。

注意:

不要删除整个 src 文件夹,删除里面的源文件。我们会在接下来的步骤中使用示例代码替换默认源文件。 

基础步骤:

第一步:

同函数式组件的搭建方式,如下是搭建完成的页面路径。

第二步:在 src/文件夹中创建一个名为 index.css 的文件,并拷贝这些 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;
}

第三步:src/ 文件夹下创建一个名为 index.js 的文件,并拷贝这些 JS 代码。

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

class Square extends React.Component {
    render() {
        return (
            <button className="square">{ }</button>
        );
        //return后面的内容加括号,防止VScode等编辑器自动在return后面加分号
    }
}

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-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)

效果展示:

Step 1.2:通过 Props 传递数据

基础知识:

1、通过阅读代码,我们可以看到有三个 React 组件:

  • Square

渲染了单独的<button>

  • Board

渲染了9个方块

  • Game

渲染了含有默认值的一个棋盘

2、render 方法

  • React 根据描述把结果展示出来
  • render 方法的返回了一个 React 元素
  • 上面的代码使用了 JSX 语法糖

3、props 数据传递

数据通过 props 的传递,从父组件流向子组件

基础步骤:

第一步:

修改代码,将数据从 Board 组件传递到 Square 组件

  • Board组件的renderSquare方法中,改写代码,将名为value的 prop 传递到Square
class Board extends React.Component {

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

第二步:

  • 修改Square组件的render方法,在 button 中加入内容
class Square extends React.Component {
    render() {
        return (
            <button className="square">  {this.props.value}</button>
        );
        //return后面的内容加括号,防止VScode等编辑器自动在return后面加分号
    }
}

效果展示:

基础代码:

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">  {this.props.value}</button>
        );
        //return后面的内容加括号,防止VScode等编辑器自动在return后面加分号
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square value={i} />;
    }

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

        return (
            <div>
                <div className="status">
                    {status}
                </div>

                <div className="board-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)

Step 1.3:组件交互

基础步骤:

这一步的目标是让棋盘的在点击之后每个格子能落下”X"作为棋子

第一步:

Square 组件中 render() 方法的返回值中的 button 标签

第二步:

增加 onClick 属性

class Square extends React.Component {
    render() {
        return (
            <button className="square" onClick={() => alert('click')}>{this.props.value}</button>
        );
    }   
}

Step 1.4:Square 组件实现“记忆”功能

基础知识:

  • React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
  • React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
  • 通过 state 来实现“记忆”功能,在 React 组件的构造函数中设置 this.state 初始化 state
  • this.state 应被视为一个组件的私有属性。在 this.state 中存储当前每个方格(Square)的值,并且在每次方格被点击的时候改变这个值。

基础步骤:

第一步:

在 Square 中用构造函数来初始化 state

class Square extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: null,
        };
    }
    render() {
        return (
            <button className="square" onClick={() => alert('click')}>{this.props.value}</button>
        );
    }
}

第二步:

修改 Square 组件的 render 方法,实现每当方格被点击时显示当前 state 值。

onClick事件监听函数中调用this.setState

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

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

这样子就能实现:在每次<button>被点击的时候通知React去重新渲染Square组件

组件更新后,Square组件的this.state.value的值会变为'X'

每次在组件中调用setState,React都会自动更新子组件

效果展示:

点击变成 x

基础代码:

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() {
        return (
            <button
                className="square"
                onClick={() => this.setState({ value: 'X' })}
            >
                {this.state.value}
            </button>
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square value={i} />;
    }

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

        return (
            <div>
                <div className="status">
                    {status}
                </div>

                <div className="board-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)

Step 1.5:状态提升实现轮流落子

基础步骤:

第一步:

为 Board 组件添加构造函数,将 Board 组件的初始状态设置为长度为 9 的空值数组

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

第二步:

考虑到填充棋盘后,每个格子上可能的形式是null,O,X三种

这些我们打算存储到 squares 数组里面,那么就要修改BoardrenderSquare方法来读取这些值。

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

这样,每个Square就都能接收到一个 value prop 了,这个 prop 的值可以是 'X''O'、 或 null(null 代表空方格)。

第三步:

修改 Square 的事件监听函数。

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

第四步:

从 Board 组件向 Square 组件中传递valueonClick两个props参数,修改 Square 组件。

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

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

第五步: 在 Board 下添加handleClick方法

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

第六步: 实现轮流落子

  • 将"X"默认设置为先手棋,设置一个布尔值来表示下一步轮到哪个玩家
  • 棋子每移动一步,xIsNext都会反转,该值确定下一步轮到哪个玩家,并且游戏的状态会被保存下来
  • 在构造函数中添加xIsNext
  • 修改handleClick函数
constructor(props) {
        super(props);
        this.state = {
            squares: Array(9).fill(null),
            xIsNext: true,

        };
    }

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

效果展示:

基础代码:

index.js

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

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

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

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

    handleClick(i) {
        const squares = this.state.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext,
        });
    }
    renderSquare(i) {
        return (<Square value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />);
    }

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

        return (
            <div>
                <div className="status">
                    {status}
                </div>

                <div className="board-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)

Step 1.6:显示轮到哪个玩家

基础步骤:

第一步:

显示轮到哪个玩家,在 Board 组件的 render 方法中修改 status 的值

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

效果展示:

点击会表面下一个操作的玩家是谁。

基础代码

index.js

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

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

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

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

    handleClick(i) {
        const squares = this.state.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext,
        });
    }
    renderSquare(i) {
        return (<Square value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />);
    }

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

        return (
            <div>
                <div className="status">
                    {status}
                </div>

                <div className="board-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)

Step 1.7:判断胜出者

基础步骤:

第一步:

定义一个函数:

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;
}

第二步:

在 Board 组件的 render 方法中调用刚刚那个函数检查是否有玩家胜出,有人胜出就把玩家信息显示出来

修改Board组件的render方法

const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
    status = 'Winner:' + winner;

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

效果展示:

O 获得了胜利

基础代码

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// 获得胜利的组件
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 Square extends React.Component {
    renderSquare(i) {
        return (<Square value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />);
    }

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

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

    handleClick(i) {

        const squares = this.state.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext,
        });
    }
    renderSquare(i) {
        return (<Square value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />);
    }

    render() {
        const winner = calculateWinner(this.state.squares);
        let status;
        if (winner) {
            status = 'Winner:' + winner;

        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
        return (
            <div>
                <div className="status">
                    {status}
                </div>

                <div className="board-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)

Step 1.8:bug 修复

已经有人胜出了,但是棋盘还能落子 —— 当某个 Square 落子之后,还能覆盖继续落子。

基础步骤:

第一步

修改handleClick,使得当有玩家胜出时,或者某个 Square 被填充时,该函数不做任何处理直接返回

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

效果展示:

基础代码

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// 获得胜利的组件
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 Square extends React.Component {
    renderSquare(i) {
        return (<Square value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />);
    }

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

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

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

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

    render() {
        const winner = calculateWinner(this.state.squares);
        let status;
        if (winner) {
            status = 'Winner:' + winner;

        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
        return (
            <div>
                <div className="status">
                    {status}
                </div>

                <div className="board-new">
                    {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>{ }</div>
                    <ol>{ }</ol>
                </div>
            </div>
        )
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
)


阅读量:2010

点赞量:0

收藏量:0