1、教程:井字棋游戏
本教程将引导你逐步实现一个简单的井字棋游戏,并且不需要你对 React 有任何了解。在此过程中你会学习到一些编写 React 程序的基本知识,完全理解它们可以让你对 React 有比较深入的理解。
1.1、教程分成以下几个部分:
配置是一些准备工作。
概览介绍了 React 的 基础知识:组件、props 和 state。
完成游戏介绍了 React 开发中 最常用的技术。
添加时间旅行可以让你更深入地了解 React 的独特优势。
1.2、实现的是什么程序?
本教程将使用 React 实现一个交互式的井字棋游戏。你可以在下面预览最终成果:
App.js
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
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 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>
</>
);
}
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
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 (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
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;
}
如果你还不是很明白上面的代码,不用担心!本教程的目的就是帮你理解 React 及其语法。
我们建议你在继续本教程之前,先看看上面的井字棋游戏。我们会注意到的一项功能是,棋盘右侧有一个编号列表,它记录了游戏中落子的历史,并随着游戏的进行而更新。
体验完游戏以后,继续阅读本教程吧!我们将从一个更简单的模板开始。下一步将介绍相关配置,以便于你着手实现这个游戏。
export default function Square() {
return <button className="square">X</button>;
}
注意
如果你想要使用本地开发环境来学习这个教程,需要按照下面的流程进行:
安装 Node.js
在之前打开的 CodeSandbox 选项卡中,按左上角的按钮打开菜单,然后选择 Download Sandbox,将代码压缩包下载到本地。下载链接https://codesandbox.io/p/sandbox/react-dev-q2z497?file=%2Fsrc%2FApp.js%3A19%2C13&utm_medium=sandpack
将压缩包解压,打开终端并使用
cd
命令切换到你解压后的目录。使用
npm install
安装依赖。运行
npm start
启动本地服务器,按照提示在浏览器中查看运行效果。
1.3、概览
完成配置以后,我们先来大致了解一下 React 吧!看一下刚刚的代码 ,在 CodeSandbox 中,你将看到三个主要的部分:
Files 部分列出了一些文件:
App.js
、index.js
、styles.css
和public
文件夹。code editor 部分可以看到你所选中文件的源码。
browser 部分可以预览代码的实时结果。
App.js
文件里面的内容应该是这样的:
export default function Square() {
return <button className="square">X</button>;
}
browser 部分应该会像下面这样在方块里面显示一个 X:
现在,让我们仔细研究一下这些文件吧。
1.4、App.js
App.js
的代码创建了一个 组件。在 React 中,组件是一段可重用代码,它通常作为 UI 界面的一部分。组件用于渲染、管理和更新应用中的 UI 元素。让我们逐行查看这段代码,看看发生了什么:
export default function Square() {
return <button className="square">X</button>;
}
第一行定义了一个名为 Square
的函数。JavaScript 的 export
关键字使此函数可以在此文件之外访问。default
关键字表明它是文件中的主要函数。
export default function Square() {
return <button className="square">X</button>;
}
第二行返回一个按钮。JavaScript 的 return
关键字意味着后面的内容都作为值返回给函数的调用者。<button>
是一个 JSX 元素。JSX 元素是 JavaScript 代码和 HTML 标签的组合,用于描述要显示的内容。className="square"
是一个 button 属性,它决定 CSS 如何设置按钮的样式。X
是按钮内显示的文本,</button>
闭合 JSX 元素以表示不应将任何后续内容放置在按钮内。
1.5、styles.css
单击 CodeSandbox 中的 styles.css
文件。该文件定义了 React 应用的样式。前两个 CSS 选择器(*
和 body
)定义了应用大部分的样式,而 .square
选择器定义了 className
属性设置为 square
的组件的样式。这与 App.js
文件中的 Square
组件中的按钮是相匹配的。
1.6、index.js
单击 CodeSandbox 中的 index.js
的文件。在本教程中我们不会编辑此文件,但它是 App.js
文件中创建的组件与 Web 浏览器之间的桥梁。
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
第 1-5 行将所有必要的部分组合在一起:
React
React 与 Web 浏览器对话的库(React DOM)
组件的样式
App.js
里面创建的组件
其他文件将它们组合在一起,并将最终成果注入 public
文件夹里面的 index.html
中。
1.7、构建棋盘
让我们回到 App.js
。接下来我们将专注于这个文件。目前棋盘只有一个方块,但你需要九个!如果你只是想着复制粘贴来制作两个像这样的方块:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
你将会得到如下错误:
/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment <>...</>
?
React 组件必须返回单个 JSX 元素,不能像两个按钮那样返回多个相邻的 JSX 元素。要解决此问题,可以使用 Fragment(<>
与 </>
)包裹多个相邻的 JSX 元素,如下所示:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
现在你应该可以看见:
非常棒!现在你只需要通过复制粘贴来添加九个方块,然后……
但事与愿违的是这些方块并没有排列成网格,而是都在一条线上。要解决此问题,需要使用 div
将方块分到每一行中并添加一些 CSS 样式。当你这样做的时候,需要给每个方块一个数字,以确保你知道每个方块的位置。
App.js
文件中,Square
组件看起来像这样:
export default function Square() {
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>
</>
);
}
借助 styles.css
中定义的 board-row
样式,我们将组件分到每一行的 div
中。最终完成了井字棋棋盘:
但是现在有个问题,名为 Square
的组件实际上不再是方块了。让我们通过将名称更改为 Board 来解决这个问题:
export default function Board() {
//...
}
此时你的代码应如下所示:App.js
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>
</>
);
}
1.8、通过 props 传递数据
接下来,当用户单击方块时,我们要将方块的值从空更改为“X”。根据目前构建的棋盘,你需要复制并粘贴九次更新方块的代码(每个方块都需要一次)!但是,React 的组件架构可以创建可重用的组件,以避免混乱、重复的代码。
首先,要将定义第一个方块(<button className="square">1</button>
)的这行代码从 Board
组件复制到新的 Square
组件中: