首页 > 其他分享 >React 井字棋

React 井字棋

时间:2023-04-07 15:13:26浏览次数:34  
标签:井字棋 棋盘 const handleClick React Square 组件 squares

React 井字棋

参考 React 的文档,用 React 搞个井字棋。代码实现主要还是参考的文档,不过也在原有的基础上也做了点优化和美化。

原型

先看原型的构成(其实是最终做完的效果,暂且当原型用):

且页面的 HTML 结构和 CSS 样式已经完成:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello Qiyuanc!</title>

<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> 
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> 
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> 
<script type="text/babel" src="./game.js"></script>
<link rel="stylesheet" type="text/css" href="game.css">
</head>
<body>

<div id="root"></div>

<script type="text/babel">

    ReactDOM.render(<Game />, document.getElementById('root'))

</script>

</body>
</html>
/* 游戏界面样式 */
body {
  background-color: #fafafa;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  /*background-image: url('./GimRt.jpg');*/
  background-size: cover;
  /*https://ss.im5i.com/2021/08/02/G8uZy.jpg*/
}

#root {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.container {
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.game {
  border: 2px solid #ccc;
  padding: 20px;
  margin-bottom: 20px;
}

.board {
  display: flex;
  flex-wrap: wrap;
  width: 240px;
  height: 240px;
  margin: 0 auto;
  border: 2px solid #ccc;
  border-radius: 5px;
}

.square {
  width: 80px;
  height: 80px;
  border: 1px solid #ccc;
  text-align: center;
  font-size: 3rem;
  line-height: 80px;
  font-weight: bold;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.square:focus,
.square:hover {
  background-color: #f2f2f2;
}

/* 按钮样式 */
button {
  background-color: #4caf50;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1.2rem;
  margin: 4px;
  margin-right: 10px;
}

button:hover {
  background-color: #3e8e41;
}

button:disabled {
  background-color: #aaa;
  cursor: not-allowed;
}

下面就可以开始对页面的搭建了。

组件

其中,页面有一个游戏区域(方框区域),其中有井字棋棋盘和下方的回溯按钮(每走一步就多一个,可以回到之前的某一步);棋盘中的每个位置也可以看作由一个个方格组件构成。进行组件的拆分,可以分解为:

  1. 方格组件 Square,点击这个组件就相当于在此位置落子;
  2. 棋盘组件 Board,由九个方格组件构成棋盘,并且在上方显示当前落子方;
  3. 历史记录组件 Record,是一个包含了回溯按钮的列表,点击按钮可以回到之前某一步;
  4. 游戏组件 Game,程序最大的组件,包含其他组件,构成游戏。

分解完组件,再分析一下状态(本来应该先根据组件实现静态页面,再寻找状态添加交互的)。
根据状态的寻找思路(随时间会改变且不可计算),我们可以很快发现,随着时间推进,玩家在棋盘上落子,棋盘的布局会发生改变,且这种改变不可被计算,因此棋盘的布局属于状态。
同时,由于有历史记录功能,随着时间推进,每次落子都会更新历史记录,所以当前步数也是状态。回溯功能就是通过改变当前步数,触发重新渲染实现回溯。
也因为需要回溯,因此我们需要保存的不仅是当前棋盘,而是每一步的棋盘,既然如此,我们可以直接将棋盘的历史记录作为状态:

  1. 状态 history,是棋盘的历史记录,结构为一个数组,其中的每个元素都是某一步的棋盘布局;
  2. 状态 currentMove,是当前步数,记录当前走到第几步,对应 history 中这一步的棋盘布局。

对于这种小程序,流程应该是自底向上先实现静态页面,再添加交互,不过这样的流程有点长,官方文档也写的够详细,我就直接从完成的组件写,记录一下其中比较重要的地方好了。

方格组件

方格组件实现比较简单:

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

Square 组件内部由按钮构成,应用了 square 样式(不然就是一个很丑的按钮)。这是已经完成了自底向上数据流的组件,所以直接看起来会有点奇怪,但还好要素比较少。
其中,接收的 props 为当前方格的棋子(value)和在当前位置落子时触发的方法(onSquareClick)。value 仅用于显示,是自顶向下流到 Square 组件的数据,告知 Square 它当前位置是什么内容,onSquareClick 是自顶向下传递给 Square 组件的方法,这个方法的作用是为组件提供反向数据流,使得 Square 组件能将变化反馈给高层次的组件,触发重新渲染。

棋盘组件

棋盘组件的实现稍稍有点复杂:

function Board({ xIsNext, squares, onPlay }) {
  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;
  }

  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    // 在 React 中,状态(state)对象和属性(props)对象都应该被视为不可变的(immutable)
    // slice() 方法创建一个副本,slice 为浅拷贝(若元素为对象,则创建一个引用指向相同的对象),但此处是基本数据类型,因此修改新数组不会影响原数组
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    onPlay(nextSquares);
  }

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </div>
  );
}

在 props 中,xIsNext 表明下一步是 O 还是 X,squares 为当前的棋盘布局,是一个具有9个元素的数组,对应棋盘的9个位置。这两个参数都是由上层组件传递过来,Board 组件需要用到的。当棋盘上的某一个方格被点击时,通过将 squares 中对应的位置设置为 O 或 X,就改变了棋盘的布局。
但这个改变需要反馈给上层组件,让上层组件传递新的参数触发重新渲染才行。这就是 onPlay 方法的工作,构建了 Board 到其上层组件的反向数据流。

此处有一个关键点:即

<Square value={squares[0]} onSquareClick={() => handleClick(0)} />

中传递给 Square 组件的方法是 () => handleClick(0),这是一个箭头函数,与

function() {
    handleClick(0)
}

类似,但箭头函数不会创建新的作用域,因此才能直接调用到 handleClick。此处如果直接写

<Square value={squares[0]} onSquareClick={handleClick(0)} />

则执行时会报错,因为传递的不是一个方法,而是 handleClick(0) 执行的返回值了。

将这个方法传递给 Square 组件,点击 Square 组件时会触发该方法,在 handleClick 中,通过 slice() 方法复制之前的棋盘布局,然后根据当前点击的方格 i 和下一步的落子 xIsNext 创造出一个最新落子的棋盘,再通过向上的数据流 onPlay 将这个新数据交给上层组件。

历史记录组件

历史记录组件也比较简单:

function Record({ history , jumpTo }) {
  // 遍历数组创建回溯按钮列表, 数组.map((元素,下标) => function(...))
  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }

    return (
        <li key={move}>
          <button onClick={() => jumpTo(move)}>{description}</button>
        </li>
    );
  });

  return (
    <ol>
      {moves}
    </ol>
  )

}

接收的 props 为棋盘的历史,和一个能改变当前步数的方法。棋盘的历史用于创建回溯按钮列表,有多少种棋盘就有多少个按钮;由于当前步数状态并不由此组件掌控,因此需要上层组件给出改变当前步数的入口,即 jumpTo 方法,这也是反向数据流的一部分。

游戏组件

到了最大的游戏组件了,这个组件虽然层次最高,但不算最复杂,它只要管理状态和传递数据/方法就可以了:

function Game() {
  // 状态:棋盘和步数都随时间(操作)变化
  // history 二维数组,其中的每个一维数组都是某一时刻的棋盘
  const [history, setHistory] = React.useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = React.useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  // 通过改变 棋盘历史和当前步数 的状态,触发对页面的重新渲染
  function handlePlay(nextSquares) {
    // 截取 历史棋盘 下标从 0 到 currentMove 的子数组,与最新的棋盘组成新的 历史棋盘
    // 如 当前下完了第2步,此时 currentMove 还是 1(set在之后),因此截取历史棋盘的 0、1 与当前的 2 组成新历史棋盘
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  // 通过改变 当前步数 的状态,触发对页面的重新渲染
  function jump(nextMove) {
    // 回到第0步,Remake棋盘
    if(nextMove === 0) {
      setHistory([Array(9).fill(null)])
    }
    setCurrentMove(nextMove);
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <Record history={history} jumpTo={jump} />
      </div>
    </div>
  );
}

这个组件的核心在于创建了两个状态,并将要渲染的数据和改变状态的入口自顶向下传递,下层组件利用数据渲染自己负责的部分,同时可以调用改变状态的方法触发数据的重新渲染。

最终效果

我感觉还不错,但这篇东西我写的确实是依托谢特,也就我自己看得懂。

标签:井字棋,棋盘,const,handleClick,React,Square,组件,squares
From: https://www.cnblogs.com/qiyuanc/p/Front8.html

相关文章

  • React的行内样式与CSS
    如何为组件添加CSS的class?传递一个字符串作为className属性:render(){return<spanclassName="menunavigation-menu">Menu</span>}CSS的class依赖组件的props或state的情况很常见:render(){letclassName='menu';if(this.props.isActive)......
  • 浅谈React与SolidJS对于JSX的应用
    React将JSX这一概念深入人心。但,并非只有React利用了JSX,VUE、SolidJS等JS库或者框架都使用了JSX这一概念。网上已经有大量关于JSX的概念与形式的讲述文章,不在本文的讨论范围。前言实际上,JSX并不是合法有效的JS代码或HTML代码。目前为止也没有任何一家浏览器的引擎实现了对JSX的......
  • 第一节:react简介和入门用法
    一.        二.        三.         !作       者:Yaopengfei(姚鹏飞)博客地址:http://www.cnblogs.com/yaopengfei/声     明1:如有错误,欢迎讨论,请勿谩骂^_^。声     明2:原创博客请在转载......
  • ReactDemo - todolist
    (ES6书籍推荐:ES6书籍React中文:http://react-china.org/React中文文档:https://react.docschina.org/DevDocs-开发文档网站:https://devdocs.io/)下面是webstorm打开看到的效果(自动建立的文件删了)public/index.html是一个入口,index.js是这个入口的js文件components是使用到的组件......
  • React父组件调用子组件属性和方法
    子组件暴露自身的属性和方法父组件使用ref绑定对应的子组件。调用即可类组件绑定ref示例importReactfrom'react'importChildfrom'./Child'exportdefaultclassParentextendsReact.Component{//...render(){return(<div><Childre......
  • reactive
    reactivereactive系统有些特性成棒为低延时,高通工载.项目reactor和spring套装共事使开发亻建企业级reactive系统是响应,恢复,弹性,消息驱动的.什是reactive处理?reactive处理是范例使开发亻建非阻,异步app可拿捏背压(流控)为什用reactive处理?reactive系统更好使用当下处理......
  • React Native学习笔记(三)—— 组件
    一、ReactNative项目1.1、创建ReactNative项目ReactNative有一个内置的命令行界面,你可以用它来生成一个新项目。您可以使用Node.js附带的访问它,而无需全局安装任何内容。让我们创建一个名为“AwesomeProject”的新ReactNative项目:npxnpxreact-native@latestinitAw......
  • 在react中使用wangEditorV5
    wangEditor是基于JavaScript和css的一款web富文本编辑器,是国内比较好用的一款轻量级富文本编辑器,上手简单,易用且开源免费.官方文档:http://www.wangeditor.com/本文讲述的是在react中如何去使用这款富文本编辑器首先引入编辑器yarnadd@wangeditor/editor-for-reactnpmi......
  • 一文搞定:前端如何选择Angular、React和Vue三大主流框架
    在前端开发领域,目前最流行的三个框架是Angular、React和Vue.js。这些框架非常高效,并且它们各自具有一系列的优缺点。在AI辅助编程工具CodeGeeX的后台中,也看到有大量的前端开发者使用这三个框架,并且Vue的使用率在CodeGeeX的后台中,持续走高。接下来我们针对Angular、React和Vue.js......
  • React Native 开发环境搭建
    一、React Native介绍二、开发环境的搭建2.1、Node.js安装Node.js要求14版或更新https://nodejs.org/en 查看版本:2.2、yarn安装2.3、react-native-cli安装安装项目:命令:2.4、下载Chocolatey包管理器在Linux下,大家喜欢用apt-get来安装应用程序,如今在windows下......