学习新东西的时候只看肯定是不够的, 所以既然 ReactJS
官方提供了一个学习的案例, 那么就一边写Demo, 一边读文档.
Tips: 我是直接看案例然后按照自己的理解去写的, 并没有按照官方案例亦步亦趋, 所以可能流程会不一样.
井字棋这个 demo 主要分为两部分:
棋盘
- 点击棋盘空白格子可以落子, 落子之后交换棋手.
- 棋格已经落子 / 有棋手获胜时不可落子
游戏信息
- 棋手信息
- 提示当前落子的棋手信息
- 棋手获得胜利后, 显示胜利方
- 对局历史记录
先从棋盘开始, 一共有9个格子, 每个格子有 3 个状态 (空, X, O)
我想到的就直接用 flex
布局了, 不需要考虑 row
和 col
, 这样也可以直接用循环直接输出 9 个棋格, 同时也可以从循环里边取出棋格的状态
所以首先是造棋盘:
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
| import React, { Component } from 'react'; import './App.styl';
class App extends Component { state = { squares: [0, 1, 2, 3, 4, 5, 6, 7, 8] }
render () { const { squares } = this.state return ( <div id='app'> <div className='game-bar'> <div className='board'> {squares.map((info, index) => ( <div className='square' key={index}>{info}</div> ))} </div> </div> </div> ) } } export default App;
|
样式这边我用的预编译是 Stylus
, 省略了括号, 冒号以及分号, 其它和 SCSS
基本一样.
如果不了解大概看一下就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| $square = 60px
.board width $square * 3 border 1px solid black display flex flex-wrap wrap
.square width $square height @width line-height @height font-size 25px text-align center border 1px solid black box-sizing border-box cursor pointer
|
完成之后大概是这样的一个样子

然后是游戏信息的展示, 因为是交替落子, 所以单数步数是 棋手X 落子的话, 双数步数就一定是 棋手O 落子.
顺便给棋格绑定上点击事件, 棋盘的组件大概可以这样初步完成
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
| import React, { Component } from 'react'; import './App.styl';
class App extends Component { state = { squares: [0, 1, 2, 3, 4, 5, 6, 7, 8], step:0, } getCurPlayer () { const { step } = this.state return step % 2 ? "O" : "X" } handleSquareClick (position, player) { const { squares, step } = this.state
console.log(`当前步数: ${step}, 棋手: ${player}, 点击位置: ${position}`)
const newStep = step + 1 const newSquares = squares newSquares[position] = player
this.setState({ squares: newSquares, step: newStep }) } render () { const { squares } = this.state const player = this.getCurPlayer() return ( <div id='app'> <div className='game-bar'> <div className='board'> {squares.map((info, index) => (
<div className='square' key={index} onClick={() => this.handleSquareClick(index,player)}>{info}</div> ))} </div> </div> <div className='game-info'> <div className='player-info'>Current player: {player}</div> </div> </div> ) } } export default App;
|
1 2 3 4 5 6 7 8 9
|
.game-info width 400px height 600px max-height 100vh margin-left 60px .player-info font-size 22px
|

现在棋盘可以被修改了, 棋手信息也有了, 同样也可以获取到每次操作的落子信息.
那么就可以先把历史记录加上, 并且展示
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
| import React, { Component } from 'react'; import './App.styl';
class App extends Component { state = { squares: [0, 1, 2, 3, 4, 5, 6, 7, 8], step:0, history:[], } getCurPlayer () { const { step } = this.state return step % 2 ? "O" : "X" } handleSquareClick (position, player) { const { squares, step, history } = this.state const newStep = step + 1 const newSquares = squares newSquares[position] = player const newHistory = history.concat([{ step:step, player:player, position:position }]) this.setState({ squares: newSquares, step: newStep, history: newHistory }) } render () { const { squares,history } = this.state const player = this.getCurPlayer() return ( <div id='app'> <div className='game-bar'> <div className='board'> {squares.map((info, index) => ( <div className='square' key={index} onClick={() => this.handleSquareClick(index,player)}>{info}</div> ))} </div> </div> <div className='game-info'> <div className='player-info'>Current player: {player}</div> <ol className='history'> {history.map((record, index) => ( <li key={index}>Player_{record.player} to {record.position}</li> ))} </ol> </div> </div> ) } } export default App;
|
现在的游戏界面应该如下

这个时候呢, 就会有几个问题:
- 棋格可以重复落子
- 没有获胜判断
- 棋盘默认显示数字, 很不美观
其实还有一个问题, 在历史回溯的时候会很麻烦, 这个我实在游戏基本都完成了之后才发现的. 这个之后再说
所以在落子的时候增加当前棋格不为空 的判断, 并且以 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 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
| import React, { Component } from 'react'; import './App.styl';
class App extends Component { state = { squares: new Array(9).fill(null), step:0, history:[], } getCurPlayer () { const { step } = this.state return step % 2 ? "O" : "X" } handleSquareClick (position, player) { const { squares, step, history } = this.state if (squares[position]) return const newStep = step + 1 const newSquares = squares newSquares[position] = player const newHistory = history.concat([{ step:step, player:player, position:position }]) this.setState({ squares: newSquares, step: newStep, history: newHistory }) } render () { const { squares, history } = this.state const player = this.getCurPlayer() return ( <div id='app'> <div className='game-bar'> <div className='board'> {squares.map((info, index) => ( <div className='square' key={index} onClick={() => this.handleSquareClick(index,player)}>{info}</div> ))} </div> </div> <div className='game-info'> <div className='player-info'>Current player: {player}</div> <ol className='history'> {history.map((record, index) => ( <li key={index}>Player_{record.player} to {record.position}</li> ))} </ol> </div> </div> ) } } export default App;
|

好了, 棋盘的大致都完成了, 现在来写获胜判断.
因为我想者是每次循环一次所有棋格, 然后找出获胜的玩家, 但是这里循环有点傻,
所以我是看了一下官方的demo, 是这样的一个思路, 把所有的胜利条件都所列了出来, 然后去循环胜利条件, 如果所有胜利条件都循环结束, 没有出现胜利的玩家则继续游戏.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| calculateWinner () { 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], ]; const { squares } = this.state 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 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 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
| import React, { Component } from 'react'; import './App.styl';
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], ]; class App extends Component { state = { squares: new Array(9).fill(null), step:0, history:[], } getCurPlayer () { const { step } = this.state return step % 2 ? "O" : "X" } handleSquareClick (position, player) { const { squares, step, history } = this.state if (this.calculateWinner() || squares[position]) return const newStep = step + 1 const newSquares = squares newSquares[position] = player const newHistory = history.concat([{ step:step, player:player, position:position }]) this.setState({ squares: newSquares, step: newStep, history: newHistory }) }
calculateWinner () { const { squares } = this.state 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 squares[a]; } } return null; } render () { const { squares,history } = this.state const player = this.getCurPlayer() const winner = this.calculateWinner() return ( <div id='app'> <div className='game-bar'> <div className='board'> {squares.map((info, index) => ( <div className='square' key={index} onClick={() => this.handleSquareClick(index,player)}>{info}</div> ))} </div> </div> <div className='game-info'> <div className='player-info'> {winner ? `Player_${winner} is Winner!!!` : `Current player: ${player}`} </div> <ol className='history'> {history.map((record, index) => ( <li key={index}>Player_{record.player} to {record.position}</li> ))} </ol> </div> </div> ) } } export default App;
|
整个游戏流程到现在基本就完成了, 但是点击历史记录回溯操作的功能还没有加上来.
这个时候我就有点卡壳了, 不知道在当前逻辑下怎么回退操作. 就去看了官方的案例,
它是每一步在历史记录内插入当前棋盘数据, 然后回退的时候直接读取历史棋盘数据, 并且没有在历史记录中展示每一步的具体操作内容.
所以想了想还是继续原先的历史记录, 并且按照历史记录来生成棋盘数据(这边参考了山地人老师的思路),
那这样的话, 就不需要手动去修改棋盘数据了, 只要调用一下生成棋盘的函数就可以了. 具体代码修改如下
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
| import React, { Component } from 'react'; import './App.styl'; 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], ]; class App extends Component { state = { squares: new Array(9).fill(null), step: 0, history: [], } getCurPlayer () { const { step } = this.state return step % 2 ? "O" : "X" } handleSquareClick (position, player) { const { squares, step, history } = this.state if (this.calculateWinner() || squares[position]) return const newStep = step + 1 const newHistory = [ ...history.slice(0, step), { step: step, player: player, position: position } ] this.setState({ step: newStep, history: newHistory, squares: this.calculateSquares(newHistory, newStep), }) } calculateWinner () { const { squares } = this.state 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 squares[a]; } } return null; } calculateSquares (history, step) { const newSquares = new Array(9).fill(null) for (let i = 0; i < step; i += 1) { const tempHistory = history[i] newSquares[tempHistory.position] = tempHistory.player } return newSquares } handleGoBack (step) { const { history } = this.state const tempHistory = history.slice(0, step) this.setState({ step: step, squares: this.calculateSquares(tempHistory, step), }) } render () { const { squares, history } = this.state const player = this.getCurPlayer() const winner = this.calculateWinner() return ( <div id='app'> <div className='game-bar'> <div className='board'> {squares.map((info, index) => ( <div className='square' key={index} onClick={() => this.handleSquareClick(index, player)}>{info}</div> ))} </div> </div> <div className='game-info'> <div className='player-info'> {winner ? `Player_${winner} is Winner!!!` : `Current player: ${player}`} </div> <ol className='history'> {history.map((record, index) => ( <li key={index} onClick={() => this.handleGoBack(record.step + 1)}>Player_{record.player} to {record.position}</li> ))} </ol> </div> </div> ) } } export default App;
|
至此为止, 基本的游戏功能都完成了, 只不过整个项目都是写在一个文件中的, 并没有进行组件化操作.
你们可以自行决定怎么拆分, 也可以继续阅读下去.
组件化
我认为可以把棋盘和游戏信息展示进行拆分;
棋盘
这部分我觉得单独一个组件就可以了, 不用继续分解了, 因为单个棋格就一个 div
元素 加上 onClick
事件, 组件化的结果并没有简化代码, 反而增加代码量和不方便阅读.
游戏信息
可以继续拆分成玩家信息和历史记录, 然后再对应组件内进行状态维护和样式的编辑.
拆分后的 App.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
| import React, { Component } from 'react'; import Board from './components/Board' import GameInfo from './components/GameInfo' import './App.styl';
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], ]; class App extends Component { state = { squares: new Array(9).fill(null), player: "", history: [], step: 0, } getCurPlayer () { const { step } = this.state return step % 2 ? "O" : "X" } calculateSquares (history, step) { const newSquares = new Array(9).fill(null) for (let i = 0; i < step; i += 1) { const tempHistory = history[i] newSquares[tempHistory.position] = tempHistory.player } return newSquares } calculateWinner () { const { squares } = this.state 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 squares[a]; } } return null; } handleSquareClick (position, player) { const { squares, step, history } = this.state if (this.calculateWinner() || squares[position]) return const newStep = step + 1 const newHistory = [ ...history.slice(0, step), { step: step, player: player, position: position } ] this.setState({ step: newStep, history: newHistory, squares: this.calculateSquares(newHistory, newStep), }) } handleGoBack (step = 0) { const { history } = this.state const tempHistory = history.slice(0, step) this.setState({ step: step, squares: this.calculateSquares(tempHistory, step), }) } handleRestart () { this.setState({ history: [] }) this.handleGoBack() } render () { const { squares, history } = this.state const player = this.getCurPlayer() const winner = this.calculateWinner() return ( <div id='app'> <div className='game-bar'> <Board squares={squares} onClick={i => this.handleSquareClick(i, player)} /> </div> <GameInfo player={player} winner={winner} history={history} goBack={step => this.handleGoBack(step)} /> </div> ) } } export default App;
|
棋盘组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React, { Component } from 'react'; import './Board.styl';
class Board extends Component { renderSquare (status, index) { return (<div className='square' onClick={() => this.props.onClick(index)} key={index}>{status}</div>) } render () { const { squares } = this.props return ( <div className='board'> {squares.map((status, index) => this.renderSquare(status, index))} </div> ) } } export default Board
|
游戏信息组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React, { Component } from 'react' import History from './History'
class GameInfo extends Component { render () { const { player, winner, history } = this.props const playerInfo = winner ? `Player_${winner} is Winner!!!` : `Current player: ${player}` return ( <div className='game-info'> <div className='player-info'>{playerInfo}</div> <History history={history} onClick={step => this.props.goBack(step)}></History> </div> ) } } export default GameInfo
|
操作历史组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, { Component } from 'react'; import Record from './Record' import './History.styl';
class History extends Component { render () { const { history } = this.props return ( <ol className='history'> <li onClick={() => this.props.onClick(0)}>Go to game start</li> // 回到游戏开始 { history.map((record, index) => ( <Record info={record} key={index} onClick={() => this.props.onClick(record.step + 1)}></Record> )) } </ol> ) } } export default History
|
单条历史记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React, { Component } from 'react'
class Record extends Component { calculatePosition (position) { const posY = (position / 3 >> 0) + 1 const posX = position % 3 + 1 return `[${posY},${posX}]` } render () { const { info } = this.props return ( <li onClick={() => this.props.onClick()}> Player_{info.player} to {this.calculatePosition(info.position)} </li> ) } } export default Record
|
样式文件如下:
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
| body font-family consolas margin 0 #app width 100vw height 100vh display flex justify-content center align-items center .game-bar height 600px max-height 100vh .game-info width 400px height 600px max-height 100vh .player-info font-size 22px
$square = 60px .board width $square * 3 border 1px solid black margin-right 50px display flex flex-wrap wrap .square width $square height $square line-height @height font-size 25px text-align center border 1px solid black box-sizing border-box cursor pointer &:hover background #ddd
.history padding 0 counter-reset number -1 li margin 5px 0 counter-increment number list-style-type none cursor pointer &:before content counter(number) '.' margin-right 10px display inline-bloack &:hover color red text-decoration underline
|

以上
附录
一些卡壳的地方
- 如何创建
State
状态
Constructor
是怎么一回事
- 组件化后子组件如何触发父组件事件
- 为何
onClick
中的事件会自动执行
- React中怎么使用
Stylus
- 组件多层嵌套的情况下怎么获取其它的属性状态,类似
Vuex
的Store