前言
这篇文章主要介绍
react官方教程中,五子棋的进阶
开发环境:macOS
node版本:11.8.0
需求清单
- 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
- 在历史记录列表中加粗显示当前选择的项目。
- 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
- 添加一个可以升序或降序显示历史记录的按钮。
- 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
- 当无人获胜时,显示一个平局的消息。
项目开始准备代码
代码我已经赋值粘贴好了。地址:https://github.com/oytoyt/reactGobang/tree/0.1
1.在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
分析
要想拿到坐标就必须要拿到对应位置的下标。
方法一
需要知道每一步棋的坐标。那么就需要点击的时候,把当前下标记录到历史记录当中。
然后在渲染列表的时候,判断当前棋子的坐标。
最后渲染出来。
方法二
在渲染列表的时候通过当前步骤棋子跟上一步棋子的记录去进行比较。获取出变化的下标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
history = [ { squares: [ null, null, null, null, null, null, null, null, null, ] }, { squares: [ null, null, null, null, 'X', null, null, null, null, ] }, { squares: [ null, null, null, null, 'X', null, null, null, 'O', ] }, ]
|
码
这里先写一个公用的方法。下面的方法都要用到的。
在代码末添加方法。通过下标获取当前坐标。
1 2 3 4 5 6
| function getPos(index) { const row = 3; const x = index % row + 1; const y = Math.floor(index / row) + 1; return `(${y}, ${x})`; }
|
方法一:记录坐标
修改Game
组件的handleClick
钩子,点击方块的时候记录下标到history
当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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, activeIndex: i } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); }
|
修改Game
组件渲染钩子,显示当前棋子坐标在历史记录当中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { let desc = move ? 'Go to move #' + move : 'Go to game start';
if(move) desc += getPos(step.activeIndex);
return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); }
|
该步代码https://github.com/oytoyt/reactGobang/blob/0.2/src/index.js
方法二:对比数组
在代码末添加方法。通过对比两个数组的每一项,判断出哪个下标不一样。
1 2 3 4 5 6 7 8 9
| function getDiff(arr, arr2) { let _index; arr.map((item, index) => { const a = JSON.stringify(item), b = JSON.stringify(arr2[index]); if(a != b) _index = index; }); return _index; }
|
修改Game
组件渲染钩子,显示当前棋子坐标在历史记录当中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { let desc = move ? 'Go to move #' + move : 'Go to game start';
if(move) { const index = getDiff(history[move].squares, history[move - 1].squares); desc += getPos(index); }
return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); }
|
该步代码https://github.com/oytoyt/reactGobang/blob/0.3/src/index.js
2.在历史记录列表中加粗显示当前选择的项目。
分析
之前做历史记录切换的时候,就用stepNumber
去记录当前选择的项目。那么就可以通过stepNumber
去做判断是否加粗。
方法
修改列表的返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { let desc = move ? 'Go to move #' + move : 'Go to game start';
if(move) { const index = getDiff(history[move].squares, history[move - 1].squares); desc += getPos(index); }
return this.state.stepNumber == move ? ( <li key={move}> <button onClick={() => this.jumpTo(move)}><b>{desc}</b></button> </li> ) : ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); }
|
该步代码https://github.com/oytoyt/reactGobang/blob/0.4/src/index.js
3.使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
分析
平时写代码的时候一定会有写到过双循环。双循环不难。但是如何双循环然后把格子渲染出来。
先改造成动态渲染的先。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
render() { const boardRow = ( <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> ) return ( <div>{boardRow}</div> ); } }
|
然后发现报错了。。。
1 2 3 4 5 6 7 8 9 10
| index.js:1375 ./src/index.js Line 30:9: Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?
28 | {this.renderSquare(2)} 29 | </div> > 30 | <div className="board-row"> | ^ 31 | {this.renderSquare(3)} 32 | {this.renderSquare(4)} 33 | {this.renderSquare(5)}
|
大概意思是最外层必须有一个父元素包裹着着三个类名为board-row
的div
。我改还不行嘛。
再次改造,先试试一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
render() { const boardRow = ( <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> ) return ( <div>{boardRow}</div> ); } }
|
发现可以玩了。只不过只有3个格子。而且这三个格子。不是动态的。。。那先把它换成动态的吧。
第一个循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
render() { const len = 3; const row = Array(len).fill(null).map((item, index) => { return this.renderSquare(index); }); const boardRow = ( <div className="board-row"> {row} </div> ); return ( <div>{boardRow}</div> ); } }
|
运行代码。发现报错了。报错信息如下:
1 2 3 4 5 6 7 8
| index.js:1375 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `Board`. See https: in Square (at src/index.js:16) in Board (at src/index.js:127) in div (at src/index.js:126) in div (at src/index.js:125) in Game (at src/index.js:143)
|
是因为用了循环但是没有加key的原因。那我们添加上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Board extends React.Component { renderSquare(i) { return ( <Square key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
render() { const len = 3;
const row = Array(len).fill(null).map((item, index) => { return this.renderSquare(index); });
const boardRow = ( <div className="board-row"> {row} </div> );
return ( <div>{boardRow}</div> ); } }
|
可以正常运行。那我们接下来做第二个循环。循环3行。
第二个循环
上面一个循环已经可以显示出三个格子出来。那么第二个循环就是。要把他们循环三遍。变成三行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Board extends React.Component { renderSquare(i) { return ( <Square key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
render() { const len = 3;
const row = Array(len).fill(null).map((item, index) => { return this.renderSquare(index); });
const boardRow = Array(len).fill(null).map((item, index) => { return ( <div key={index} className="board-row"> {row} </div> ); });
return ( <div>{boardRow}</div> ); } }
|
这里渲染是渲染出来了。但是有个小问题就是点击一个格子。跟他同一列的都变状态了。
原因是因为第一个循环渲染每个格子的时候传入的参数都是index不会根据第二个循环的循环次数而改变。
那么就是要将第二个循环的下标作为参数传给第一个循环。
那就需要将他改造为函数的形式。接受参数。
完善代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const len = 3;
class Board extends React.Component { renderSquare(i) { return ( <Square key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
row(i) { return Array(len).fill(null).map((item, index) => { return this.renderSquare((i * len) + index); }); }
render() { const boardRow = Array(len).fill(null).map((item, index) => { return ( <div key={index} className="board-row"> {this.row(index)} </div> ); });
return ( <div>{boardRow}</div> ); } }
|
运行成功。
该步代码https://github.com/oytoyt/reactGobang/blob/0.5/src/index.js
4.添加一个可以升序或降序显示历史记录的按钮。
分析
- 这里增加一个按钮。
- 给按钮添加点击事件。
- 存储一个状态。判断当前升序还是降序。
- 写一个函数改写第三部的状态。并且判断是否需要将历史记录数组倒叙。
- 判断状态,是否需要降序。
码
1.添加按钮。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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> <button>升序/倒序</button> <ol>{moves}</ol> </div> </div> );
|
2.给按钮添加点击事件。
1
| <button onClick={() => this.reverse()}>升序/倒序</button>
|
3.存储一个状态。判断当前升序还是降序。
这里默认是升序的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Game extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true, isReverse: false }; } }
|
4.写一个函数改写第三部的状态。并且判断是否需要将历史记录数组倒叙。
1 2 3
| reverse() { this.setState({isReverse: !this.state.isReverse}); }
|
5.判断状态,是否需要降序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { let desc = move ? 'Go to move #' + move : 'Go to game start';
if(move) { const index = getDiff(history[move].squares, history[move - 1].squares); desc += getPos(index); }
return this.state.stepNumber == move ? ( <li key={move}> <button onClick={() => this.jumpTo(move)}><b>{desc}</b></button> </li> ) : ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); });
if(this.state.isReverse) moves.reverse();
}
|
这样该需求就可以啦。
该步代码https://github.com/oytoyt/reactGobang/blob/0.6/src/index.js
5.每当有人获胜时,高亮显示连成一线的 3 颗棋子。
分析
判断是否有人获胜是通过官方给的calculateWinner
方法去判断的。其主要是通过判断当前是否有符合lines
变量中的规则。
那么我们需要将连成一线的棋子高亮的话,就必须要知道他们的下标。然后将对应的高亮出来。
但是calculateWinner
方法只是返回了获胜的名字。那么我门需要把对应符合胜利的目标也返回出来。
码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function calculateWinner(squares, flag) { 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 flag ? lines[i] : squares[a]; } } return null; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Board extends React.Component { renderSquare(i) { const winner = calculateWinner(this.props.squares, true); let flag; if(winner) flag = winner.indexOf(i) == -1 ? false : true; return flag ? ( <Square key={i} mark={true} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ) : ( <Square key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| function Square(props) { return props.mark ? ( <button className="square" onClick={props.onClick}> <mark>{props.value}</mark> </button> ) : ( <button className="square" onClick={props.onClick}> {props.value} </button> ); }
|
这样该需求就可以啦。
该步代码https://github.com/oytoyt/reactGobang/blob/0.7/src/index.js
6.当无人获胜时,显示一个平局的消息。
分析
这个只要判断当前是否为最后一步。并且有无获胜。就可以是否为平局了。
码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| let status; if (winner) { status = "Winner: " + winner;
} else if (this.state.stepNumber == (len * len)) { status = "Peace!"; }else { status = "Next player: " + (this.state.xIsNext ? "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> <button onClick={() => this.reverse()}>升序/倒序</button> <ol>{moves}</ol> </div> </div> );
|
这样该需求就可以啦。
该步代码https://github.com/oytoyt/reactGobang/blob/0.7/src/index.js
总结
初学react。可能会有写得不好的地方。
可能会有更好的方法。如果你有更好的方法,欢迎评论留言一起交流。
谢谢🙏!
源码地址:https://github.com/oytoyt/reactGobang/
官方链接:https://react.docschina.org/tutorial/tutorial.html