构建一个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
.
您可能希望提取更多组件,例如Target
或Timer
组件。 尽管添加类似这样的组件可能会提高代码的可读性,但我将使示例保持简单,仅使用两个组件: Game
和Number
。
步骤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)
.
如果我们需要30
到50
之间的目标数字,我们可以简单地使用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 theselectedIds
array to figure out whether it is a selected number?Number
组件是否需要知道selectedIds
数组才能确定它是否为选定的数字?Does the
Number
component need to be aware of the currentgameStatus
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
组件不需要同时知道selectedIds
和gameStatus
。 它只需要知道是否可以单击它即可。 如果无法单击,则需要以其他方式呈现自己。
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 theNumber
component, then every time thegameStatus
changes (for example, fromplaying
towon
), 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 fromnew
toplaying
because of the masking question marks feature at the beginning. To avoid passing down thegameStatus
toNumber
, we can compute the value displayed in aNumber
component within themap
function callback in theGame
component.A数字组件确实需要重新渲染当
gameStatus
从改变new
来playing
,因为掩蔽问号开头功能。 为了避免将gameStatus
传递给Number
,我们可以计算Game
组件中map
函数回调中Number
组件中显示的值。If we pass the
selectedIds
array down to theNumber
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 aclickable
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] }}> {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:
最后,仅当gameStatus
为new
时,才应显示“ 开始”按钮。 否则,我们应该只显示remainingSeconds
Second计数器。 当比赛won
或lost
,让我们来显示一个播放按钮再次 。 这是我们需要做的所有修改:
<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)改变gameStatus
到playing
和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
调用完成后,才启动计时器。 使用第二个参数函数callback到setState
可以做到这一点。
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
toselectedIds
. 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 newselectedIds
value to thecalcGameStatus
function rather than using the currentselectedIds
value. It has not been updated yet to include the newnumberIndex
at this point.由于新
gameStatus
是当我们正在更新状态来计算,我通过了新selectedIds
价值的calcGameStatus
函数,而不是使用当前selectedIds
值。 目前尚未更新,以包括新的numberIndex
。In
calcGameStatus
, I used areduce
call. This computes the current sum after a click using a combination of what is selected and the originalchallengeNumbers
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 notplaying
, I used the second callback argument forsetState
to implement that logic. This ensures it will use the newgameStatus
after the asyncsetState
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.
您还可以通过在开始新游戏时更改challengeSize
, challengeRange
和initialSeconds
来增加以后的回合难度。
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游戏,该课程可在Lynda和LinkedIn Learning上找到 。
Thanks for reading.
谢谢阅读。
Learning React or Node? Checkout my books:
学习React还是Node? 结帐我的书:
构建一个react项目