React 官方教程: zh-hans.reactjs.org/tutorial/tu…
github地址:github.com/ShigureRain…
预览地址:shigurerain.github.io/react-tic-t…
使用 React 实现井字棋小游戏,前面步骤可以查看 React 官方教程,这里实现最后的改进
- 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
- 在历史记录列表中加粗显示当前选择的项目。
- 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
- 添加一个可以升序或降序显示历史记录的按钮。
- 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
- 当无人获胜时,显示一个平局的消息。
记录坐标
分析需求:
- 我们需要一个 坐标hash 来记录每一次点击的坐标
- 既然要回退历史记录,我们仍然需要 history 来记录我们所做的每一步操作
获取坐标
声明一个查找坐标函数,作用是传进点击的格子参数,获取它的位置
const coordinate = (i) => {
const findCoordinate = [
[1, 1],
[1, 2],
[1, 3],
[2, 1],
[2, 2],
[2, 3],
[3, 1],
[3, 2],
[3, 3],
]
return findCoordinate[i]
}
history 添加记忆数据
- 在 Game 组件的构造函数内添加 history 的参数
constructor(props) {
super(props)
this.state = {
history: [{
squares: Array(9).fill(null),
coordinates: Array(2).fill(null), //坐标参数
xIsNext: true //用于展示这一步是谁下的
}],
stepNumber: 0,
xIsNext: true
}
}
- click 事件的 history 参数
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,
coordinates: coordinate(i), //记录点击时的坐标
xIsNext: this.state.xIsNext, //记录这一步的棋子
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
})
}
渲染页面
render(){
...
const moves = history.map((step, move) => {
const desc = move ?
'#' + move + '\n' + (step.xIsNext ? 'X' : 'O') + '\n' + step.coordinates :
'Go to game start'
return (
<li key={move}>
<button onClick={() => {this.jumpTo(move)}}>{desc}</button>
</li>
)
})
...
}
加粗当前选择
分析需求:遍历history,找到步骤的一项添加className
Class 组件的 render 函数
render() {
...
const moves = history.map((step, move) => {
...
return (
<li key={move}>
//添加了className
<button className={move === this.state.stepNumber ? 'current-step' : ''}
onClick={() => {this.jumpTo(move)}}>{desc}</button>
</li>
)
})
...
}
循环渲染棋子
分析需求:使用数组的 map 方法来做出双重循环
const Board = (props) => {
...
const row = [1, 2, 3]
let column = 0
return (
<div>
{
row.map((key) => {
return (
<div key={key} className="board-row">
{
row.map(() => {
return renderSquare(column++)
})
}
</div>
)
})
}
</div>
)
}
可升序降序显示历史记录
分析需求:
- 添加一个点击按钮,可反转 history 数据
- 识别列表是否为反转状态,用于点击棋盘时数据改变逻辑
- stepNumber 需要重新计算为之前点击的一步
sort按钮
...
sort() {
this.setState({
history: this.state.history.slice().reverse(),
reverse: !this.state.reverse,
stepNumber: this.state.history.length - this.state.stepNumber - 1
})
}
...
render(){
...
return (
...
<div className="game-info">
<div>{status}</div>
<button onClick={() => {this.sort()}}>Reverse</button>
<ol>{moves}</ol>
</div>
...
)
...
}
识别是否反转状态,改变操作逻辑
- 在构造函数添加 reverse
constructor(props) {
super(props)
this.state = {
...
reverse: false
}
}
- 根据是否反转,处理点击事件,反转时:
- 需要从头部插入数据
- 插入最新的 stepNumber 为第一条
- 点击后的 history 记录从上开始删除
- 当前的 stepNumber 需要重新计算
handleClick(i) {
const history = this.state.reverse ? this.state.history.slice(this.state.stepNumber) :
this.state.history.slice(0, this.state.stepNumber + 1) //删除点击的那一项以外多余的记录
const current = this.state.reverse ? history[0] : history[history.length - 1]
const squares = current.squares.slice()
if (calculateWinner(squares) || squares[i]) {return}
squares[i] = this.state.xIsNext ? 'X' : 'O'
if (this.state.reverse) { //如果是反转模式,需要从头部插入数据
this.setState({
history: ([{
squares: squares,
coordinates: coordinate(i),
xIsNext: this.state.xIsNext,
}]).concat(history),
})
} else {
this.setState({
history: history.concat([{
squares: squares,
coordinates: coordinate(i),
xIsNext: this.state.xIsNext,
}]),
})
}
this.setState({
stepNumber: this.state.reverse ? 0 : history.length, //插入最新的 stepNumber 为第一条
xIsNext: !this.state.xIsNext
})
}
render(){
...
const moves = history.map((step, move) => {
//
const nowMove = this.state.reverse
? history.length - move - 1 : move //当前的 stepNumber 需要重新计算
const desc = nowMove ?
'#' + nowMove + '\n' + (step.xIsNext ? 'X' : 'O') + '\n' + step.coordinates :
'Go to game start'
//
return (
...
)
})
...
}
高亮显示获胜的棋子
分析需求:找到获胜棋子的位置改变 className
- 先添加css样式
.highlight-winner {
color: red;
}
- 修改一下计算获胜棋子的方法,改为返回坐标
const calculateWinner = (squares) => {
...
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 lines[i] //返回坐标
}
}
return null
}
- 在 Game 组件的 render 函数内获取坐标,并传参给 Board
render(){
let winner
if (calculateWinner(current.squares)) { //这里替换之前的winner参数
winner = current.squares[calculateWinner(current.squares)[0]]
}
const winnerCoordinate = calculateWinner(current.squares)
renturn
...
<Board winner={winnerCoordinate} squares={current.squares} onClick={(i) => {this.handleClick(i)}}/>
...
}
- 在 Board 组件传参给 Square
const renderSquare = (i) => {
const className = props.winner ? props.winner.includes(i) ? 'highlight-winner' : '' : ''
return (
<Square
key={i}
winnerClass={className}
value={props.squares[i]}
onclick={() => {props.onClick(i)}}/>
)
}
- Square 组件中使用 className
<button className={`square ${props.winnerClass}`} onClick={props.onclick}>
{props.value}
</button>
无人获胜时,显示平局
分析需求:当棋盘满的时候,winner也为空时,展示平局消息
- 可以添加一个加粗样式
.font-bold{
font-weight: bold;
}
- Game 的 render 函数内做平局的条件判断
let status
let classDraw
if (winner) {
status = 'winner: ' + winner
} else {
if (this.state.history.length === 10 && this.state.reverse ? this.state.stepNumber === 0 : this.state.stepNumber === 9) {
status = 'Game draw!'
classDraw = 'font-bold'
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O')
}
}
...
<div className={classDraw}>{status}</div>