自从~v0.12发布以来,我一直在用React工作。(2014年!哇,时间都去哪儿了?)它已经改变了很多。我记得一路上的某些 "Aha "时刻。有一件事一直保持着,就是使用它的心态。我们以不同的方式思考问题,而不是直接用DOM工作。
对我来说,我的学习风格是尽可能快地启动和运行一些东西。然后,只要有必要,我就会去探索文档中更深的领域和包括的一切。在实践中学习,享受乐趣,并推动事情的发展。
目标
这里的目的是向你展示足够的React,以涵盖那些 "Aha "时刻。我建议在你想深入了解的情况下查看文档。我不会重复它们。
请注意,你可以在CodePen中找到所有的例子,但你也可以跳到我的Github repo中找到一个完整的工作游戏。
第一个应用程序
你可以用各种方式引导React应用。下面是一个例子。
import React from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
const App = () => <h1>{`Time: ${Date.now()}`}</h1>
render(<App/>, document.getElementById('app')
起始点
我们已经学会了如何制作一个组件,我们可以大致判断我们需要什么。
import React, { Fragment } from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
const Moles = ({ children }) => <div>{children}</div>
const Mole = () => <button>Mole</button>
const Timer = () => <div>Time: 00:00</div>
const Score = () => <div>Score: 0</div>
const Game = () => (
<Fragment>
<h1>Whac-A-Mole</h1>
<button>Start/Stop</button>
<Score/>
<Timer/>
<Moles>
<Mole/>
<Mole/>
<Mole/>
<Mole/>
<Mole/>
</Moles>
</Fragment>
)
render(<Game/>, document.getElementById('app'))
开始/停止
在我们做任何事情之前,我们需要能够启动和停止游戏。开始游戏将触发像计时器和鼹鼠这样的元素,让它们活过来。这就是我们可以引入条件渲染的地方。
const Game = () => {
const [playing, setPlaying] = useState(false)
return (
<Fragment>
{!playing && <h1>Whac-A-Mole</h1>}
<button onClick={() => setPlaying(!playing)}>
{playing ? 'Stop' : 'Start'}
</button>
{playing && (
<Fragment>
<Score />
<Timer />
<Moles>
<Mole />
<Mole />
<Mole />
<Mole />
<Mole />
</Moles>
</Fragment>
)}
</Fragment>
)
}
我们有一个playing 的状态变量,我们用它来渲染我们需要的元素。在JSX中,我们可以使用一个带有&& 的条件,如果条件是true ,就渲染一些东西。在这里,我们说如果我们正在玩,就渲染棋盘和它的内容。这也会影响到按钮文本,我们可以使用三元组。
打开这个链接中的演示,将扩展名设置为高亮渲染。接下来,你会看到计时器会随着时间的变化而渲染,但当我们敲击鼹鼠时,所有组件都会重新渲染。
JSX中的循环
你可能会想,我们渲染Mole的方式效率很低。你这样想是对的!这里有一个机会,我们可以用循环的方式渲染这些东西。
对于JJSX,我们倾向于在99%的情况下使用Array.map 来渲染一个事物的集合。比如说。
const USERS = [
{ id: 1, name: 'Sally' },
{ id: 2, name: 'Jack' },
]
const App = () => (
<ul>
{USERS.map(({ id, name }) => <li key={id}>{name}</li>)}
</ul>
)
另一种方法是在for循环中生成内容,然后渲染一个函数的返回。
return (
<ul>{getLoopContent(DATA)}</ul>
)
那个key 属性是做什么的?这可以帮助React确定哪些变化需要渲染。如果你可以使用一个唯一的标识符,那就这样做吧作为最后的手段,使用集合中项目的索引。阅读关于列表的文档了解更多)。
对于我们的例子,我们没有任何数据可以使用。如果你需要生成一个事物的集合,那么这里有一个技巧你可以使用。
new Array(NUMBER_OF_THINGS).fill().map()
这在某些情况下可能对你有用。
return (
<Fragment>
<h1>Whac-A-Mole</h1>
<button onClick={() => setPlaying(!playing)}>{playing ? 'Stop' : 'Start'}</button>
{playing &&
<Board>
<Score value={score} />
<Timer time={TIME_LIMIT} onEnd={() => console.info('Ended')}/>
{new Array(5).fill().map((_, id) =>
<Mole key={id} onWhack={onWhack} />
)}
</Board>
}
</Fragment>
)
或者,如果你想要一个持久的集合,你可以使用像uuid 。
import { v4 as uuid } from 'https://cdn.skypack.dev/uuid'
const MOLE_COLLECTION = new Array(5).fill().map(() => uuid())
// In our JSX
{MOLE_COLLECTION.map((id) =>
)}
结束游戏
我们只能用 "开始 "按钮来结束我们的游戏。当我们真的结束它时,当我们再次开始时,分数仍然存在。我们的onEnd ,Timer ,也还没有什么作用。
我们要引入一个第三方的解决方案,让我们的鼹鼠上下晃动。这是一个关于如何引入与DOM一起工作的第三方解决方案的例子。在大多数情况下,我们使用参照物来抓取DOM元素,然后在一个效果中使用我们的解决方案。
我们将使用GreenSock(GSAP)来使我们的鼹鼠晃动。我们今天不会深入研究GSAP的API,但如果你对它们的作用有任何疑问,请问我吧
这里有一个更新的Mole ,有GSAP 。
import gsap from 'https://cdn.skypack.dev/gsap'
const Mole = ({ onWhack }) => {
const buttonRef = useRef(null)
useEffect(() => {
gsap.set(buttonRef.current, { yPercent: 100 })
gsap.to(buttonRef.current, {
yPercent: 0,
yoyo: true,
repeat: -1,
})
}, [])
return (
<div className="mole-hole">
<button
className="mole"
ref={buttonRef}
onClick={() => onWhack(MOLE_SCORE)}>
Mole
</button>
</div>
)
}
我们给button 添加了一个包装器,它允许我们显示/隐藏Mole ,我们也给我们的button 一个ref 。使用一个效果,我们可以创建一个Tween(GSAP动画),使按钮上下移动。
你还会注意到,我们正在使用className ,这是JSX中与class 相等的属性,用于应用类名。为什么我们不使用GSAP中的className ?因为如果我们有很多元素有那个className ,我们的效果会试图使用它们全部。这就是为什么useRef 是一个很好的选择,可以坚持使用。
请看《笔8》。Moving Molesby@jh3y.
太棒了,现在我们有了会晃动的Mole,从功能上来说,我们的游戏已经完成了。他们都是完全一样的移动,这并不理想。它们应该以不同的速度运行。Mole 被击打的时间越长,得分也应该减少。
我们的鼹鼠的内部逻辑可以处理得分和速度如何更新的问题。将初始的speed 、delay 、points 作为道具传入,将使组件更加灵活。
<Mole key={index} onWhack={onWhack} points={MOLE_SCORE} delay={0} speed={2} />
现在,对我们的Mole 逻辑进行分解。
让我们从我们的分数将如何随时间减少开始。这可能是一个很好的候选ref 。我们有一个不影响渲染的东西,它的值可能会在一个闭合中丢失。我们在一个效果中创建了我们的动画,它永远不会被重新创建。在我们的动画的每一次重复中,我们想通过一个倍数来减少points 的值。点值可以有一个最小值,由一个pointsMin 的道具定义。
const bobRef = useRef(null)
const pointsRef = useRef(points)
useEffect(() => {
bobRef.current = gsap.to(buttonRef.current, {
yPercent: -100,
duration: speed,
yoyo: true,
repeat: -1,
delay: delay,
repeatDelay: delay,
onRepeat: () => {
pointsRef.current = Math.floor(
Math.max(pointsRef.current * POINTS_MULTIPLIER, pointsMin)
)
},
})
return () => {
bobRef.current.kill()
}
}, [delay, pointsMin, speed])
我们也要创建一个ref ,为我们的GSAP动画保留一个参考。当Mole 被捶打时,我们将使用这个。注意我们还返回了一个函数,在卸载时杀死动画。如果我们不在卸载时杀死动画,重复代码就会继续发射。
当鼹鼠被捶打时,会发生什么?我们需要一个新的状态来解决这个问题。
const [whacked, setWhacked] = useState(false)
而不是使用我们的button 的onClick 中的道具onWhack ,我们可以创建一个新的函数whack 。这将把whacked 设置为true ,并以当前的pointsRef 值调用onWhack 。
const whack = () => {
setWhacked(true)
onWhack(pointsRef.current)
}
return (
<div className="mole-hole">
<button className="mole" ref={buttonRef} onClick={whack}>
Mole
</button>
</div>
)
最后要做的是用useEffect 来响应效果中的whacked 状态。使用依赖性数组,我们可以确保只有在whacked 发生变化时才运行这个效果。如果whacked 是true ,我们就重置点,暂停动画,并将Mole 在地下制作动画。一旦进入地下,我们在重新启动动画之前等待一个随机延迟。使用timescale ,动画的启动速度会加快,我们将whacked 设为false 。
useEffect(() => {
if (whacked) {
pointsRef.current = points
bobRef.current.pause()
gsap.to(buttonRef.current, {
yPercent: 100,
duration: 0.1,
onComplete: () => {
gsap.delayedCall(gsap.utils.random(1, 3), () => {
setWhacked(false)
bobRef.current
.restart()
.timeScale(bobRef.current.timeScale() * TIME_MULTIPLIER)
})
},
})
}
}, [whacked])
这就给了我们。
见笔者10。React to Whacksby@jh3y.
最后要做的是向我们的Mole 实例传递道具,这将使它们表现得不同。但是,我们如何生成这些道具可能会导致一个问题。
<div className="moles">
{new Array(MOLES).fill().map((_, id) => (
<Mole
key={id}
onWhack={onWhack}
speed={gsap.utils.random(0.5, 1)}
delay={gsap.utils.random(0.5, 4)}
points={MOLE_SCORE}
/>
))}
</div>
这将导致一个问题,因为当我们生成鼹鼠时,道具会在每次渲染时改变。一个更好的解决方案是,在我们每次启动游戏时生成一个新的Mole 数组,并在上面进行迭代。这样,我们就可以保持游戏的随机性,而不会造成问题。
const generateMoles = () => new Array(MOLES).fill().map(() => ({
speed: gsap.utils.random(0.5, 1),
delay: gsap.utils.random(0.5, 4),
points: MOLE_SCORE
}))
// Create state for moles
const [moles, setMoles] = useState(generateMoles())
// Update moles on game start
const startGame = () => {
setScore(0)
setMoles(generateMoles())
setPlaying(true)
setFinished(false)
}
// Destructure mole objects as props
<div className="moles">
{moles.map(({speed, delay, points}, id) => (
<Mole
key={id}
onWhack={onWhack}
speed={speed}
delay={delay}
points={points}
/>
))}
</div>
这就是结果!我已经为我们的按钮添加了一些样式和一些品种的鼹鼠。
我们现在有了一个在React中构建的完全有效的 "打地鼠 "游戏。我们花了不到200行的代码。在这个阶段,你可以把它带走并使其成为你自己的。按你喜欢的方式设计它,添加新的功能,等等。或者你可以坚持下去,我们可以把一些额外的东西放在一起!
追踪最高分
我们有一个工作的 "打地鼠",但我们如何跟踪我们取得的最高分数?我们可以使用一个效果,在每次游戏结束时把我们的分数写到localStorage 。但是,如果持久化的东西是一个普遍的需求呢。我们可以创建一个名为usePersistentState 的自定义钩子。这可以是一个围绕着useState 的包装器,它可以读/写到localStorage 。
const usePersistentState = (key, initialValue) => {
const [state, setState] = useState(
window.localStorage.getItem(key)
? JSON.parse(window.localStorage.getItem(key))
: initialValue
)
useEffect(() => {
window.localStorage.setItem(key, state)
}, [key, state])
return [state, setState]
}
然后我们可以在我们的游戏中使用它。
const [highScore, setHighScore] = usePersistentState('whac-high-score', 0)
我们使用它与useState 完全一样。我们还可以在游戏过程中,在适当的时候连接到onWhack ,设置一个新的高分。
const endGame = points => {
if (score > highScore) setHighScore(score) // play fanfare!
}
我们怎样才能知道我们的游戏结果是否是一个新的高分?另一个状态?很有可能。
奇思妙想的点缀
在这个阶段,我们已经涵盖了我们需要的一切。甚至包括如何制作你自己的自定义钩子。请自由发挥,把它变成你自己的。
还在犹豫吗?让我们创建另一个自定义钩子来为我们的游戏添加音频。
const useAudio = (src, volume = 1) => {
const [audio, setAudio] = useState(null)
useEffect(() => {
const AUDIO = new Audio(src)
AUDIO.volume = volume
setAudio(AUDIO)
}, [src])
return {
play: () => audio.play(),
pause: () => audio.pause(),
stop: () => {
audio.pause()
audio.currentTime = 0
},
}
}
这是一个用于播放音频的初级钩子实现。我们提供一个音频src ,然后我们得到播放它的API。我们可以在 "whac "鼹鼠的时候添加噪音。然后决定是,这是不是Mole 的一部分?它是我们传递给Mole 的东西吗?它是我们在onWhack 中调用的东西吗?
这些都是在组件驱动的开发中出现的决策类型。我们需要考虑到可移植性。另外,如果我们想让音频静音,会发生什么**?**我们怎样才能在全球范围内做到这一点?作为第一种方法,在Game 组件中控制音频可能更合理。
// Inside Game
const { play: playAudio } = useAudio('/audio/some-audio.mp3')
const onWhack = () => {
playAudio()
setScore(score + points)
}
这都是关于设计和决定。如果我们带入大量的音频,重命名play 变量可能会变得很繁琐。从我们的类似于钩子的useState 中返回一个数组,将允许我们为变量命名任何我们想要的东西。但是,也可能很难记住数组的哪个索引代表哪个API方法。
就这样吧!
足够让你开始你的React之旅了,而且我们还得做一些有趣的东西。我们确实涵盖了很多内容。
- 创建一个应用程序。
- JSX。
- 组件和道具。
- 创建定时器。
- 使用参考文献。
- 创建自定义钩子。
我们做了一个游戏!现在你可以用你的新技能来增加新的功能或使它成为你自己的。
我把它带到哪里了?在写这篇文章的时候,到目前为止是在这个阶段。
请看@jh3y的PenWhac-a-Mole w/ React &&GSAP。
下一步该怎么走!
我希望建造 "打地鼠 "能激励你开始你的React之旅。下一步是什么?如果你想深入研究,这里有一些资源的链接,其中一些是我在这一路上发现的有用资源。
- React文档
- "Making
setIntervalDeclarative With React Hooks," Dan Abramov - "如何用React Hooks获取数据," Robin Wieruch
- "何时使用
useMemo和useCallback," Kent C Dodds - 在Smashing Magazine上阅读更多React文章