官网有详细的初始环境配置过程,可以对比这篇教材详解进行学习:官网入门教程跳转链接
一个 package 管理器,比如 Yarn 或 npm。
它能让你充分利用庞大的第三方 package 的生态系统,并且轻松地安装或更新它们。
一个打包器,比如 webpack 或 Parcel。
它能让你编写模块化代码,并将它们组合在一起成为小的 package,以优化加载时间。
一个编译器,例如 Babel。
它能让你编写的新版本 JavaScript 代码,在旧版浏览器中依然能够工作。
1、开启环境
npx create-react-app my-app
cd my-app
npm start
删除src里面的内容,保留文件夹
cd my-app #找到文件位置
cd src # 如果你使用 Windows:
del * #删除掉
cd .. #返回项目文件
在 src/ 文件夹中创建一个名为 index.css 的文件,并拷贝这些CSS 代码 。
在 src/ 文件夹下创建一个名为 index.js 的文件,并拷贝这些 JS 代码。
拷贝以下三行代码到 src/ 文件夹下的 index.js 文件的顶部:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
现在,在项目文件夹下执行 npm start 命令,然后在浏览器访问 http://localhost:3000 。
这样你就可以在浏览器中看见一个空的井字棋的棋盘了。
注意,这里只要更改了js或者css,代码会自动重新运行渲染。
2、解释代码运行原理
状态提升的思想
每个 Square 组件都维护了游戏的状态。我们可以把所有 9 个 Square 的值收集起来,用来判断游戏的优胜者。
如果在棋盘 Board 组件中一个个收集每个格子 Square 组件中的 state。虽然技术上来讲可以实现,但是代码如此编写会让人很难理解,并且我们以后想要维护重构时也会非常困难。因为state分布在各个square里面,就会嵌套得很复杂。
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>
);
}
}
class Board extends React.Component { //这个是父组件
renderSquare(i) { return <Square />; } //就是为了能够把位置信息i给传递给下一个组件
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')
);
最好的解决方式是直接将所有的 state 状态数据存储在 Board 父组件当中。之后 Board 组件可以将这些数据通过 props (不可变)传递分发给各个 Square 子组件,正如上文我们把数字传递给每一个 Square 一样。
当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。
专门解释props和state的文章:下面是完好的的代码JS部分
import React from 'react'; //导入需要的库 **
import ReactDOM from 'react-dom';
import './index.css';
function Square(props) { //(子组件)每次调用这个函数组件 ,就会实现(把点击后的位置显示为传入值)的功能
return ( <button className="square" onClick={props.onClick}> {props.value} </button> );
} //返回一个按钮,如果点击下去
class Board extends React.Component { //父组件
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, //实现反转的布尔值
};
}
handleClick(i) {
const squares = this.state.squares.slice(); //slice出新数组
if (calculateWinner(squares) || squares[i]) { return; } //判断胜负,已经赢了直接跳过。没有再改变点击的属性
squares[i] = this.state.xIsNext ? 'X' : 'O'; //直接改变点击后需要反转的值,是X还是O
this.setState({ //然后把新的数组更新到旧数组去,反转判断的布尔值也改变
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
//这里一个是被动改变的值,收集9个以后用来判断输赢。另一个是主动的鼠标点击事件,改变state的OX和前面那个输赢判断值
renderSquare(i) { //返回一个棋子,然后传入值(来自父组件的向下分发),传入点击事件处理函数,
return ( //如果不传这些,子组件无法使用父组件函数,所以只能在这里通过props来向下传递分发
<Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />
);
}
render() {
const winner = calculateWinner(this.state.squares); //先传入输赢判断的依据去判断输赢
let status; //定义出提示文本,下面return出提示文本的内容
if (winner) { status = 'Winner: ' + winner; }
else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O' );
}
return ( //这里是基本的井字棋布局,提示信息+9宫格
<div>
<div className="status">{status}</div>
<div className="board-row"> //下面传state也可以,直接传函数,拿子组件去渲染也可以,不过传子组件配参数,应该ok
{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'));
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]; //里面的值是X或者O,所以可以直接返回出来,就知道谁是赢家了
}
}
return null;
}
到此为止,已经完成了基本功能
1、一开始从结构入手,不考虑数据流向,,选择使用父组件渲染九宫格期盼,子组件定义棋子的所有信息,外表交给css来完成。
2、后来考虑数据流向,在子组件上收集state信息进行胜负判断嵌套严重,所以把state的状态提升到父组件,通过父组件下发state给子组件。在父组件中进行胜负判断,而子组件只要负责点击事件和显示(在按钮处即可实现)即可,在不需要state的时候,就可以用函数形式来写子组件。
加入悔棋功能,甚至可以跳转到任意时间段
import React from 'react'; //导入需要的库 **
import ReactDOM from 'react-dom';
import './index.css';
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
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
};
}
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 } ]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = 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"));
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;
}