React 入门教程:构建井字棋游戏
December 19, 2024 (1y ago)
官方入门级别教程,完成一个小游戏,带你了解react的设计
关于环境问题
在vue中有自己的,cli在angular中也有自己的cil,在react,当然也有啦,就是这个东西
create-react-app
# 正确的使用步骤,使用npx来运行它,当然你也可以先全局装一个
npx create-react-app [你的项目名称]完成上一步之后,把src中的文件干掉,去官网写下这几个文件
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import ShopingList from './ShoppingList'
// square 继承了reactCompoennt 它是一个组件
class Square extends React.Component {
render () {
return (
<button className="square">
{/* TODO */}
</button>
);
}
}
// board
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 />
<ShopingList />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// 挂载点
ReactDOM.render(
<Game />,
document.getElementById('root')
);
index.css
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;
}
shoping.js
// 我们来写一个最简单的组件
import React from 'react'
class ShopingList extends React.Component {
// render去渲染最原始的结构
render () {
// 这里面丢html就行了
return <div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
}
// 以上的写法很常见,在vue中你可能见过这样的文档,事实上render就是一个语法糖
// return React.createElement('div', {className: 'shopping-list'},
// React.createElement('h1', /* ... h1 children ... */),
// React.createElement('ul', /* ... ul children ... */),
// );
}
export default ShopingList上面的代码仅仅是初始化的代码,具有简单的功能,我们打开yarn start 运行之后得到如下的效果

核心概念问题
在react中有如下的几个重要的概念,component(组件) ,state (状态), props (参数),接下来我们一步一步的进行讲解
在我们上面的index.js中有三个组件,Square(每一个方块),Board(方块构成的棋盘),Game(游戏本体),
props
接下来,我们通过props进行数据的传递,从board组件中传递数据到square中去
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />; }
}
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value} </button>
);
}
}于是,我们得到了这样的效果

如何进行用户的交互?
我们沿用前端开发中重要的编程方式来进行这项工作:"事件驱动" ,主要的步骤就是,监听点击事件作出对应的反应
class Square extends React.Component {
render () {
return (
<button
className="square"
onClick={() => {
alert('you clik me!')
}}
>
{this.props?.value}
</button>
);
}
}
于是我们得到了这样的效果

state
接下来,我们希望点击哪个方块,那个方块就出现'x',首先我们来分析一下,要想实现这样的功能,我们需要点击事件(已经有了),我们还需要一个保存这些点击的值的存储容器,这个容器就是state,注意需要特殊的说明是,我们现在把state容器全部存储在自己的this上去了,后面我们还需要优化它。
// square 继承了reactCompoennt 它是一个组件
class Square extends React.Component {
// 先构造一下
constructor(Props) {
super(Props)
this.state = {
value: null
}
}
render () {
return (
<button
className="square"
onClick={() => {
this.setState({ value: 'X' })
}}
>
{this.state?.value}
</button>
);
}
}于是,我们得到了如下的效果

状态提升
在react中状态提升,是一个非常重要的一个概念,我们解析下来,要把 每一个独立的state 存储的那个“x”丢到 ,父组件中去,这样我们就可以判断出胜者了。
当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。
// square 继承了reactCompoennt 它是一个组件
class Square extends React.Component {
// 先构造一下
// constructor(props) {
// super(props);
// }
render () {
return (
<button
className="square"
onClick={() => {
this.props.onClick()
}}
>
{this.props?.value}
</button>
);
}
}
// board
class Board extends React.Component {
constructor(Props) {
super(Props);
this.state = {
squares: Array(9).fill(null)
};
}
handerClick = (i) => {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({ squares: squares });
}
renderSquare (i) {
return <Square value={this.state.squares[i]} onClick={() => this.handerClick(i)} />;
}
----上述的效果是和之前的效果是一样,不同的地方在于,我们把状态'提升'了出去,
为什么不可变性在react中如此的重要
我们都了解vue中的数据mvvm的原理,在watch监视器中无法进行数组等复杂对象的监听,在react中也存在这样的问题,接下来,我们来看看为什么在react中的会是这样的设计,
- 一般来说,有两种改变数据的方式。第一种方式是直接_修改_变量的值,第二种方式是使用新的一份数据替换旧数据。
let palyer = { score:1,name:'Jeff' }
player.score = 2;- 新旧数据的替换
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'}
// 使用对象展开语法,就可以写成:
// var newPlayer = {...player, score: 2};不仅仅是为了监听数据变化,和性能优化,才使用这种方式,他还有其它的好处,比如:简化复杂的功能,跟踪数据的变化(要知道跟踪不可变的数据要简单得多)
函数组件
在上述的代码中,我们发现,在组件Square中,并不存在状态和其它需要操作的复杂逻辑,而且它只是一个纯的展示UI组件,因此我们把它抽离成 函数组件,简化代码
const Square = (Props) => {
return (
<button
className="square"
onClick={Props.onClick}
>
{Props?.value}
</button>
)
}实现下一个功能轮流落子&判断胜负
现在我们来实现接下里的这两个功能,
- 首先是实现轮流落子,棋子每移动一步,
xIsNext(布尔值)都会反转,该值将确定下一步轮到哪个玩家,并且游戏的状态会被保存下来。
// 设计代码的构建方式 ,远远比如何编写代码要重要得多!
class Board extends React.Component {
constructor(Props) {
super(Props);
this.state = {
squares: Array(9).fill(null),
isXNext: true // 标识下一步是否是X
};
}
handerClick = (i) => {
const squares = this.state.squares.slice();
squares[i] = this.state.isXNext ? 'X' : 'O'; //落子
this.setState({
squares: squares,
isXNext: !this.state.isXNext // 取反
});
}
renderSquare (i) {
return <Square value={this.state.squares[i]} onClick={() => this.handerClick(i)} />;
}
render () {
const status = `Next player: ${this.state.isXNext ? 'X' : 'O'}`;
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>
);
}
}- 如何判断正负?实际上也非常的简单,这里涉及一点点的算法
// board
class Board extends React.Component {
constructor(Props) {
super(Props);
this.state = {
squares: Array(9).fill(null),
isXNext: true
};
}
// click
handerClick = (i) => {
const squares = this.state.squares.slice();
squares[i] = this.state.isXNext ? 'X' : 'O';
this.setState({
squares: squares,
isXNext: !this.state.isXNext
});
}
// who is Winer?
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;
}
// square
renderSquare (i) {
return <Square value={this.state.squares[i]} onClick={() => this.handerClick(i)} />;
}
// render
render () {
// 到底是谁赢得了胜利
const winner = this.calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.isXNext ? 'X' : 'O');
}
实现高级功能
接下里的内容会渐渐的加大难度,加油啊!
- 保存每一次的历史记录
要实现这样的功能,我们需要每一个都保存一下,本次改变前的数据,如果你不使用:"不可变数据"那么,这个需求的实现就会非常棘手
// 把每一个改变前的历史保存在一个数组里
// 它看起来就是这个样子
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',
]
},
// ...
]- 提升状态
我们需要把所有德玛历史记录都再次的提升,而不是放在Borad中,因为我们后续要回退到某一次的历史记录中,整个棋盘都将会被重新渲染,所以必须提升出去
// 接下来,我们把所有的状态提升出去
// board
class Board extends React.Component {
// square 修改这里的值,我们从父组件拿值
renderSquare (i) {
return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
}
// render
render () {
return (
<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 {
constructor(props) {
super(props)
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
}
}
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;
}
handleClick (i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (this.calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
render () {
// 到底是谁赢得了胜利
const history = this.state.history;
const current = history[history.length - 1];
const winner = this.calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} 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>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
- 展示保存的历史记录·
// 下面的代码是展示,历史记录,我们的设计很简单,既然历史记录是数组,那么我们把数组映射到react元素去,使用map方法渲染历史界面
render () {
// 到底是谁赢得了胜利
const history = this.state.history;
const current = history[history.length - 1];
const winner = this.calculateWinner(current.squares);
// 历史记录
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
// 渲染
let status;
if (winner) {
status = 'Winner: ' + winner;
} 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>
<ol>{moves}</ol>
</div>
</div>
);
}
- 小心key的问题
// key 非从重要 ,在性能优化和判断是否更新UI的时候,也很重要,建议在map中都添加key
// 历史记录
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});- 整体的功能实现
我们还需要实现一个jumpTo函数。用户真正的跳转,这里改的地方比较多,主要核心修改点就是在“当前的步骤的棋盘数据”,“上一步的棋盘的数据”
class Board extends React.Component {
// square 修改这里的值,我们从父组件拿值
renderSquare (i) {
return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
}
// render
render () {
return (
<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 {
constructor(props) {
super(props)
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0, // 当前是在那一次历史记录
xIsNext: true,
}
}
// 谁赢了?
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;
}
// 点击落子
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 (this.calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
stepNumber: history.length,
});
}
// 跳转去哪儿?
jumpTo = (step) => {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0, // 由于我们第一步的X 先落,所有这里很简单就能计算
});
}
render () {
// 到底是谁赢得了胜利
const history = this.state.history;
const current = history[this.state.stepNumber]; // 这就是我们当前在那儿一步
const winner = this.calculateWinner(current.squares);
// 历史记录
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
// 渲染
let status;
if (winner) {
status = 'Winner: ' + winner;
} 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>
<ol>{moves}</ol>
</div>
</div>
);
}
}至此你的很NB 的游戏就做完了!

完整的代码
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
const Square = (Props) => {
return (
<button
className="square"
onClick={Props.onClick}
>
{Props?.value}
</button>
)
}
// board
class Board extends React.Component {
// square 修改这里的值,我们从父组件拿值
renderSquare (i) {
return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
}
// render
render () {
return (
<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 {
constructor(props) {
super(props)
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
}
}
// 谁赢了?
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;
}
// 点击落子
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 (this.calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
stepNumber: history.length,
});
}
// 跳转去哪儿?
jumpTo = (step) => {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0, // 由于我们第一步的X 先落,所有这里很简单就能计算
});
}
render () {
// 到底是谁赢得了胜利
const history = this.state.history;
const current = history[this.state.stepNumber]; // 这就是我们当前在那儿一步
const winner = this.calculateWinner(current.squares);
// 历史记录
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
// 渲染
let status;
if (winner) {
status = 'Winner: ' + winner;
} 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>
<ol>{moves}</ol>
</div>
</div>
);
}
}
// 挂载点
ReactDOM.render(
<Game />,
document.getElementById('root')
);