安装
使用Create React App,最好的方式,但是必须node>6的版本
全局安装
npm install -g create-react-app
create-react-app my-app
cd my-app
npm start
删除掉生成项目中
src/
文件夹下的所有文件。在
src/
文件夹下新建一个名为index.css
的文件并拷贝 这里的 CSS 代码 到文件中。在
src/
文件夹下新建一个名为index.js
的文件并拷贝 这里的 JS 代码 到文件中, 并在此文件的最开头加上下面几行代码:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
接下来通过命令行在你的项目目录下运行 npm start 命令并在浏览器中打开 http://localhost:3000 你就能够看到空的井字棋棋盘了
开始编码
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Square extends React.Component {
render() {
return (
<button className="square">
{/* TODO */}
</button>
);
}
}
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 />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
讲得更具体一点,我们现在有3个组件:
Square
Board
Game
Square 组件代表一个单独的 <button>,Board 组件包含了9个squares,也就是棋盘的9个格子。Game 组件则为我们即将要编写的代码预留了一些位置。现在这几个组件都是不具备任何的交互功能的
通过 Props 传递数据
我们先来试着从 Board 组件传递一些数据到 Square 组件。
在 Board 组件的 renderSquare 方法中,我们将代码改写成下面这样,传递一个名为 value 的 prop 到 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>
);
}
}
给组件添加交互功能
接下来我们试着让棋盘的每一个格子在点击之后能落下一颗 “X” 作为棋子。我们试着把 render() 方法修改为如下内容:
现在你试着点击一下某个格子,在浏览器里就会弹出一个警示框。
在 React 组件的构造方法 constructor 当中,你可以通过 this.state 为该组件设置自身的状态数据。我们来试着把棋盘格子变化的数据储存在组件的 state 当中吧:
首先,我们为组件添加构造函数并初始化 state:
lass Square extends React.Component {
constructor(){
super()
this.state = {
value:null,
}
},
render() {
return (
<button className="square" onClick={()=>alert('click')}>
{this.props.value}
</button>
);
}
}
现在我们试着通过点击事件触发 state 的改变来更新棋盘格子显示的内容:
将 <button> 当中的 this.props.value 替换为 this.state.value 。
将 () => alert() 方法替换为 () => this.setState({value: 'X'}) 。
现在我们的 <button> 标签就变成了下面这样:
class Square extends React.Component {
constructor(){
super()
this.state = {
value:null,
}
}
render() {
return (
<button className="square" onClick={()=>this.setState({value: 'X'})}>
{this.state.value}
</button>
);
}
}
每当 this.setState 方法被触发时,组件都会开始准备更新,React 通过比较状态的变化来更新组件当中跟随数据改变了的内容。当组件重新渲染时,this.state.value 会变成 'X' ,所以你也就能够在格子里看到 X 的字样。
现在你试着点击任何一个格子,都能够看到 X 出现在格子当中。
开发工具
在 Chrome 或 Firefox 上安装 React 开发者工具可以让你在浏览器的开发控制台里看到 React 渲染出来的组件树。
你同样可以在开发工具中观察到各个组件的 props 和 state.
安装好开发工具之后,你可以在任意页面元素上面右键选择 “审查元素”,之后在弹出的控制台选项卡最右边会看到名为 React 的选项卡。
状态提升
我们现在已经编写好了井字棋游戏最基本的可以落子的棋盘。但是现在应用的状态是独立保存在棋盘上每个格子的 Square 组件当中的。想要编写出来一个真正能玩的游戏,我们还需要判断哪个玩家获胜,并在 X 或 O 两方之间交替落子。想要检查某个玩家是否获胜,需要获取所有9个格子上面的棋子分布的数据,现在这些数据分散在各个格子当中显然是很麻烦的。
最好的解决方式是直接将所有的 state 状态数据存储在 Board 组件当中。之后 Board 组件可以将这些数据传递给各个 Square 组件
当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中的状态数据就能够更方便地交流共享了
像这种提升组件状态的情形在重构 React 组件时经常会遇到。我们趁现在也就来实践一下,在 Board 组件的构造函数中初始化一个包含9个空值的数组作为状态数据,并将这个数组中的9个元素分别传递到对应的9个 Square 组件当中。
constructor(){
super()
this.state = {
squartes: Array(9).fill(null)
}
}
现在传入的都是空数据,井字棋游戏进行会把数组填充成类似下面这样:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
我们在 value 属性中传递对应 state 数组元素的值。
renderSquare(i) {
return <Square value={this.state.squares[i]}/>;
}
现在我们需要修改当某个格子被点击时触发的事件处理函数。现在每个格子当中的数据是存储在整个棋盘当中的,所以我们就需要通过一些方法,让格子组件能够修改整个棋盘组件数据的内容。因为每个组件的 state 都是它私有的,所以我们不可以直接在格子组件当中进行修改。
惯例的做法是,我们再通过 props 传递一个父组件当中的事件处理函数到子组件当中。也就是从 Board 组件里传递一个事件处理函数到 Square 当中,我们来把 renderSquare 方法改成下面这样
renderSquare(i) {
return <Square
value={this.state.squares[i]}
onClick={()=>this.handleClick(i)}
/>;
}
注意到我们在写代码的时候,在各个属性直接换了行,这样可以改善我们代码的可读性。并且我们在 JSX 元素的最外层套上了一小括号,以防止 JavaScript 代码在解析时自动在换行处添加分号。
现在我们从 Board 组件向 Square 组件中传递两个 props 参数:value 和 onClick. onClick 里传递的是一个之后在 Square 组件中能够触发的方法函数。我们动手来修改代码吧:
将 Square 组件的 render 方法中的 this.state.value 替换为 this.props.value 。
将 Square 组件的 render 方法中的 this.setState() 替换为 this.props.onClick() 。
删掉 Square 组件中的 构造函数 constructor ,因为它现在已经不需要保存 state 了。
进行如上修改之后,代码会变成下面这样:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => this.props.onClick()}>
{this.props.value}
</button>
);
}
}
现在每次格子被点击时就会触发传入的 onClick 方法。我们来捋一下这其中发生了什么:
1.添加 onClick 属性到内置的 DOM 元素 <button> 上让 React 开启了对点击事件的监听。
2.当按钮,也就是棋盘格子被点击时, React 会调用 Square 组件的 render() 方法中的 onClick 事件处理函数。
3.事件处理函数触发了传入其中的 this.props.onClick() 方法。这个方法是由 Board 传递给 Square 的。
4.Board 传递了 onClick={() => this.handleClick(i)} 给 Square,所以当 Square 中的事件处理函数触发时,其实就是触发的 Board 当中的 this.handleClick(i) 方法。
5.现在我们还没有编写 handleClick() 方法,所以代码还不能正常工作。
注意到这里的 onClick 事件是 React 组件当中所特有的。不过 handleClick 这些方法则只是我们编写事件处理函数时候的命名习惯。
现在我们来动手编写 handleClick 方法吧:
handleClick(i) {
//slice() 方法可从已有的数组中返回选定的元素 创建和state.squares一样的数组
const squares = this.state.squares.slice()
squares[i] = 'X'
this.setState({squares: squares})
}
我们使用了 .slice()
方法来将之前的数组数据浅拷贝到了一个新的数组中,而不是修改已有的数组。你可以在 这个章节 来了解为什么不可变性在 React 当中的重要性。
现在你点击棋盘上的格子应该就能够正常落子了。而且状态数据是统一保管在棋盘组件 Board 当中的。你应该注意到了,当事件处理函数触发棋盘父组件的状态数据改变时,格子子组件会自动重新渲染。
现在格子组件 Square 不再拥有自身的状态数据了。它从棋盘父组件 Board 接受数据,并且当自己被点击时通知触发父组件改变状态数据,我们称这类的组件为 受控组件。
为什么不可变性在React当中非常重要
在上一节内容当中,我们通过使用 .slice() 方法对已有的数组数据进行了浅拷贝,以此来防止对已有数据的改变。接下来我们稍微了解一下为什么这样的操作是一种非常重要的概念。
改变应用数据的方式一般分为两种。第一种是直接修改已有的变量的值。第二种则是将已有的变量替换为一个新的变量。
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}
替换修改数据
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}
// 或者使用最新的对象分隔符语法,你可以这么写:
// var newPlayer = {...player, score: 2};
函数定义组件
我们刚才已经去掉了 Square 的构造函数,事实上,更进一步的,React 专门为像 Square 组件这种只有 render 方法的组件提供了一种更简便的定义组件的方法: 函数定义组件 。只需要简单写一个以 props 为参数的 function 返回 JSX 元素就搞定了。
下面我们以函数定义的方式重写 Square 组件:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
记得把所有的 this.props 替换成参数 props. 我们应用中的大部分简单组件都可以通过函数定义的方式来编写,并且 React 在将来还会对函数定义组件做出更多优化。
另外一部分简化的内容则是事件处理函数的写法,这里我们把 onClick={() => props.onClick()} 直接修改为 onClick={props.onClick} , 注意不能写成 onClick={props.onClick()} 否则 props.onClick 方法会在 Square 组件渲染时被直接触发而不是等到 Board 组件渲染完成时通过点击触发,又因为此时 Board 组件正在渲染中(即 Board 组件的 render() 方法正在调用),又触发 handleClick(i) 方法调用 setState() 会再次调用 render() 方法导致死循环。
轮流落子
很明显现在我们点击棋盘只后落子的只有 X 。 下面我们要开发出 X 和 O 轮流落子的功能。
我们将 X 默认设置为先手棋:
class Board extends React.Component {
constructor() {
super();
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
接下来,我们每走一步棋,都需要切换 xIsNext 的值以此来实现轮流落子的功能,接下来在 handleClick 方法中添加修改 xIsNext 的语句
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
到这里我们就实现了 X 和 O 轮流落子的效果了。我们再到 render 方法里添加一点内容来显示当前执子的一方
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// the rest has not changed
判断赢家
接下来我们来编写判断游戏获胜方的代码,首先在你的代码里添加下面这个判断获胜方的算法函数
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 组件的 render 方法里调用它,来检查是否有人获胜并根据判断显示出 “Winner: [X/O]” 来表示获胜方。
将 render 中的 status 替换为如下内容:
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
// the rest has not changed
继续完善游戏规则,我们在 handleClick 里添加当前方格内已经落子/有一方获胜就就无法继续落子的判断逻辑
handleClick(i) {
const squares = this.state.squares.slice()
if(this.calculateWinner(squares) || squares[i]) return
squares[i] = this.state.xIsNext ? 'X' : 'O'
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
})
}
保存历史记录
接下来我们一起来实现保存棋局每一步的历史记录的功能。在现有的代码逻辑中,我们已经是在每走一步棋之后就返回一个新的 squares 数组了,所以想要保存历史记录也非常简单。
我们计划通过一个数组对象来保存每一步的状态数据
我们期望在顶层的 Game 组件中展示一个链接每一步历史记录的列表。所以就像我们之前将 state 从 Square 组件提升到 Board 中一样,现在我们把 Board 中的状态数据再提升到 Game 组件中来。
首先在 Game 组件的构造函数中初始化我们需要的状态数据
class Game extends React.Component {
constructor(){
super()
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
接下来,就好像我们之前对 Square 组件的操作一样。我们将 Board 中的状态数据全都移动到 Game 组件当中。Board 现在通过 props 获取从 Game 传递下来的数据和事件处理函数。
1.删除 Board 的构造方法 constructor 。
2.把 Board 的 renderSquare 方法中的 this.state.squares[i] 替换为 this.props.squares[i] 。
3.把 Board 的 renderSquare 方法中的 this.handleClick(i) 替换为 this.props.onClick(i) 。
将calculateWinner提到外面公用的
Game 组件的 render 方法现在则要负责获取最近一步的历史记录(当前棋局状态),以及计算出游戏进行的状态(是否有人获胜)。
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = 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>
);
}
既然现在由 Game 组件负责渲染游戏状态,我们可以直接把 Board 组件的 render 方法里的 <div className="status">{status}</div> 删掉:
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>
);
}
之后,我们需要将 Board 组件里的 handleClick 移动到 Game 组件当中。你可以直接把它剪切粘贴过来。
不过为了实现我们新的历史记录的功能,还需要稍微修改一下我们的代码,让 handleClick 在每次触发时,添加当前的棋局状态数据到 histroy 当中
handleClick(i) {
const history = this.state.history;
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
}]),
xIsNext: !this.state.xIsNext,
});
}
代码编写到这一步,Board 组件当中现在应该只有 renderSquare 和 render 两个方法;应用状态 state 以及事件处理函数现在都定义在 Game 组件当中。
完成代码
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>
)
}
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;
}
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),
}],
xIsNext: true,
};
}
handleClick(i) {
const history = this.state.history
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
}]),
xIsNext: !this.state.xIsNext
})
}
render() {
const history = this.state.history
const current = history[history.length - 1]
const winner = calculateWinner(current.squares)
let status
if(winner) {
status = 'Winner: ' + winner
}else {
status = 'Nest 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>
);
}
}
ReactDOM.render(
<Game />,
document.getElementById('root')
);
展示每步历史记录链接
现在我们来试着展示每一步棋的历史记录链接。在教程的开始我们提到过,React 元素事实上都是 JS 当中的对象,我们可以把元素当作参数或定义到变量中使用。在 React 当中渲染多个重复的项目时,我们一般都以数组的方式传递 React 元素。最基本的方法是使用数组的 map 方法,我们试着来修改 Game 组件的 render 方法吧:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li>
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</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>
);
}
对于每一步的历史记录,我们都创建了一个带 <a> 链接的 <li> 列表项。目前链接还没指向任何地方,别着急我们后面会继续实现切换至对应棋步的功能。有了我们现有的代码,已经能渲染出一个列表了,不过你留心的话,就会在控制台看到警告:
Keys
当你在 React 当中渲染列表项时,React 都会试着存储对应每个单独项的相关信息。如果你的组件包含 state 状态数据,那么这些状态数据必须被排序,不管你组件是怎么编写实现的。
当你想要更新这些列表项时,React 必须能够知道是那一项改变了。这样你才能够在列表中增删改查项目。
比方说下面这个例子,从前一个表单
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
变成下面这个表单
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
你用肉眼可以很轻易地分辨,Alexa 被移到了最后,多出来一个 Claudia。可是 React 只是电脑里运行地程序,它无从知晓这些改变。所以我们必须为列表中的每一项添加一个 key 作为唯一的标识符。标识符必须是唯一的,比方说刚才这个例子中的 alexa, ben, claudia 就可以用来做标识符。更普遍的一种情况,假如我们的数据是从数据库获取的话,表单每一项的 ID 就很适合当作它的 key :
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
强烈建议你在渲染列表项时添加 keys 值。 假如你没有现成可以作为唯一 key 值的数据使用的话,你可能需要考虑重新组织设计你的数据了。
实现时间旅行
在我们的棋步的列表中,已经有了现成的唯一 key 值,也就是每一次 move 的记录值。我们通过 <li key={move}> 来添加一下。
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li key={move}>
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</li>
);
});
在上面的代码中,我们同样为每一个 <a> 添加了一个 jumpTo 方法,用来将棋盘的状态切换至对应的棋步时的状态。接下来我们来着手实现这个方法:
首先在 Game 组件的初始状态中多设置一项 stepNumber: 0
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
接下来,我们正是编写 jumpTo 来切换 stepNumber 的值。根据游戏的逻辑,与此同时我们还需要修改 xIsNext 来保证对应棋步时,执子的一方是能对应上的。我们可以根据棋步计算出是谁在执子。
我们把 jumpTo 编写在 Game 组件中:
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) ? false : true,
});
}
接下来,我们在 handleClick 方法中对 stepNumber 进行更新,添加 stepNumber: history.length 保证每走一步 stepNumber 会跟着改变:
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,
});
}
现在你可以直接在 Game 组件的 render 方法里根据当前的棋步获取对应的棋局状态了:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
现在你试着点击每一步棋记录的列表中的一项,棋盘会自动更新到对应项时的棋局状态
总结,招帮官网文档来的,算是自我学习的一种方式