学习教程来自官方文档:https://react.docschina.org/tutorial/tutorial.html#what-are-we-building
1st:
npx creact-react-app my-app
小tips:npm与npx的区别
把源代码删掉
cd src
del *
windows命令行使用del *
在src目录下新建文件
touch index.css
touch index.js
在 index.js写入:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
运行网页
cd my-app
npm start
此时应该能看到空白网页
npm查看源地址以及更换源地址
解决npx create-react-app速度过慢的问题
html文件就是自动生成的public路径下的index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React Apptitle>
head>
<body>
<noscript>You need to enable JavaScript to run this app.noscript>
<div id="root">div>
body>
html>
CSS文件 src路径下的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;
}
js文件 src
路径下的index.js
import React form 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Square extends React.Component {
render() {
return (
);
//return后面的内容加括号,防止VScode等编辑器自动在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)}
)
}
}
class Game extends React.Component {
render() {
return (
{ }
{ }
)
}
}
ReactDOM.render(
,
document.getElementById('root')
)
初始代码中有三个React组件
React根据描述把结果展示出来
render方法的返回了一个React元素
上面的代码使用了JSX语法糖
在Board
组件的renderSquare
方法中,改写代码,将名为value
的prop传递到Square
中
renderSquare(i) {
return <Square value={i} />;
}
然后,修改Square
组件的render
方法,在button中加入内容
class Square extends React.Component {
render() {
return(
);
}
}
修改后,在my-app路径下打开终端使用npm start
启动项目
在渲染结果中,可以看到每个方格中都有数字
刚刚修改代码的过程中,把一个prop从父组件Board
传递给了子组件Square
这一步的目标是让棋盘的在点击之后每个格子能落下”X"作为棋子
首先修改Square
组件的render()
方法的返回值中的button标签
增加onClick属性
class Square extends React.Component {
render() {
return (
);
}
}
箭头函数也可以写为
onClick={function(){alert('click'); }}
但是箭头函数可以减少代码量
效果
React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
通过state来实现“记忆”功能
在React组件的构造函数中设置this.state
初始化state
this.state
应被视为一个组件的私有属性。在 this.state
中存储当前每个方格(Square)的值,并且在每次方格被点击的时候改变这个值。
首先,在Square中用构造函数来初始化state
class Square extends React.Component {
constructor(props){
super(props);
this.state = {
value : null.
};
}
render() {
return (
);
}
}
在所有含有构造函数的的 React 组件中,构造函数必须以
super(props)
开头
其次,修改Square组件的render方法,实现每当方格被点击时显示当前state值
方法是在Square组件的render方法中的onClick
事件监听函数中调用this.setState
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
);
}
}
这样子就能实现:在每次被点击的时候通知React去重新渲染
Square
组件
组件更新后,Square
组件的this.state.value
的值会变为'X'
每次在组件中调用setState
,React都会自动更新子组件
井字棋游戏 要放置 "X"和"O"两种棋
为Board组件添加构造函数,将Board组件的初始状态设置为长度为9的空数组值
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
其次
考虑到填充棋盘后,每个格子上可能的形式是null
,O
,X
三种
这些我们打算存储到squares数组里面,那么就要修改Board
的renderSquare
方法来读取这些值
renderSquare(i) {
return ;
}
这样,每个Square
就都能接收到一个 value
prop 了,这个 prop 的值可以是 'X'
、 'O'
、 或 null
(null
代表空方格)。
接着,要修改Square的事件监听函数
renderSquare(i) {
return ( this.handleClick(i)}
/>);
}
再其次,从Board组件向Square组件中传递value
和onClick
两个props
参数
需要修改Square
的代码
class Square extends React.Component {
render(){
return (
);
}
}
当每一个Square
被点击时,Board
提供的onClick
函数就会触发,这是如何实现的呢?
于是,添加handleClick
方法
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
定义一个函数,接收props参数,然后返回需要渲染的元素
把Square组件重写为一个函数组件
function(props)
{
return(
);
}
可以注意到
this.props
被换成了props
箭头函数也变成了
onClick={props.onClick}
将"X"默认设置为先手棋,设置一个布尔值来表示下一步轮到哪个玩家
棋子每移动一步,xIsNext
都会反转,该值确定下一步轮到哪个玩家,并且游戏的状态会被保存下来
在构造函数中添加xIsNext
修改handleClick
函数
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext? 'X':'O';
this.setState({
squares: squares,
xIsNext:!this.state.xIsNext,
});
}
效果
这样子就实现了轮流落子了
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: 5px solid rgb(7, 130, 168); float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 50px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 50px; } .square:focus { outline: none; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; }
显示轮到哪个玩家
在Board组件的render方法中修改status的值
render() {
const status = 'Next player:'+(this.state.xIsNext?'X':'O');
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)}
)
}
效果:
定义一个函数
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;
}
代码中const[a,b,c]=lines[i]
相当于
const a = lines[i][0]; const b = lines[i][1]; const c = lines[i][2];
接着,在Board组件的render方法中调用刚刚那个函数检查是否有玩家胜出,有人胜出就把玩家信息显示出来
修改Board
组件的render
方法
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner:' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
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)}
)
}
效果
此时还有个问题
已经有人胜出了,但是棋盘还能落子
还有个bug,当某个Square落子之后,还能覆盖继续落子
修改handleClick
,使得当有玩家胜出时,或者某个Square被填充时,该函数不做任何处理直接返回
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext? 'X':'O';
this.setState({
squares: squares,
xIsNext:!this.state.xIsNext,
});
}
此时已经实现了井字棋游戏的功能
目前的index.js
代码如下
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// class Square extends React.Component {
// render(){
// return (
//
// );
// }
// }
function Square(props) {
return (
);
}
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();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext? 'X':'O';
this.setState({
squares: squares,
xIsNext:!this.state.xIsNext,
});
}
renderSquare(i) {
return ( this.handleClick(i)}
/>);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner:' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
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)}
)
}
}
class Game extends React.Component {
render() {
return (
{ }
{ }
)
}
}
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 squares[a];
}
}
return null;
}
目标是实现顶层Game组件展示出一个历史步骤的列表
这个功能需要访问history
的数据
故把history
这个state放在顶层Game组件中
首先,在Game组件的构造函数中初始化state
constructor(props) {
super(props);
this.state = {
history:[{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
接着
修改后的Board组件的代码:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext? 'X':'O';
this.setState({
squares: squares,
xIsNext:!this.state.xIsNext,
});
}
renderSquare(i) {
return ( this.props.onClick(i)}
/>);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner:' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
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)}
)
}
}
接着,修改Game
组件的render
函数,用最新的一次历史记录来确定并展示游戏的状态
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
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 (
this.handleClick(i)}
/>
{status}
{ }
)
}
}
因为Game组件渲染了游戏状态,所以可以把Board组件的render方法中的对应代码移除,修改Board组件如下
class Board extends React.Component {
renderSquare(i) {
return (
this.props.onClick(i)}
/>
);
}
render() {
return (
{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
组件中
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,
})
}
可以把历史记录以历史步骤列表的形式展现给玩家
可以通过map方法,把历史步骤映射为代表按钮的React元素
在Game组件的render方法中调用history的map方法
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
-
)
})
对于井字棋历史记录的每一步,都创建出了一个包含按钮 元素的
的列表。这些按钮拥有一个
onClick
事件处理函数,在这个函数里调用了this.jumpTo()
方法
下面就来实现jumpTo()
方法
当需要渲染一个列表时,React会存储这个列表每一项的相关信息
要更新这个列表时,React要确定哪些项发生了改变
要考虑列表的增删改查
React 无法得知我们人类的意图,所以我们需要给每一个列表项一个确定的 key 属性,它可以用来区分不同的列表项和他们的同级兄弟列表项
在井字棋的历史记录中,每一个历史步骤都有一个与之对应的唯一ID:这个ID就是每一步棋的序号
因为历史步骤不需要重新排序、新增、删除,所以使用步骤的索引作为
key
是安全的
添加key
return (
-
)
在Game的构造函数中添加stepNumber这个值来表示我们当前正在查看哪一项历史记录
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
添加jumpTo方法
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext:(step%2)===0,
})
}
修改Game组件的handleClick方法
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方法,将代码改为根据当前stepNumber渲染
const current = history[this.state.stepNumber];
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// class Square extends React.Component {
// render(){
// return (
//
// );
// }
// }
function Square(props) {
return (
);
}
class Board extends React.Component {
renderSquare(i) {
return (
this.props.onClick(i)}
/>
);
}
render() {
return (
{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)}
);
}
}
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 (
-
)
})
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
this.handleClick(i)}
/>
{status}
{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 squares[a];
}
}
return null;
}
效果