人生就像迷宫,我们用上半生找寻入口,用下半生找寻出口。——朱德庸

上次说到我们按照官方文档体验了一下React

这次我们搭建本地react开发环境,首先需要将node升级到14以上并且npm需要5.6以上,这个去官网下载安装包覆盖安装即可

然后我们按照教程创建项目

1
npx create-react-app my-app

注意

第一行的 npx 不是拼写错误 —— 它是 npm 5.2+ 附带的 package 运行工具

然后删除src目录下的默认文件,创建一个index.css以及index.js

index.css

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
41
42
43
44
45
46
47
48
49
50
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

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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')
);

完成后按照教程一步一步来

做到最后实现了整个功能,我还进行了总结中的拓展

如果你还有充裕的时间,或者想练习一下刚刚学会的 React 新技能,这里有一些可以改进游戏的想法供你参考,这些功能的实现顺序的难度是递增的:

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

最后我的index.js为:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
return (<button className='square' style={{ color: props.highlight ? 'red' : '' }} onClick={props.onClick}>{props.value}</button>)
}

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

renderSquare(i) {
return <Square highlight={this.props.line?.includes(i)} key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
}

render() {
let index = 0
// 3.使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
return (
<div>
{[1, 2, 3].map(i => (
<div key={i} className='board-row'>
{Array(3).fill(null).map(() => this.renderSquare(index++))}
</div>
))}
</div>);
}
}

class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{ squares: Array(9).fill(null), index: null }],
stepNumber: 0,
xIsNext: true,
historyIsDesc: false
}
}

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).winner || squares[i]) {
return
}
squares[i] = this.state.xIsNext ? 'X' : 'O'
// 1.在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
let row = i + 1;
let cell = 1;
while (row > 3) {
row -= 3;
cell++;
}

this.setState({
history: history.concat([{ squares, index: { row, cell } }]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
})
}

changeHistoryOrderBy() {
this.setState({ ...this.state, historyIsDesc: !this.state.historyIsDesc })
}

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

render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const { winner, line } = calculateWinner(current.squares);
let historyIsDesc = this.state.historyIsDesc;

const moves = history.map((step, move) => {
// console.log({ '步数': move, '记录': step.squares, '坐标': step.index, '是否选中': this.state.stepNumber == move })

const desc = move ? `Go to move #${move} (${step.index.row},${step.index.cell})` : 'Go to game start';
return (
<li key={move}>
{/* 2.在历史记录列表中加粗显示当前选择的项目。 */}
<button style={{ fontWeight: this.state.stepNumber == move ? 'bold' : '' }} onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
)
})

let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
// 6.当无人获胜时,显示一个平局的消息。
// console.log({ 'this.state.stepNumber': this.state.stepNumber })
if (this.state.stepNumber < 9) {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
} else {
status = 'Draw!';
}
}


return (
<div className="game">
<div className="game-board">
<Board line={line} squares={current.squares} onClick={(i) => this.handleClick(i)} />
</div>
<div className="game-info">
<div>{status}</div>
{/* 4.添加一个可以升序或降序显示历史记录的按钮。 */}
<button onClick={() => this.changeHistoryOrderBy()}>{`切换为${historyIsDesc ? '升' : '降'}序显示历史记录`}</button>
<ol>{historyIsDesc ? moves.reverse() : moves}</ol>
</div>
</div>
);
}
}

// ========================================

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


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];
// console.log([a, b, c])
// console.log({ 'squares[a]': squares[a], 'squares[b]': squares[b], 'squares[c]': squares[c] })
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
// 5.每当有人获胜时,高亮显示连成一线的 3 颗棋子。
return { winner: squares[a], line: [a, b, c] };
}
}
return {};
}

完整代码放到了码云:https://gitee.com/VampireAchao/simple-react.git