Navigation
阅读进度0%
No headings found.

React 入门教程:构建井字棋游戏

December 19, 2024 (1y ago)

React
JavaScript
Frontend

官方入门级别教程,完成一个小游戏,带你了解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')
);