井字棋游戏
本教程将引导你逐步实现一个简单的井字棋游戏,并且不需要你对 React 有任何了解。
在此过程中你会学习到一些编写 React 程序的基本知识,完全理解它们可以让你对 React 有比较深入的理解。
教程分成以下几个部分:
- 配置 是一些准备工作。
- 概览 介绍了 React 的 基础知识:组件、props 和 state。
- 完成游戏 介绍了 React 开发中 最常用的技术。
- 添加“时间旅行” 可以让你更深入地了解 React 的独特优势
实现的是什么程序?
本教程将使用 React 实现一个交互式的井字棋游戏。
你可以在下面预览最终成果:
如果你还不是很明白上面的代码,不用担心!本教程的目的就是帮你理解 React 及其语法。
我们建议你在继续本教程之前,先看看上面的井字棋游戏。我们会注意到的一项功能是,棋盘右侧有一个编号列表,它记录了游戏中落子的历史,并随着游戏的进行而更新。
体验完游戏以后,继续阅读本教程吧!我们将从一个更简单的模板开始。下一步将介绍相关配置,以便于你着手实现这个游戏。
1. 配置
官网上在在线浏览器里面提供了代码沙盒环境,但是为了自己能随时编辑和查看,我们还是把代码放进了自己的仓库
拉取代码: git clone https://github.com/www-chengxuyuancd-com/react-dev.git
或者fork到自己的仓库:https://github.com/www-chengxuyuancd-com/react-dev.git
cd react-dev/game-1
进入到本次的教程,并切换到master
分支git checkout master
看见我为你准备的代码
依次安装:npm install
运行: npm run dev
启动后如果看到如下的图片,则说明你配置成功了:
解释下上述部分代码:
1.1 App.jsx
文件解释
此代码创建了一个组件。
在 React 中,组件是一段可重用代码,它通常作为 UI 界面的一部分。
组件用于渲染、管理和更新应用中的 UI 元素。
让我们逐行查看这段代码,看看发生了什么:
export default function Square() {
return <button className="square">X</button>;
}
第一行定义了一个名为 Square 的函数。
JavaScript 的 export 关键字使此函数可以在此文件之外访问。
default 关键字表明它是文件中的主要函数。
第二行返回一个按钮。
JavaScript 的 return 关键字意味着后面的内容都作为值返回给函数的调用者。
<button>
是一个 JSX 元素。
JSX 元素是 JavaScript 代码和 HTML 标签的组合,用于描述要显示的内容。
className="square"
是一个 button 属性,它决定 CSS 如何设置按钮的样式。
X 是按钮内显示的文本,</button>
闭合 JSX 元素以表示不应将任何后续内容放置在按钮内。
1.2 index.css
文件解释
该文件定义了 React 应用的样式。
前两个 CSS 选择器(* 和 body)定义了应用大部分的样式,而 .square 选择器定义了 className 属性设置为 square 的组件的样式。
这与 App.jsx
文件中的 Square 组件中的按钮是相匹配的。
1.3 main.jsx
文件解释
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
第 1-5 行将所有必要的部分组合在一起:
- React
- React 与 Web 浏览器对话的库(React DOM)
- 需要引入
App.jsx
里面的App组件 - 组件的样式 其他文件将它们组合在一起,并将最终成果注入 public 文件夹里面的 index.html 中。
2. 构建棋盘
让我们回到 App.jsx
, 接下来我们将专注于这个文件。
目前棋盘只有一个方块,但你需要九个!如果你只是想着复制粘贴来制作两个像这样的方块:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
你将会得到如下错误:
JSX expressions must have one parent element.
React 组件必须返回单个 JSX 元素,不能像两个按钮那样返回多个相邻的 JSX 元素。
要解决此问题,可以使用 Fragment( 与 )包裹多个相邻的 JSX 元素,如下所示:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
如果我们继续复制来添加9个方块:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
可以看到如下效果:
显然不是我们期望的结果,我们期望是类似"井"字的正方形格式排列
此时可以引用index.css
里面的board-row
样式:
.board-row:after {
clear: both;
content: '';
display: table;
}
应用到之前的代码中,将组件分到每一行的 div 中。最终完成了井字棋棋盘
但是现在有个问题,名为 Square 的组件实际上不再是方块了。
我们通过将名称更改为 Board 来解决这个问题
export default function Board() {
//...
}
此时你的代码应如下所示
export default function Board() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}
最终效果参见 game-1-build-board-1
分支:git checkout game-1-build-board-1
, 注意commit也体现了我的开发过程,可以参考学习下
3. 通过 props 传递数据
接下来,当用户单击方块时,我们要将方块的值从空更改为“X”。
根据目前构建的棋盘,你需要复制并粘贴九次更新方块的代码(每个方块都需要一次)!
但是,React 的组件架构可以创建可重用的组件,以避免混乱、重复的代码。
首先,要将定义第一个方块(<button className="square">1</button>
)的这行代码从 Board 组件复制到新的 Square 组件中:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
然后,更新 Board 组件并使用 JSX 语法渲染 Square 组件:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
需要注意的是,这并不像 div,这些你自己的组件如 Board 和 Square,必须以大写字母开头。
最终代码:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
效果:
但你失去了你以前有正确编号的方块。现在每个方块都写着“1”。
要解决此问题,需要使用 props 将每个方块应有的值从父组件(Board)传递到其子组件(Square)。
更新 Square 组件,读取从 Board 传递的 value props:
function Square({value}) {
return <button className="square">{value}</button>;
}
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1"/>
<Square value="2"/>
<Square value="3"/>
</div>
<div className="board-row">
<Square value="4"/>
<Square value="5"/>
<Square value="6"/>
</div>
<div className="board-row">
<Square value="7"/>
<Square value="8"/>
<Square value="9"/>
</div>
</>
);
}