萌新react学习日记(一)

940 阅读3分钟

非程序员的萌新,第一次学react,若有谬误,欢迎指正

完成官方入门教程


按照react官方的入门教程实现了一个三连棋(井字棋),实现的功能有:

  • tic-tac-toe(三连棋)游戏的所有功能
  • 能够判定玩家何时获胜
  • 能够记录游戏进程
  • 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态

官方也留下一些课外作业:

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

开始做作业


首先所需文具

  • nodejs
  • react

第一题

在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。

这里说历史记录中每一步的坐标,那我们就在历史记录上动手脚,让每次点击(下子)都记录棋子的坐标。

game的处理点击事件的函数handleClick中增加记录坐标lastMoves

 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,
            lastMoves: {
                player: squares[i],
                row: parseInt(i / Options.boardRow) + 1,
                col: i % Options.boardCol + 1,
            }
        }]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext,
    });
}

lastMoves是一个对象,记录落子的选手和落子的坐标(行,列)
这里的Options.boardRow是第三题的一部分,这里先不细说,它代表着棋盘的行数,井字棋里的棋盘是3x3,所以这里实际上就是parseInt(i / 3) + 1,因为i的取值范围为0-8,并且棋盘上共有三列,所以i每隔3就代表着换行0-2,3-5,6-8,这样i/3 再用parseInt取整得到的就是0,1,2代表着行数,但我们坐标的需要从1开始所以要都加上1。获取落子的行号就是parseInt(i / Options.boardRow) + 1,列号就不细说了都差不多。

目前为止我们已经能够记录每下一步棋的棋子坐标以及下棋的人,剩下的就是把它显示出来

我们找到显示坐标的地方,就是Game中渲染历史记录的Rander里的moves

const moves = history.map((step, move) => {
    const desc = move ? `player:${step.lastMoves.player},moves(row,col):(${step.lastMoves.row},${step.lastMoves.col})` : 'Go to game start';
    return (
        <li key={move}>
            <button onClick={() => this.jumpTo(move)} className={'historyBtn ' + (move === this.state.stepNumber ? 'active' : '')}>{desc}</button>
        </li>
    );
})

这里教程有留下一个变量没用上,现在刚好可以用到step
这个代表这历史记录里的每一步棋盘,也包含了我们刚刚加的落子坐标对象,显示出来就可以。这样第一题就做完了~

第二题

在历史记录列表中加粗显示当前选择的项目。

加粗涉及到样式,个人不太喜欢在脚本里直接操作DOM和style,所以我们要在css中添加加粗样式 在渲染的时候通过变量来控制显示class。
index.css

.historyBtn {
    border: none;
    background: #fff;
}

.historyBtn.active {
    font-weight: bold;
}

.historyBtn:hover {
    color: royalblue;
    cursor: pointer;
}

.historyBtn作为项目的默认样式,附加.active作为当前选中的样式。

然后还是在刚刚js的moves中把样式加进去。通过判断stepNumber是不是当前步骤把附加的.active拼上去,记得class之间留空格。

 <li key={move}>
    <button onClick={() => this.jumpTo(move)} className={'historyBtn ' + (move === this.state.stepNumber ? 'active' : '')}>{desc}</button>
</li>

然后第二题完成,抬走,下一题~

第三题

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

循环的方法很多,我选最麻烦的...这里仅供参考。
首先原教程的生成棋盘的方式是生成一个长度9的数组,但是格式我不喜欢,不够直观。 我将其改造成[[0,1,2][3,4,5][6,7,8]]这样的格式。每一行一个数组组成一个棋盘。

其次,3x3的棋盘这里3x3也是写死的,也给他改成配置的,到时候3x4,4x4的改下配置和胜利条件即可。

这里我添加了一个变量作为设置,包含棋盘的行数列数。

const Options = {
    boardRow: 3,
    boardCol: 3
}

棋盘的改造可能会带来一些后遗症,所以先实现一个格式化棋盘的方法formatBoardMap方便以后使用棋盘。

const formatBoardMap = (mapRow, mapCol, boardMap) => {
    boardMap = boardMap || [...Array(mapRow * mapCol).keys()]
    return [...Array(mapRow)].map((x, i) => boardMap.slice(i * mapCol, (i + 1) * mapCol))
}

接收三个参数,用作生成新棋盘的行数,列数,以及用作已有棋盘格式化的棋盘对象,其中行数列数必选,最后返回格式化好的棋盘。

接下来就是循环生成棋盘

formatBoardMap(this.props.boardMap[0], this.props.boardMap[1]).map((items, index) => {
    return (
        <div className="board-row" key={index}>
            {items.map((item, i) => this.renderSquare(item, index + i))}
        </div>
    )
})

传入配置的行数列数生成棋盘,返回的是一个数组,通过数组的map循环生成行,在行内循环生成格子。 然后第三题也好了,抬走,下一题~

第四题

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

这题超简单,不多哔哔

constructor(props) {
    super(props);
    this.state = {
        history: [{
            squares: Array(Options.boardRow * Options.boardCol).fill(null),
        }],
        stepNumber: 0,
        xIsNext: true,
        ascending: true
    }
}

直接在Game的构造函数中,把排序状态加进state。这里用正序的布尔值标记排序状态。 然后在历史记录下面增加一个排序按钮(要加在上面也行...)

let ascending = this.state.ascending;
return (
    <div className="game">
        <div className="game-board">
            <Board winSquare={winnerIndex} boardMap={[Options.boardRow, Options.boardCol]} squares={current.squares} onClick={i => this.handleClick(i)} />
        </div>
        <div className="game-info">
            <div>{status}</div>
            <ol>{ascending ? moves : moves.reverse()}</ol>
            <button onClick={() => this.setState({ ascending: !ascending })}>
                {ascending ? 'Ascending' : 'Descending'}
            </button>
        </div>
    </div>
);

按钮的点击事件的就是改变这个排序的值,按钮的显示内容也根据这个值进行切换。

最后把<ol>列表中的显示内容更改下{ascending ? moves : moves.reverse()}使用数组的倒序方法。大功告成,抬走下一题!

第五题

每当有人获胜时,高亮显示连成一线的 3 颗棋子。
当无人获胜时,显示一个平局的消息。

五六两题我把它合成一题,都是判断胜利的。

  • 显示平局的简单我们先做。
 let status = winner ? 'Winner: ' + winner.winner : history.length > 9 ? `No Winner, for peace~` : 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

改造一下判断胜利的状态,如果没有胜利者判断一下棋盘是不是被填满了。也就是history.length > 9,如果棋盘满了那就显示平局

  • 胜利时显示连线

这个比较复杂,首先整个游戏的组件从大到小分别是Game -> Board -> squares,在Game中我们很难对每个格子进行准确的操控,所以把操控权交到Board手上,因为Board负责生成格子,那么思路就是Game把获胜者的格子名单交给Board,由Board通知这些格子该变色了。那么我们开始。

首先格子要有接收变色信号和自己变色的能力

.square.winner {
    color: red;
}
const Square = props => <button className={"square" + (props.isWin ? " winner" : "")} onClick={props.onClick}> {props.value} </button>;

isWin为格子和棋盘约定的暗号,棋盘通知格子isWin的时候,格子就开始变色。

然后就是棋盘在生成格子的时候要把这个约定植入到格子里,等之后棋盘点名了,格子就可以收到通知。当棋盘收到Game给到的胜利者格子名单时,棋盘对照一下名单通知给对应的格子。

renderSquare(i, k) {
    let [a, b, c] = this.props.winSquare;
    let isWin = i === a || i === b || i === c
    return <Square isWin={isWin} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} key={k} />
}

实现Game给出胜利格子名单比较简单 只要改造一下计算胜利者的方法,把胜利的序号一起返回回来。

const 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 {
                winner: squares[a],
                index: [a, b, c]
            };
        }
    }
    return null;
}

Game每次计算胜利者的时候判断时候有胜利者,有的话将胜利者以及格子序号给到名单winnerIndex

let winnerIndex = winner ? winner.index : [];

然后把名单通知给棋盘

<Board winSquare={winnerIndex} boardMap={[Options.boardRow, Options.boardCol]} squares={current.squares} onClick={i => this.handleClick(i)} />

交卷~

完整代码

index.js

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

const Options = {
    boardRow: 3,
    boardCol: 3
}

const 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 {
                winner: squares[a],
                index: [a, b, c]
            };
        }
    }
    return null;
}

const formatBoardMap = (mapRow, mapCol, boardMap) => {
    boardMap = boardMap || [...Array(mapRow * mapCol).keys()]
    return [...Array(mapRow)].map((x, i) => boardMap.slice(i * mapCol, (i + 1) * mapCol))
}

const Square = props => <button className={"square" + (props.isWin ? " winner" : "")} onClick={props.onClick}> {props.value} </button>;


class Board extends React.Component {
    renderSquare(i, k) {
        let [a, b, c] = this.props.winSquare;
        let isWin = i === a || i === b || i === c
        return <Square isWin={isWin} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} key={k} />
    }
    render() {
        return (
            <div>
                {
                    formatBoardMap(this.props.boardMap[0], this.props.boardMap[1]).map((items, index) => {
                        return (
                            <div className="board-row" key={index}>
                                {items.map((item, i) => this.renderSquare(item, index + i))}
                            </div>
                        )
                    })
                }
            </div>
        );
    }
}

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(Options.boardRow * Options.boardCol).fill(null),
            }],
            stepNumber: 0,
            xIsNext: true,
            ascending: 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,
                lastMoves: {
                    player: squares[i],
                    row: parseInt(i / Options.boardRow) + 1,
                    col: i % Options.boardCol + 1,
                }
            }]),
            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) => {

            const desc = move ? `player:${step.lastMoves.player},moves(row,col):(${step.lastMoves.row},${step.lastMoves.col})` : 'Go to game start';
            return (
                <li key={move}>
                    <button onClick={() => this.jumpTo(move)} className={'historyBtn ' + (move === this.state.stepNumber ? 'active' : '')}>{desc}</button>
                </li>
            );
        })

        let status = winner ? 'Winner: ' + winner.winner : history.length > 9 ? `No Winner, for peace~` : 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        let winnerIndex = winner ? winner.index : []
        let ascending = this.state.ascending;

        return (
            <div className="game">
                <div className="game-board">
                    <Board winSquare={winnerIndex} boardMap={[Options.boardRow, Options.boardCol]} squares={current.squares} onClick={i => this.handleClick(i)} />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <ol>{ascending ? moves : moves.reverse()}</ol>
                    <button onClick={() => this.setState({ ascending: !ascending })}>
                        {ascending ? 'Ascending' : 'Descending'}
                    </button>
                </div>
            </div>
        );
    }
}

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


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.winner {
    color: red;
}

.square:focus {
    outline: none;
}

.kbd-navigation .square:focus {
    background: #ddd;
}

.game {
    display: flex;
    flex-direction: row;
}

.game-info {
    margin-left: 20px;
}

.historyBtn {
    border: none;
    background: #fff;
}

.historyBtn.active {
    font-weight: bold;
}

.historyBtn:hover {
    color: royalblue;
    cursor: pointer;
}