react五子棋进阶

前言
这篇文章主要介绍
react官方教程中,五子棋的进阶
开发环境:macOS
node版本:11.8.0


需求清单

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

项目开始准备代码

代码我已经赋值粘贴好了。地址: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[0]跟history[1]的不同。

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) desc += getPos(step.activeIndex);

// 方法二
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) desc += getPos(step.activeIndex);

// 方法二
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-rowdiv。我改还不行嘛。

再次改造,先试试一个。

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://fb.me/react-warning-keys for more information.
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 (
// 添加key值
<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 (
// 添加key值
<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);
});

// 第二个循环。并且加上key。
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 (
// 添加key值
<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() {
// 这里先提升到代码头部, 因为row函数也要用到
// const len = 3;
// 第二个循环。并且加上key。
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. 这里增加一个按钮。
  2. 给按钮添加点击事件。
  3. 存储一个状态。判断当前升序还是降序。
  4. 写一个函数改写第三部的状态。并且判断是否需要将历史记录数组倒叙。
  5. 判断状态,是否需要降序。

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) desc += getPos(step.activeIndex);

// 方法二
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
// flag为true时,返回符合的数组
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;
// 将值传给`Square`
return flag ? (
// 添加key值
<Square
key={i}
mark={true}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
) : (
// 添加key值
<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



react五子棋进阶
作者
墨陌默
发布于
2019年11月12日
许可协议