构建一个react项目_您想要了解更多有关React的内容吗? 让我们构建一个游戏,然后玩。...

构建一个react项目

by Samer Buna

通过Samer Buna

您想要了解更多有关React的内容吗? 让我们构建一个游戏,然后玩。 (Do you want to learn more about React? Let’s build — and then play — a game.)

Update: This article is now part of my book “React.js Beyond The Basics”.

更新:本文现在是我的书《超越基础的React.js》的一部分。

Read the updated version of this content and more about React at jscomplete.com/react-beyond-basics.

jscomplete.com/react-beyond-basics中阅读此内容的更新版本以及有关React的更多信息

When I teach React to beginners, I start by introducing them to the React API. Then I have them build a simple browser game after that. I think this is a good introduction strategy, because a simple game usually has a small state and, in most cases, no data dependencies at all. Learners get to focus entirely on the React API itself. The official React tutorial is a simple Tic-Tac-Toe game, which is an excellent choice.

当我向初学者教授React时,我首先将他们介绍给React API。 然后,我让他们构建一个简单的浏览器游戏。 我认为这是一个很好的介绍策略,因为简单的游戏通常状态很小,并且在大多数情况下完全没有数据依赖性。 学习者可以完全专注于React API本身。 官方的React教程是一个简单的Tic-Tac-Toe游戏 ,这是一个很好的选择。

Building simple game apps beats building abstract (and todo) apps on so many levels. I have always been against the use of abstract foo-bar types of examples, because they lack context and engagement.

构建简单的游戏应用胜过在多个层次上构建抽象(和待办事项)应用。 我一直反对使用抽象的foo-bar类型的示例,因为它们缺乏上下文和参与性。

Learners need to like what they are building. They need to accomplish something at the end of each phase in their learning journey. They need to make design decisions and see progress on features they can relate to.

学习者需要喜欢自己的建筑。 他们需要在学习过程的每个阶段的最后完成一些工作。 他们需要做出设计决策,并查看与它们相关的功能的进展。

Please note that this article is not a beginner’s tutorial. I will be assuming that you know the basics of React. If you are absolutely new to React, start by writing your first React component and then learn the fundamental concepts of React.

请注意 ,本文不是初学者的教程。 我假设您知道React的基础知识。 如果您绝对不熟悉React, 请首先编写第一个React组件 ,然后学习React的基本概念

I named the game we are going to build in this article The Target Sum. It is a simple one: you start with a random number in the header, the target (42 in the screenshot above), and a list of random challenge numbers below that target (the six numbers in the screenshot above).

我在“目标总和”一文中将我们要构建的游戏命名为。 这很简单:您从标题中的随机数字, 目标 (上面的屏幕快照中的42)以及该目标下方的随机挑战数字列表(上面的屏幕快照中的六个数字)开始。

Four of the six random numbers used above (8, 5, 13, 16) add up exactly to the target sum of 42. Picking the correct subset of numbers is how you win the game.

上面使用的六个随机数中的四个(8、5、13、16)完全等于目标总和42。选择正确的数字子集是您赢得比赛的方式。

Wanna play a few rounds? Click the Start button below:

想玩几回合吗? 点击下面的开始按钮:

Were you able to win? I am SO bad at this game.

你有能力赢吗? 我在这场比赛中太烂了。

Now that you know what we are going to build, let’s dive right in. Don’t worry— we will build this game in small increments, one step at a time.

现在您已经知道我们将要构建的内容,让我们深入研究吧。不用担心,我们将以较小的增量构建此游戏,一次仅一步。

步骤1:初始标记和样式 (Step 1: initial markup and styles)

It is a good idea to start with any known markups and styles to get those out of the way. With simple games like this one, this is usually an easy task. Just put mock static content where the dynamic content will eventually be.

最好从任何已知的标记和样式开始,以免这些标记和样式。 对于像这样的简单游戏,这通常很容易。 只需将模拟静态内容放在动态内容最终将要放置的位置即可。

To keep this article as short as possible and focused on React, I will start with some initial ready markup and CSS. Here is a jsComplete code session that you can use to start: jsdrops.com/rg-0

为了使本文尽可能简短,并专注于React,我将从一些初始的就绪标记和CSS开始。 这是您可以用来启动的jsComplete代码会话: jsdrops.com/rg-0

If you want to follow along with a different development environment, here is all the CSS that I used to style the markup above:

如果您想遵循不同的开发环境,这是我用来在上面标记样式的所有CSS:

.game {  display: inline-flex; flex-direction: column;  align-items: center; width: 100%;}.target {  border: thin solid #999; width: 40%; height: 75px;  font-size: 45px; text-align: center; display: inline-block;  background-color: #ccc;}.challenge-numbers {  width: 85%; margin: 1rem auto;}.number {  border: thin solid lightgray; background-color: #eee;  width: 40%; text-align: center; font-size: 36px;  border-radius: 5px; margin: 1rem 5%; display: inline-block;}.footer {  display: flex; width: 90%; justify-content: space-between;  }.timer-value { color: darkgreen; font-size: 2rem; }

I am not very good with CSS, and some of my choices above are probably questionable. Do not get distracted by that. We have a game to build.

我对CSS不太满意,上面的一些选择可能令人怀疑。 不要为此而分心。 我们有一个游戏要构建。

步骤2:提取组件 (Step 2: extracting components)

Once we reach a good state for the initial markup and styles, it’s natural to think about components as a next step. There are many reasons to extract part of the code into a component. For this example, I would like to focus on just one reason: Shared Behavior.

一旦我们对初始标记和样式达到了良好的状态,自然就可以将组件视为下一步。 将部分代码提取到组件中的原因很多。 对于此示例,我只想关注一个原因: 共享行为

A good indicator that you need a new component is when multiple elements are going to share the exact same behavior. In our example, you can click any of the six random challenge numbers to sum towards the target number. These clicks will trigger UI changes. This shared behavior means that we should create a component to represent a single number. I will simply name that Number.

当多个元素要共享完全相同的行为时,就需要一个新的组件是一个很好的指示。 在我们的示例中,您可以单击六个随机挑战数字中的任意一个以求和目标数字。 这些点击将触发UI更改。 这种共享的行为意味着我们应该创建一个代表单个数字的组件。 我将简单地命名为Number

The new changes introduced in every code snippet below are highlighted in bold.

以下每个代码段中引入的新更改均以粗体突出显示

// Step #2
class Number extends React.Component {  render() {    return <div className="number">{this.props.value}</div>;  }}
class Game extends React.Component {  render() {    return (      <div className="game">        <div className="target">42</div>        <div className="challenge-numbers">          <Number value={8} />          <Number value={5} />          <Number value={12} />          <Number value={13} />          <Number value={5} />          <Number value={16} />        </div>        <div className="footer">          <div className="timer-value">10</div>          <button>Start</button>        </div>      </div>    );  }}
ReactDOM.render(<Game />, document.getElementById('mountNode'));

You might want to extract more components such as a Target or Timer component. While adding components like these might enhance the readability of the code, I am going to keep the example simple and use only two components: Game and Number.

您可能希望提取更多组件,例如TargetTimer组件。 尽管添加类似这样的组件可能会提高代码的可读性,但我将使示例保持简单,仅使用两个组件: GameNumber

步骤3:使事物充满活力 (Step 3: making things dynamic)

Every time we render a new game, we need to create a new random target number. This is easy. We can use Math.random() to get a random number within the min...max range using this function:

每次渲染新游戏时,我们都需要创建一个新的随机目标编号。 这很简单。 我们可以使用Math.random()来使用此函数在min...max范围内获取随机数:

// Top-level function
const randomNumberBetween = (min, max) =>  Math.floor(Math.random() * (max - min + 1)) + min;

If we need a target number between 30 and 50, we can simply use randomNumberBetween(30, 50).

如果我们需要3050之间的目标数字,我们可以简单地使用randomNumberBetween(30, 50)

Then, we need to generate the six random challenge numbers. I am going to exclude the number 1 from these numbers and probably not go above 9 for the first level. This allows us to simply use randomNumberBetween(2, 9) in a loop to generate all challenge numbers. Easy, right? RIGHT?

然后,我们需要生成六个随机质询数。 我将从这些数字中排除数字1 ,并且第一级可能不会超过9 。 这使我们可以在循环中简单地使用randomNumberBetween(2, 9)来生成所有质询号码。 容易吧? 对?

This set of random challenge numbers needs to have a subset that actually sums to the random target number that we generated. We cannot just pick any random number. We have to pick some factors of the target number (with some of their factorization results), and then some more distracting random numbers. This is hard!

这组随机质询号需要具有一个实际上等于我们生成的随机目标数的子集。 我们不能只选择任何随机数。 我们必须选择目标数量的一些因素 (及其一些分解结果),然后再选择一些分散注意力的随机数。 这很难!

If you were doing this challenge in a coding interview, what you do next might make or break the job offer. What you need to do is to simply ask yourself: is there an easier way?

如果您在编程面试中正在挑战这一挑战,那么接下来的工作可能会成败。 您需要做的就是简单地问自己:是否有更简单的方法?

Take a minute and think about this particular problem. To make things interesting, let’s make the size of the challenge numbers list dynamic. The Game component will receive two new properties:

花一点时间考虑一下这个特殊的问题。 为了使事情变得有趣,让我们将挑战数字列表的大小设为动态。 Game组件将获得两个新属性:

<Game challengeSize={6} challengeRange={[2, 9]} />

The simple alternative to the factorization problem above is to pick the random challenge numbers first, and then compute the target from a random subset of these challenge numbers.

上面分解因子问题的简单替代方案是先选择随机挑战数字然后从这些挑战数字的随机子集计算目标。

This is easier. We can use Array.from to create an array of random numbers with the help of the randomNumberBetween function. We can then use the lodash sampleSize method to pick a random subset, and then just sum that subset and call it a target.

这比较容易。 我们可以在randomNumberBetween函数的帮助下,使用Array.from创建随机数数组。 然后,我们可以使用lodash sampleSize方法选择一个随机子集,然后将该子集求和并将其称为目标。

Since none of these numbers are going to change during a single game session, we can safely define them as instance properties.

由于这些数字在单个游戏会话中都不会改变,因此我们可以安全地将它们定义为实例属性。

Here are the modifications that we need so far:

这是到目前为止我们需要的修改:

// In the Game class
challengeNumbers = Array    .from({ length: this.props.challengeSize })    .map(() => randomNumberBetween(...this.props.challengeRange));
target = _.sampleSize(    this.challengeNumbers,    this.props.challengeSize - 2  ).reduce((acc, curr) => acc + curr, 0);
render() {    return (      <div className="game">        <div className="target">{this.target}</div>                <div className="challenge-numbers">         {this.challengeNumbers.map((value, index) =>           <Number key={index} value={value} />          )}        </div>        <div className="footer">          <div className="timer-value">10</div>          <button>Start</button>        </div>      </div>    )  }

Note how I used the index value from the map call as the key for every Number component. Remember that this is okay as long as we are not deleting, editing, or re-arranging the list of numbers (which we will not be doing here).

请注意,我如何使用map调用中的index值作为每个Number组件的key 。 请记住,只要我们不删除,编辑或重新排列数字列表(在此不再做)就可以。

You can see the full code we have so far here.

您可以在此处看到到目前为止的完整代码。

步骤4:决定状态如何 (Step 4: deciding what goes on the state)

When the Start button is clicked, the game will move into a different state and the 10 second timer will start its countdown. Since these are UI changes, a game status and the current value of that timer at any given time should be placed on the state.

单击开始按钮时,游戏将进入另一种状态,并且10秒计时器将开始其倒计时。 由于这些是UI更改,因此应将游戏状态和该计时器在任何给定时间的当前值置于状态上。

When the game is in the playing mode, the player can start clicking on challenge numbers. Every click will trigger a UI change. When a number is selected, we need the UI to represent it differently. This means we also need to place the selected numbers on the state as well. We can simply use an array for those.

当游戏处于playing模式时,玩家可以开始点击挑战号。 每次点击都会触发用户界面更改。 选择数字后,我们需要用户界面以不同的方式表示它。 这意味着我们还需要将选定的数字也放置在状态上。 我们可以简单地使用一个数组。

However, we cannot use the number values in this new array, because the list of random challenge numbers might contain repeated values. We need to designate the unique IDs of these numbers as selected. We used a number positional index as its ID, so we can use that to uniquely select a number.

但是,我们不能在新数组中使用数字 ,因为随机质询数字列表可能包含重复的值。 我们需要选择这些数字的唯一ID 。 我们使用数字位置索引作为其ID,因此我们可以使用它唯一地选择一个数字。

All of these identified state elements can be defined on the state of the Game component. The Number component does not need any state.

所有这些标识的状态元素都可以在Game组件的状态上定义。 Number组件不需要任何状态。

Here is what we need to place on the Game component state so far:

到目前为止,这是我们需要放置在Game组件状态上的内容:

// In the Game component
state = {  gameStatus: 'new' // new, playing, won, lost  remainingSeconds: this.props.initialSeconds,  selectedIds: [],};

Note how I made the initial value for the number of remainingSeconds customizable as well. I used a new game-level prop (initialSeconds) for that:

请注意,我也是如何自定义remainingSeconds Seconds数量的初始值的。 为此,我使用了一个新的游戏级道具( initialSeconds ):

<Game   challengeSize={6}   challengeRange={[2, 9]}   initialSeconds={10} />

To be honest, we do not need the gameStatus to be on the state at all. It is mostly computable. However, I am intentionally making an exception by placing it on the state as a simplified form of caching that computation.

老实说,我们根本不需要将gameStatus置于状态。 它主要是可计算的。 但是,我有意通过将其放在状态上作为缓存该计算的简化形式来作为例外。

Ideally, it’s better to cache this computation as an instance property, but I will keep it on the state to keep things simple.

理想情况下,最好将此计算作为实例属性进行缓存,但为了使事情简单,我将其保留在状态上。

What about the background colors used for the target number when the player wins or loses a game? Do those need to go on the state?

当玩家赢或输游戏时,用于目标号码的背景颜色如何? 那些需要继续吗?

Not really. Since we have a gameStatus element, we can use that to lookup the right background color. The dictionary of background colors can be a simple static Game property (or you can pass it down if you want to make it customizable):

并不是的。 由于我们有一个gameStatus元素,因此我们可以使用它来查找正确的背景色。 背景颜色的字典可以是简单的静态Game属性(或者,如果您想使其可自定义,则可以将其传递下来):

// In the Game component
static bgColors = {    playing: '#ccc',    won: 'green',    lost: 'red',  };

You can see the full code we have so far here.

您可以在此处看到到目前为止的完整代码。

步骤5:将视图设计为数据和状态的函数 (Step 5: designing views as functions of data and state)

This is really the core of React. Now that we have identified all of the data and state this game needs, we can design the whole UI based on them.

这确实是React的核心。 既然我们已经确定了所有数据并说明了游戏需求,那么我们就可以基于这些数据设计整个UI。

Since the state usually starts with empty values (like the empty selectedIds array), it is hard to design the UI without testing actual values. However, mock values can be used to make testing easier:

由于状态通常以空值开头(例如空的selectedIds数组),因此如果不测试实际值就很难设计UI。 但是,模拟值可用于简化测试:

// Mock states:
state = {  gameStatus: 'playing',  remainingSeconds: 7,  selectedIds: [0, 3, 4],};
// Also test with  gameStatus: 'lost'
// And  gameStatus: 'won'

Using this strategy, we do not have to worry about behavior and user interactions (yet). We can focus on just having the UI designed as functions of data and (mock) state.

使用这种策略,我们不必担心行为和用户交互(尚未)。 我们可以专注于仅将UI设计为数据和(模拟)状态的函数。

The key to executing this step correctly is making sure child components receive only the minimum data that they actually need to re-render themselves in the various states. This is probably the most important statement in the entire article.

正确执行此步骤的关键是确保子组件仅接收在各种状态下重新呈现自身所需的最少数据 。 这可能是整篇文章中最重要的陈述。

We only have one child component, so let’s think about what it needs to render itself. We are already passing down its value from the map call, so what else does it need? For example, think about these questions:

我们只有一个子组件,所以让我们考虑一下它需要呈现什么。 我们已经从map调用中传递了它的值,那么还需要什么呢? 例如,考虑以下问题:

  • Does the Number component need to be aware of the selectedIds array to figure out whether it is a selected number?

    Number组件是否需要知道selectedIds数组才能确定它是否为选定的数字?

  • Does the Number component need to be aware of the current gameStatus value?

    Number组件是否需要知道当前的gameStatus值?

I will admit that answering these questions is not as easy as you might think. While you might be tempted to answer yes to both, the Number component does not need to be aware of both selectedIds and gameStatus. It only needs to be aware of whether or not it can be clicked. If it cannot be clicked, it will need to render itself differently.

我承认回答这些问题并不像您想的那么容易。 尽管您可能会想对两者都回答“是”,但Number组件不需要同时知道selectedIdsgameStatus 。 它只需要知道是否可以单击它即可。 如果无法单击,则需要以其他方式呈现自己。

Passing anything else to the Number component will make it re-render unnecessarily, which is something we should avoid.

将其他任何内容传递给Number组件将使其不必要地重新呈现,这是我们应避免的事情。

We can use a lower opacity to represent a non-clickable number. Let’s make the Number component receive a clickable prop.

我们可以使用较低的不透明度来表示不可点击的数字。 让我们让Number组件获得一个clickable道具。

Computing this boolean clickable prop should happen in the Game component so that you avoid having to pass more data to the Number component. Let me give examples about the importance of making sure a child component receives only the minimum data that it needs:

计算此布尔clickable道具应该在Game组件中进行,以便避免将更多数据传递给Number组件。 让我举例说明确保子组件仅接收所需的最少数据的重要性:

  • If we pass the gameStatus value to the Number component, then every time the gameStatus changes (for example, from playing to won), React will re-render all six challenge numbers. But in this case, it did not really need to re-render any of them.

    如果我们将gameStatus值传递给Number组件,则每当gameStatus发生变化(例如,从playing变为won )时,React都会重新渲染所有六个挑战数字。 但是在这种情况下,实际上并不需要重新渲染任何一个。

  • A Number component does need to re-render when the gameStatus changes from new to playing because of the masking question marks feature at the beginning. To avoid passing down the gameStatus to Number, we can compute the value displayed in a Number component within the map function callback in the Game component.

    A数字组件确实需要重新渲染当gameStatus从改变newplaying ,因为掩蔽问号开头功能。 为了避免将gameStatus传递给Number ,我们可以计算Game组件中map函数回调中Number组件中显示的值。

  • If we pass the selectedIds array down to the Number component, then on every click React will re-render all six challenge numbers when it only needed to re-render one number. This is why a clickable boolean flag is a much better choice here.

    如果我们将selectedIds数组传递给Number组件,那么每次单击时,React只需重新渲染一个数字,便会重新渲染所有六个挑战数字。 这就是为什么clickable布尔标志在这里是更好的选择的原因。

With every prop you pass to a child React component comes great responsibility.

每个传递给子React组件的道具都会带来巨大的责任。

This is more important than you might think. However, React will not optimize the re-rendering of a component automatically. We will have to decide if we want it to do so. This is discussed in step #8 below.

这比您想象的要重要。 但是,React不会自动优化组件的重新渲染。 我们将必须决定是否要这样做。 这将在下面的步骤8中讨论。

Besides the clickable prop, what else does the Number component need? Since it is going to be clicked, and we need to place the clicked number’s ID on the Game state, the click handler of every Number component needs to be aware of its own ID. And we cannot use React’s key prop value in this case. Let’s make the Number component receive an id prop as well.

除了clickable道具以外, Number组件还需要什么? 由于将要单击它,并且我们需要将被单击的数字的ID置于Game状态,因此每个Number组件的单击处理程序都需要知道其自己的ID。 而且在这种情况下,我们不能使用React的key prop值。 让我们让Number组件也接收一个id属性。

// In the Number component
render() {    return (      <div         className="number"         style={{ opacity: this.props.clickable ? 1 : 0.3 }}        onClick={() => console.log(this.props.id)}      >        {this.props.value}      </div>    );  }

To compute whether a number is available and clickable, you can use a simple indexOf call on the selecetdIds array. Let’s create a function for that:

要计算数字是否可用和可单击,可以在selecetdIds数组上使用简单的indexOf调用。 让我们为此创建一个函数:

// In the Game classisNumberAvailable = (numberIndex) =>    this.state.selectedIds.indexOf(numberIndex) === -1;

One behavior you probably noticed while playing the game above is that the number squares start out displaying a question mark until the Start button is clicked. We can use a ternary operator to control the value of each Number component based on the gameStatus value. Here is what we need to change to render a Number component inside the map call:

在上面的游戏中,您可能会注意到的一种行为是,数字方块开始显示一个问号,直到单击“开始”按钮为止。 我们可以使用三元运算符基于gameStatus值来控制每个Number组件的值。 这是我们需要更改以在map调用内呈现Number组件的内容:

<Number  key={index}  id={index}  value={this.state.gameStatus === 'new' ? '?' : value}  clickable={this.isNumberAvailable(index)}/>

We can use a similar ternary expression for the target number value. We can also control its background color using a lookup call to the static bgColors object:

我们可以对目标数字值使用类似的三元表达式。 我们还可以使用对静态bgColors对象的查找调用来控制其背景色:

<div  className="target"  style={{ backgroundColor: Game.bgColors[gameStatus] }}&gt;  {this.state.gameStatus === 'new' ? '?' : this.target}</div>

Finally, we should show the Start button only when the gameStatus is new . Otherwise we should just show the remainingSeconds counter. When the game is won or lost, let’s show a Play Again button. Here are the modifications we need for all that:

最后,仅当gameStatusnew时,才应显示“ 开始”按钮。 否则,我们应该只显示remainingSeconds Second计数器。 当比赛wonlost ,让我们来显示一个播放按钮再次 。 这是我们需要做的所有修改:

<div className="footer">  {this.state.gameStatus === 'new' ? (    <button>Start</button>  ) : (    <div className="timer-value">{this.state.remainingSeconds}</div>  )}  {['won', 'lost'].includes(this.state.gameStatus) && (    <;button>Play Again</button>  )}</div>

You can see the full code we have so far here.

您可以在此处看到到目前为止的完整代码。

步骤6:设计行为以更改状态 (Step 6: designing behaviors to change the state)

The first behavior that we need to figure out is how to start the game. We need two main actions here: 1) change the gameStatus to playing and 2) start a timer to decrement the remainingSeconds value.

我们需要弄清的第一个行为是如何开始游戏。 我们在这里需要两个主要动作:1)改变gameStatusplaying和2)启动一个定时器递减remainingSeconds值。

If remainingSeconds is decremented all the way to zero, we need to force the game into the lost state and stop the timer as well. Otherwise, it will decrement beyond zero.

如果remainingSeconds一直递减为零,我们需要迫使游戏进入lost状态并停止计时器。 否则,它将减少超过零。

Here is a function we can use to do all that:

这是我们可以用来完成所有操作的函数:

// In the Game class
startGame = () => {  this.setState({ gameStatus: 'playing' }, () => {    this.intervalId = setInterval(() => {      this.setState((prevState) => {        const newRemainingSeconds = prevState.remainingSeconds - 1;        if (newRemainingSeconds === 0) {          clearInterval(this.intervalId);          return { gameStatus: 'lost', remainingSeconds: 0 };        }        return { remainingSeconds: newRemainingSeconds };      });    }, 1000);  });};

Note how I start the timer only after the setState call is complete. This is possible using the second argument function callback to setState.

请注意,只有在setState调用完成后,才启动计时器。 使用第二个参数函数callbacksetState可以做到这一点。

Next, let’s figure out what should happen when a number is clicked during a game session. Let’s create a selectNumber function for that. This function should receive the ID of the clicked number and should only work when the gameStatus is playing. Every time a number is clicked, we need to add its ID to the selectedIds array.

接下来,让我们弄清楚在游戏过程中单击数字时会发生什么。 让我们selectNumber创建一个selectNumber函数。 该函数应接收点击编号的ID,并且仅在gameStatus playing时才起作用。 每次单击数字时,我们需要将其ID添加到selectedIds数组中。

We also need to compute the new gameStatus because every click might result in a won/lost status. Let’s create a calcGameStatus function to do that.

我们还需要计算新的gameStatus因为每次点击都可能导致won / lost状态。 让我们创建一个calcGameStatus函数来执行此操作。

Here is one way to implement these two new functions:

这是实现这两个新功能的一种方法:

// In the Game class
selectNumber = (numberIndex) => {  if (this.state.gameStatus !== 'playing') {    return;  }  this.setState(    (prevState) => ({      selectedIds: [...prevState.selectedIds, numberIndex],      gameStatus: this.calcGameStatus([        ...prevState.selectedIds,        numberIndex,      ]),    }),    () => {      if (this.state.gameStatus !== 'playing') {        clearInterval(this.intervalId);      }    }  );};
calcGameStatus = (selectedIds) => {  const sumSelected = selectedIds.reduce(    (acc, curr) => acc + this.challengeNumbers[curr],    0  );  if (sumSelected < this.target) {    return 'playing';  }  return sumSelected === this.target ? 'won' : 'lost';};

Note a few things about the functions above:

请注意上述功能的一些注意事项:

  • We used the array spread operator to append numberIndex to selectedIds. This is a handy trick to avoid mutating the original array.

    我们使用了数组扩展运算符 numberIndex附加到selectedIds 。 这是避免变异原始数组的便捷技巧。

  • Since the new gameStatus is to be computed while we are updating the state, I passed the new selectedIds value to the calcGameStatus function rather than using the current selectedIds value. It has not been updated yet to include the new numberIndex at this point.

    由于新gameStatus我们正在更新状态来计算,我通过了新selectedIds价值的calcGameStatus函数,而不是使用当前selectedIds值。 目前尚未更新,以包括新的numberIndex

  • In calcGameStatus, I used a reduce call. This computes the current sum after a click using a combination of what is selected and the original challengeNumbers array, which holds the actual values of numbers. Then, a few conditionals can do the trick of determining the current game status.

    calcGameStatus ,我使用了reduce调用。 单击后,将使用所选内容和原始的challengeNumbers数组的组合计算单击后的当前总和,该数组保存数字的实际值。 然后,一些条件可以完成确定当前游戏状态的技巧。

  • Since the timer has to be stopped if the new gameStatus is not playing, I used the second callback argument for setState to implement that logic. This ensures it will use the new gameStatus after the async setState call is done.

    由于如果新的gameStatus没有playing ,则必须停止计时器,因此我将setState的第二个回调参数用于实现该逻辑。 这样可以确保在异步setState调用完成后,它将使用新的gameStatus

The game is currently completely functional with the exception of the Play Again button. You can see the full code we have so far here.

该游戏目前已完全正常运行,但 再次播放按钮。 您可以在此处看到到目前为止的完整代码。

Now, how exactly are we going to implement this Play Again action? Can we simply just reset the state of the Game component?

现在,我们究竟将如何实施“再次播放”操作? 我们可以简单地重置Game组件的状态吗?

Nope. Think about why.

不。 想一想为什么。

步骤7:重置React组件 (Step 7: resetting a React component)

The Play Again action needs more than a simple reset of the state of the Game component. We need to generate a new set of challengeNumbers alongwith a new target number. In addition, we need to clear any currently running timers and auto-start the game.

再次玩”操作不仅仅需要简单地重置Game组件的状态。 我们需要生成一组新的challengeNumbers一个新的target编号。 此外,我们需要清除所有当前运行的计时器并自动启动游戏。

We can certainly improve the startGame function to do all of that. But React offers an easier way to reset a component: unmount that component and just remount it. This will trigger all initialization code and take care of any timers as well.

我们当然可以改进startGame函数来完成所有这些工作。 但是React提供了一种更轻松的方式来重置组件:卸载该组件然后重新安装。 这将触发所有初始化代码,并同时照顾所有计时器。

We do not really have to worry about the timer part of the state, because that part is controlled by behavior. However, in general, unmounting a component should also clear any timers defined in that component. Always do that:

我们实际上不必担心状态的计时器部分,因为该部分由行为控制。 但是,通常,卸载组件还应清除该组件中定义的所有计时器。 始终这样做:

// In the Game class
componentWillUnmount() {    clearInterval(this.intervalId);  }

Now, if the Game component is unmounted and re-mounted, it will start a completely fresh instance with new random numbers and an empty state. However, to re-mount a component based on a behavior, we will need to introduce a new parent component for Game . We will name it App . Then we’ll put something on the state of this new parent component which will trigger a UI change.

现在,如果卸载并重新安装了Game组件,它将启动一个具有新随机数和空状态的全新实例。 但是,要基于行为重新安装组件,我们将需要为Game引入一个新的父组件。 我们将其命名为App 。 然后,我们在此新父组件的状态上放置一些内容,这将触发UI更改。

React has another useful trick we can use to accomplish this task. If any React component is rendered with a certain key and later re-rendered with a different key, React sees a completely new instance. It then automatically unmounts and re-mounts that component!

React还有另一个有用的技巧,我们可以用来完成此任务。 如果任何React组件使用某个key渲染,然后再使用其他key重新渲染,则React会看到一个全新的实例。 然后,它会自动卸载并重新安装该组件!

All we need to do is have a unique game ID as part of the state of the App component, use that as the key for the Game component, and change it when we need to reset a game.

我们需要做的就是将唯一的游戏ID作为App组件状态的一部分,将其用作Game组件的key ,并在需要重置游戏时进行更改。

We also want the game to auto-start when the player clicks Play Again, instead of having them click Start after Play Again. So let’s make the App component also pass down an autoPlay prop to Game and compute that based on the new gameId attribute. Only the first game should not be auto-played.

我们还希望游戏在玩家单击“再次播放”时自动启动而不是让他们单击“再次播放开始 ”。 因此,让我们将App组件也传递给游戏一个autoPlay道具,并根据新的gameId属性对其进行计算。 只有第一个游戏不能自动播放。

Here are the modifications that we need:

这是我们需要的修改:

// Create new App component
class App extends React.Component {  state = {    gameId: 1,  };
resetGame = () =>    this.setState((prevState) => ({      gameId: prevState.gameId + 1,    }));
render() {    return (      <Game        key={this.state.gameId}        autoPlay={this.state.gameId > 1}        challengeSize={6}        challengeRange={[2, 9]}        initialSeconds={10}        onPlayAgain={this.resetGame}      />    );  }}
// In the Game class: respect the value of the new autoPlay prop  componentDidMount() {    if (this.props.autoPlay) {      this.startGame();    }  }
// In the Game render call// Wire the Play Again action using the parent prop<button onClick={this.props.onPlayAgain}>  Play Again</button>
// Render the new App component instead of GameReactDOM.render(<App />, document.getElementById('mountNode'));

You can see the full code we now have here.

您可以在此处看到完整的代码。

步骤8:优化是否可以衡量 (Step 8: optimize if you can measure)

One of the challenging aspects of a React application is avoiding the wasteful rendering of the components that do not need to be re-rendered. We went to great lengths in step #5 not to pass any prop that will cause a Number component to re-render unnecessarily.

React应用程序具有挑战性的方面之一是避免不必要地渲染不需要重新渲染的组件。 在第5步中,我们竭尽全力不传递任何会导致Number组件不必要地重新呈现的属性。

However, the code as it is now is still wastefully re-rendering most of the Number components. To see this in action, use a componentWillUpdate method in the Number component and just console.log something there:

但是,现在的代码仍在浪费大量的Number组件。 若要查看实际效果,请在Number组件中使用componentWillUpdate方法,然后在其中console.log一些内容:

// In the Number componentcomponentWillUpdate() {  console.log('Number Updated');}

Then, go ahead and play. On every state change in the Game component, you will see that we are re-rendering all 6 Number components. This happens when we click the Start button and every second after that!

然后,继续玩。 在Game组件中的每个状态更改中,您都会看到我们正在重新渲染所有6 Number组件。 当我们单击“ 开始”按钮时,然后每隔一秒钟,就会发生这种情况!

The fact is, a Number component should not re-render itself unless the player clicks on it. The 60 re-renders that were triggered by the timer change were wasteful. Furthermore, when the player clicks a number, only that number needs to be re-rendered. Right now, React also re-renders all six numbers when the player selects any number.

事实是,除非玩家单击,否则Number组件不应重新呈现自身。 由计时器更改触发的60重新渲染很浪费。 此外,当玩家单击一个数字时,仅需要重新渲染该数字。 现在,当玩家选择任何数字时,React也会重新渲染所有六个数字。

Luckily, we have been careful enough to only pass to the Number component the exact props that it needs to re-render. Only the challenge number that needs to be re-rendered will receive different values in these props.

幸运的是,我们已经足够小心,只将需要重新渲染的确切道具传递给Number组件。 在这些道具中,仅需要重新渲染的挑战编号将获得不同的值。

This means we can use a conditional in React’s shouldComponentUpdate to short-circuit the render operation if all nextProps of a Number component match the current props.

这意味着如果Number组件的所有nextProps与当前props匹配,我们可以在React的shouldComponentUpdate使用一个条件来缩短渲染操作。

React’s PureComponent class will do exactly that. Go ahead and change the Number component to extend React.PureComponent instead of React.Component and see how the problem magically goes away.

React的PureComponent类将完全做到这一点。 继续并更改Number组件以扩展React.PureComponent而不是React.Component然后看问题是如何神奇消失的。

class Number extends React.PureComponent

However, is this optimization worth it? We cannot answer that question without measuring. Basically, you need to measure which code uses fewer resources: a component render call or the if statement in React.PureComponent that compares previous and next state/props. This completely depends on the sizes of the state/props trees and the complexity of what is being re-rendered. Do not just assume one way is better than the other.

但是,这种优化值得吗? 我们无法衡量就不能回答这个问题。 基本上,您需要衡量哪些代码使用的资源更少:组件渲染调用或React.PureComponent中的if语句,用于比较上一个和下一个状态/ React.PureComponent 。 这完全取决于状态/属性树的大小以及要重新渲染的内容的复杂性。 不要仅仅假设一种方法比另一种更好。

You can see the final code here. MVP complete. Now, for the love of CSS, can someone please style this game to make it appealing to kids? :)

您可以在此处查看最终代码。 MVP完成。 现在,出于对CSS的热爱,有人可以为这款游戏设计风格以使其吸引孩子吗? :)

Do not stop here if you like this. Add more features to the game. For example, keep a score for winning and increase it every time the player wins a round. Maybe make the score value depend on how fast the player wins the round.

如果您喜欢这样,请不要在这里停下来。 为游戏添加更多功能。 例如,保留获胜分数,并在玩家每次赢得一回合时提高分数。 也许让得分值取决于玩家赢得回合的速度。

You can also make future rounds harder by changing challengeSize, challengeRange, and initialSeconds when starting a new game.

您还可以通过在开始新游戏时更改challengeSizechallengeRangeinitialSeconds来增加以后的回合难度。

The Target Sum game was featured in my React Native Essential Training course, which is available on Lynda and LinkedIn Learning.

我的React Native基础培训课程中介绍了Target Sum游戏课程可在LyndaLinkedIn Learning上找到

Thanks for reading.

谢谢阅读。

Learning React or Node? Checkout my books:

学习React还是Node? 结帐我的书:

翻译自: https://www.freecodecamp.org/news/do-you-want-to-learn-more-about-react-lets-build-and-then-play-a-game-218e0da5be44/

构建一个react项目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值