一、安装node
- 可以通过官网下载稳定版本并执行安装;
- 或者通过nvm安装你需要的node版本(可以对node版本进行管理,比较推荐)。
二、创建新项目
打开命令终端;
进入你要存放项目的路径;
-
创建react项目"tic-tac-toe"(项目名称):
npx create-react-app tic-tac-toe
-
进入项目"tic-tac-toe":
cd tic-tac-toe
-
运行项目:
run start
浏览器访问地址
http://localhost:3000
,可看到默认页面;-
使用IDE或者编辑器打开项目(推荐使用 webstorm、vscode ),列表如下,已忽略"node_modules":
│ .gitignore │ package-lock.json │ package.json │ README.md │ ├─public │ favicon.ico │ index.html │ logo192.png │ logo512.png │ manifest.json │ robots.txt │ └─src App.css App.js App.test.js index.css index.js logo.svg serviceWorker.js setupTests.js
- 删除src目录下的所有文件;
三、初始化项目文件
-
在src目录下创建
index.css
,可以自己编写css样式代码,也可以将例子中的样式代码复制到此文件中;body { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; } ol, ul { padding-left: 30px; } li { list-style: none; } .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; height: 60px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 60px; } .square:focus { outline: none; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; justify-content: center; } .game-info { margin-left: 20px; }
-
在src目录下创建
index.js
,引入所需要的依赖:import React from 'react'; import ReactDOM from 'react-dom'; import './index.css';
-
将例子中的
javascript
代码复制到index.js
文件中;import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; class Square extends React.Component { render() { return ( ); } } class Board extends React.Component { renderSquare(i) { return
; } render() { const status = 'Next player: X'; return ( {status}{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}{/* status */}- {/* TODO */}
, document.getElementById('root') ); 再次查看浏览器,此时应该已经刷新页面,或者可以手动刷新;
四、改造代码
-
在
Board
组件中向Square
组件传递参数,value={i}
:renderSquare(i) { return
; } -
在
Square
组件中接收Board
组件传来的参数,并显示:class Square extends React.Component { render() { return ( ); } }
-
尝试在
Square
组件上添加点击方法:class Square extends React.Component { render() { return ( ); } }
-
在
Square
组件中添加状态用来记录按钮被点击过:class Square extends React.Component { constructor(props) { super(props); this.state = { value: null }; } render() { return ( ); } }
-
通过setState()方法,当点击按钮,就把按钮的显示内容改为
state.value
的值:class Square extends React.Component { constructor(props) { super(props); this.state = { value: null }; } render() { return ( ); } }
五、进一步完善,轮流落子
-
将
Square
组件中的状态统一移动到父组件Board
中进行管理:class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null) } } }
-
在父组件
Board
中向子组件Square
传递两个参数value
、onClick
:class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null) } } renderSquare(i) { return
this.handleClick(i)} />; } } -
在子组件
Square
修改按钮显示为父组件传来的参数,监听的点击方法也为父组件传来的方法:class Square extends React.Component { constructor(props) { super(props); this.state = { value: null }; } render() { return ( ); } }
-
删除子组件
Square
的自有状态:class Square extends React.Component { render() { return ( ); } }
-
优化子组件
Square
为函数组件,无状态使用函数组件更效率:function Square(props) { return ( ); }
-
定义父组件
Board
的handleClick方法:handleClick(i) { let temp = this.state.squares.slice(); temp[i] = "X"; this.setState({ squares: temp }); }
-
完善轮流落子功能,在父组件
Board
中设置一个开关isX
,默认为true
,每落下一步,isX将反转,依此判断下一步该落什么子:constructor(props) { super(props); this.state = { squares: Array(9).fill(null), isX: true } } handleClick(i) { let temp = this.state.squares.slice(); temp[i] = this.state.isX ? "X" : "O"; this.setState({ squares: temp, isX: !this.state.isX }); }
-
在父组件
Board
中在修改提示信息,以便知道下一步是什么子:const status = `Next player: ${this.state.isX ? "X" : "O"}`;
六、进一步完善,判断输赢
-
编写判断输赢的方法,放在
index.js
最后面,写好输赢规则lines
,依次遍历参数lines
,将squares
中每一项进行三三比对,得出结果: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]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
-
父组件
Board
中,修改点击事件,当已经有人胜出,或者当前方格已经有数据,不做操作:handleClick(i) { let temp = this.state.squares.slice(); if (calculateWinner(this.state.squares) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ squares: temp, isX: !this.state.isX }); }
七、继续完善,增加历史记录功能
-
在顶层
Game
组件中添加状态history
,用来保存每一步的棋盘数据,并且把Board
组件中的状态移到Game
组件中:class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null) }], isX: true } } }
-
将
Board
组件中的状态移除,并把原先状态中的参数改为接收父组件Game
传来的参数:class Board extends React.Component { renderSquare(i) { return
this.props.onClick(i)} />; } render() { return ( {this.props.status}{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} -
将
Board
组件中的handleClick
方法移动到父组件Game
中,根据当前Game
组件的状态进行调整:class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null) }], isX: true } } handleClick(i) { const history = this.state.history; const cur = history[history.length -1]; let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp }]), isX: !this.state.isX }); } }
-
将
Board
组件中render
方法中的前置判断移动到父组件Game
中,并在调用Board
组件的时候传入参数squares
、status
、onClick
:class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null) }], isX: true } } handleClick(i) { const history = this.state.history; const cur = history[history.length -1]; // 使用最新一次的棋盘数据 let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp }]), isX: !this.state.isX }); } render() { const history = this.state.history; const cur = history[history.length -1]; // 使用最新一次的棋盘数据 const winner = calculateWinner(cur.squares); let status = ""; if (winner) { status = `Winner is: ${winner}`; } else { status = `Next player: ${this.state.isX ? "X" : "O"}`; } return (
this.handleClick(i)} /> {/* status */}- {/* TODO */}
八、展示历史记录
-
在
Game
中渲染一个历史记录按钮列表,使用history
状态的map
方法形成一个history
的历史记录映射:const moves = history.map((item, i) => { const con = i ? `Go to step-${i}` : `Go to start`; return(
-
给每一步添加唯一key:
const moves = history.map((item, i) => { const con = i ? `Go to step-${i}` : `Go to start`; return(
-
在
Game
中将moves
映射插入到对应展示的位置:return (
this.handleClick(i)} /> {status}- {moves}
-
在
Game
中声明状态参数step,来表示当前步骤:constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null) }], isX: true, step: 0 } }
-
在
Game
中编写jumpTo
方法,更改step
状态,并且当步数是偶数时,isX
为true
:jumpTo(i) { this.setState({ step: i, isX: i % 2 === 0 }) }
-
在
Game
中修改handleClick
方法,每走一步,同时更新step
状态:handleClick(i) { const history = this.state.history; const cur = history[history.length -1]; let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp }]), isX: !this.state.isX, step: history.length }); }
-
在
Game
中修改handleClick
方法,将history
改成截取0到当前步骤的数据:handleClick(i) { const history = this.state.history.slice(0, this.state.step + 1); const cur = history[history.length -1]; let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp }]), isX: !this.state.isX, step: history.length }); }
-
在
Game
中修改render方法中当前棋盘通过当前步骤获取:const history = this.state.history; const cur = history[this.state.step]; //通过当前步骤获取 const winner = calculateWinner(cur.squares);
完成
九、功能优化
-
在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号):
class Board extends React.Component { renderSquare(i, axis) { /* 接收坐标参数 */ return
this.props.onClick(i, axis)} />; } render() { return ( {this.props.status}{this.renderSquare(0, [1,1])} // 传入坐标参数 {this.renderSquare(1, [1,2])} {this.renderSquare(2, [1,3])}{this.renderSquare(3, [2,1])} {this.renderSquare(4, [2,2])} {this.renderSquare(5, [2,3])}{this.renderSquare(6, [3,1])} {this.renderSquare(7, [3,2])} {this.renderSquare(8, [3,3])}class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), axis: [] // 声明坐标状态 }], isX: true, step: 0 } } handleClick(i, axis) { // 接收坐标参数 const history = this.state.history.slice(0, this.state.step + 1); const cur = history[history.length - 1]; let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp, axis: axis // 更改坐标状态 }]), isX: !this.state.isX, step: history.length, }); } jumpTo(i) { this.setState({ step: i, isX: (i % 2) === 0 }); } render() { const history = this.state.history; const cur = history[this.state.step]; const winner = calculateWinner(cur.squares); const moves = history.map((item, i) => { // 展示坐标 const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`; const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`; return(
this.handleClick(i,axis)} // 接收、传入坐标参数 /> {status}- {moves}
-
在历史记录列表中加粗显示当前选择的项目:
return( // 添加判断是否需要".active"
/* index.css 添加.active */ .active { font-weight: bold; color: #38ccff; }
-
使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode):
class Board extends React.Component { renderSquare(i, axis) { const randomKey = `cell${Math.floor(Math.random() * 900000 + 100000)}`; return
this.props.onClick(i, axis)} />; } // 渲染行方法 renderRow(j) { const rowACell = this.props.rowACell; let temp = []; for (let i = 0; i < rowACell; i++) { temp.push(this.renderSquare(i+rowACell*j, [j+1,i+1])); } const randomKey = `row${Math.floor(Math.random() * 900000 + 100000)}`; return( {temp}) } // 渲染棋盘方法 renderBoard() { const rowACell = this.props.rowACell; let temp = []; for (let i = 0; i < rowACell; i++) { temp.push(this.renderRow(i)); } return temp; } render() { return ({this.props.status}{this.renderBoard()} // 渲染棋盘class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), axis: [] }], isX: true, step: 0, rowACell: 3 // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则 } } handleClick(i, axis) { const history = this.state.history.slice(0, this.state.step + 1); const cur = history[history.length - 1]; let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp, axis: axis }]), isX: !this.state.isX, step: history.length, }); } jumpTo(i) { this.setState({ step: i, isX: (i % 2) === 0 }); } render() { const history = this.state.history; const cur = history[this.state.step]; const winner = calculateWinner(cur.squares); const moves = history.map((item, i) => { const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`; const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`; return(
this.handleClick(i,axis)} /> {status}- {moves}
-
添加一个可以升序或降序显示历史记录的按钮:
class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), axis: [] }], isX: true, step: 0, rowACell: 3, // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则 order: true // 升序true,降序false } } handleClick(i, axis) { const history = this.state.history.slice(0, this.state.step + 1); const cur = history[history.length - 1]; let temp = cur.squares.slice(); if (calculateWinner(temp) || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp, axis: axis }]), isX: !this.state.isX, step: history.length }); } jumpTo(i) { this.setState({ step: i, isX: (i % 2) === 0 }); } // 改变排序方法 changeOrder() { this.setState({ order: !this.state.order }) } // 最终展示的历史记录 historyCoder(moves) { if (this.state.order) { return moves; } else { return moves.reverse(); } } render() { const history = this.state.history; const cur = history[this.state.step]; const winner = calculateWinner(cur.squares); const moves = history.map((item, i) => { const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`; const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`; return(
this.handleClick(i,axis)} /> // 改变按钮文字// 展示历史记录- {this.historyCoder(moves)}
-
每当有人获胜时,高亮显示连成一线的 3 颗棋子:
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]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { // 返回值改造 return { player: squares[a], line: lines[i] }; } } // 返回值改造 return { player: null, line: [] }; }
class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), axis: [] }], isX: true, step: 0, rowACell: 3, // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则 order: true, // 升序true,降序false } } handleClick(i, axis) { const history = this.state.history.slice(0, this.state.step + 1); const cur = history[history.length - 1]; let temp = cur.squares.slice(); // 修改winner const res = calculateWinner(temp); const winner = res.player; if (winner || temp[i]) { return; } temp[i] = this.state.isX ? "X" : "O"; this.setState({ history: history.concat([{ squares: temp, axis: axis }]), isX: !this.state.isX, step: history.length }); } jumpTo(i) { this.setState({ step: i, isX: (i % 2) === 0 }); } changeOrder() { this.setState({ order: !this.state.order }) } historyCoder(moves) { if (this.state.order) { return moves; } else { return moves.reverse(); } } render() { const history = this.state.history; const cur = history[this.state.step]; // 修改winner const res = calculateWinner(cur.squares); const winner = res.player; const moves = history.map((item, i) => { const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`; const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`; return(
this.handleClick(i,axis)} /> - {this.historyCoder(moves)}
class Board extends React.Component { renderSquare(i, axis) { // 判断是否高亮 const highLight = this.props.highLight; const isHigh = highLight.includes(i); const randomKey = `cell${Math.floor(Math.random() * 900000 + 100000)}`; return
this.props.onClick(i, axis)} isHigh={isHigh} />; // 传入高亮 } renderRow(j) { const rowACell = this.props.rowACell; let temp = []; for (let i = 0; i < rowACell; i++) { temp.push(this.renderSquare(i+rowACell*j, [j+1,i+1])); } const randomKey = `row${Math.floor(Math.random() * 900000 + 100000)}`; return( {temp}) } renderBoard() { const rowACell = this.props.rowACell; let temp = []; for (let i = 0; i < rowACell; i++) { temp.push(this.renderRow(i)); } return temp; } render() { return ({this.props.status}{this.renderBoard()}function Square(props) { return ( ); }
-
当无人获胜时,显示一个平局的消息:
// 修改Game组件render中的结果判断 let status = ""; let full = true; cur.squares.forEach(item => { full = full && item; }); if (winner) { status = `Winner is: ${winner}`; } else if (!winner && full) { status = `It's a draw!!!`; } else { status = `Next player: ${this.state.isX ? "X" : "O"}`; }
十、完成啦,代码如下:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Square(props) {
return (
);
}
class Board extends React.Component {
renderSquare(i, axis) {
const highLight = this.props.highLight;
const isHigh = highLight.includes(i);
const randomKey = `cell${Math.floor(Math.random() * 900000 + 100000)}`;
return this.props.onClick(i, axis)} isHigh={isHigh} />;
}
renderRow(j) {
const rowACell = this.props.rowACell;
let temp = [];
for (let i = 0; i < rowACell; i++) {
temp.push(this.renderSquare(i+rowACell*j, [j+1,i+1]));
}
const randomKey = `row${Math.floor(Math.random() * 900000 + 100000)}`;
return(
{temp}
)
}
renderBoard() {
const rowACell = this.props.rowACell;
let temp = [];
for (let i = 0; i < rowACell; i++) {
temp.push(this.renderRow(i));
}
return temp;
}
render() {
return (
{this.props.status}
{this.renderBoard()}
);
}
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
axis: []
}],
isX: true,
step: 0,
rowACell: 3, // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则
order: true, // 升序true,降序false
}
}
handleClick(i, axis) {
const history = this.state.history.slice(0, this.state.step + 1);
const cur = history[history.length - 1];
let temp = cur.squares.slice();
const res = calculateWinner(temp);
const winner = res.player;
if (winner || temp[i]) {
return;
}
temp[i] = this.state.isX ? "X" : "O";
this.setState({
history: history.concat([{
squares: temp,
axis: axis
}]),
isX: !this.state.isX,
step: history.length
});
}
jumpTo(i) {
this.setState({
step: i,
isX: (i % 2) === 0
});
}
changeOrder() {
this.setState({
order: !this.state.order
})
}
historyCoder(moves) {
if (this.state.order) {
return moves;
} else {
return moves.reverse();
}
}
render() {
const history = this.state.history;
const cur = history[this.state.step];
const res = calculateWinner(cur.squares);
const winner = res.player;
const moves = history.map((item, i) => {
const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`;
const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`;
return(
)
});
let status = "";
let full = true;
cur.squares.forEach(item => {
full = full && item;
});
if (winner) {
status = `Winner is: ${winner}`;
} else if (!winner && full) {
status = `It's a draw!!!`;
} else {
status = `Next player: ${this.state.isX ? "X" : "O"}`;
}
return (
this.handleClick(i,axis)}
/>
{this.historyCoder(moves)}
);
}
}
// ========================================
ReactDOM.render(
,
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];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return {
player: squares[a],
line: lines[i]
};
}
}
return {
player: null,
line: []
};
}
index.css
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
li {
list-style: none;
}
.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;
height: 60px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 60px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
justify-content: center;
}
.game-info {
margin-left: 20px;
}
.active {
font-weight: bold;
color: #38ccff;
}