您是否正在为您与React合作的职位进行技术面试做准备?我看到给受访者最常见的任务之一是:“构建一个简单的TIC TAC TOE游戏” 。
通常,您正在协调采访的个人将使您对技术面试中的期望有一个模糊的概述。如果他们提到实施一个简单的游戏,那么您就在正确的位置!
如果您只想查看最终结果,请前往此GitHub repository。
为什么将TIC-TAC TOE用于技术访谈?
与大多数技术访谈一样,TIC TAC TOE挑战使面试官有机会看到一些事情:
- 您如何看待构建功能
- 您对React的知识深度
- 您的交流
在面试中,您被要求建造任何东西,而不仅仅是tic-tac-toe,必须了解面试官在您身边并期待协作会议。
如果您陷入困境,提出问题!不确定提示吗? 询问它!您表明自己是一个不怕寻求帮助的团队合作者,您会给面试官提供与您合作的完整图片。
除了个性和协作技巧外,此测试是一个很好的方法来查看个人是否理解以下内容:
- 国家管理
- 受控组件
- 上下文api
- CSS和JSX技能
- 基本的JavaScript技能
解决方案可能是什么样的
您可以通过许多不同的方式来构建此游戏。在这里,我将带您浏览我认为非常简单的解决方案,并使用对React有很好理解的模式。
本教程将假定您的技术评估需要您使用Typescript,但是这些概念转化为JavaScript。
起点
通常在与基于React的挑战的技术访谈中,您将从基本的React应用程序开始。可能在诸如CoderPad或CodeSandbox的平台中。
起点通常包括:
- 运行React项目的基本设置
- 一个包含
App.tsx
和main.tsx
的src/
文件夹 - 可能会删除或忽略的其他一些文件
在大多数情况下,您不必担心启动文件,因为您的焦点将放在App.tsx
和您添加的新文件中。
建造游戏板
要开始构建游戏,我们将构建一个充当游戏板的组件。这是最终将呈现出来自Tic Tac Toe网格的所有正方形。
为此mkdir src/components
touch src/components/GameBoard.tsx
您可能会在在线编辑中,您必须在其中手动通过UI
手动创建这些编辑 GameBoard.tsx
文件将导出一个function component作为其默认导出:
// src/components/GameBoard.tsx
import React from 'react'
const GameBoard: React.FC = () => {
return <></>
}
export default GameBoard
此组件需要渲染3 x 3个正方形。我们将假设有一个根级App.css
文件,我们可以为本教程提供全局样式。
在App.css
中,添加以下内容:
/* src/App.css */
.gameboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-radius: 10px;
overflow: hidden;
}
这应该给我们一个类,将div
的内容作为3 x 3网格。
// src/components/GameBoard.tsx
import React from 'react'
const GameBoard: React.FC = () => {
return <div className="gameboard">
{/* Render grid here! */}
</div>
}
export default GameBoard
在这一点上,我们有一个GameBoard
组件,可以在游戏板上渲染一些网格项目。接下来,我们将解决这个问题。
构建平方分量
要启动将为每个网格项目渲染的组件,请在src/components
中创建一个名为Square.tsx
的新文件:
// src/components/Square.tsx
import React from 'react'
const Square: React.FC = () => {
return <></>
}
export default Square
此组件将渲染包含X或O的正方形。创建另一个类以样式的这些正方形:
/* src/App.css */
.square {
width: 100px;
height: 100px;
border: 1px solid gray;
background: #f1f1f1;
display: flex;
align-items: center;
justify-content: center;
}
然后将其用于新组件:
// src/components/Square.tsx
import React from 'react'
const Square: React.FC = () => {
return <div className="square"></div>
}
export default Square
在该div
标签中,您将在其中渲染x或o,具体取决于播放器单击正方形。
要为其未来用法准备此组件,请定义一些属性,您可以将其传递以指定选择它的用户和单击处理程序函数:
// src/components/Square.tsx
import React from 'react'
type props = {
user: string | null
onClick: () => void
}
const Square: React.FC<props> = ({ user, onClick }) => {
return <div className="square" onClick={onClick}>
{user}
</div>
}
export default Square
我们现在有一个游戏板,也可以在该板上渲染正方形。接下来我们需要一些结构来存储游戏数据并根据该数据渲染板。
在这一点上,我们实际上并没有渲染我们已建立的任何一个组件。那很快就会来了!
使用上下文API构建游戏状态
要处理本应用程序中的状态数据,您将使用React的Context API。此API为您提供了一种向应用程序提供全球状态管理的方法,使您可以轻松地跨组件共享数据。
要保持井井有条,上下文通常存储在自己的文件夹中。在src/
中创建一个名为contexts
的新文件夹,并在该新目录中的一个名为GameState.tsx
的文件:
mkdir src/contexts
touch src/contexts/GameState.tsx
此文件是我们将创建上下文的地方。将其打开并从创建新上下文并导出它开始:
// src/contexts/GameState.tsx
import React, { createContext } from 'react'
export type GameState = {
users: string[]
activeUser: string
selections: Array<string | null>
}
export const GameStateContext = createContext<GameState>({
users: ["x", "y"],
activeUser: null,
selections: [],
})
在上面的摘要中,我们正在导入createContext
函数,该功能使我们能够创建一个反应上下文。我们还创建了一种类型的GameState
来定义此上下文公开的属性。目前,它包含:
-
users
:一个包含两个玩家数据的数组 -
activeUser
:转弯的用户 -
selections
:每个单独网格项目的数据
在这一点上,我们有一个上下文,但是我们还需要导出一个使用provider :
提供该上下文的组件
// src/contexts/GameState.tsx
import React, { createContext, useState } from 'react'
// ...
export const GameStateProvider: React.FC = ({ children }) => {
const [activeUser, setActiveUser] = useState("x")
const [selections, setSelections] = useState<Array<string | null>>(
Array(9).fill(null)
)
return <GameStateContext.Provider value={{
users: ["x", "y"],
activeUser,
selections,
}}>
{children}
</GameStateContext.Provider>
}
请注意,useState
是从react
库中添加到导入的
此提供商组件将包装整个应用程序,使其全局访问其提供的数据。重要的是在这里注意:
-
children
参数,其中包含儿童组件最终将包装。 -
useState
Invications。我们正在用两种数据来初始化提供商的状态。activeUser
默认为"x"
和selections
,其中包含一个带有9
长度的空数组。
将添加到此文件中,但是现在让我们继续使用上下文来使用上下文,以便我们可以开始渲染游戏板。
使用游戏上下文
要使用上下文,我们将首先导入提供商组件并将整个应用程序包装在其中。
进入src/App.tsx
,并通过导入并将JSX内容包装到组件中来使用:
// src/App.tsx
import './App.css'
import { GameStateProvider } from './contexts/GameState'
function App() {
return (
<GameStateProvider>
{/* original contents */}
</GameStateProvider>
)
}
该GameStateProvider
组件中包含的任何内容都可以访问GameState
上下文。
GameBoard
组件将是此游戏的切入点。接下来导入并用该组件替换GameStateProvider
内部的内容:
// src/App.tsx
import { GameStateProvider } from './contexts/GameState'
import GameBoard from './components/GameBoard'
function App() {
return (
<GameStateProvider>
<GameBoard />
</GameStateProvider>
)
}
这将导致GameBoard
在屏幕上渲染,尽管该组件当前没有渲染任何正方形。
渲染游戏网格
如果您回想一下GameState
上下文初始化,您会记住我们用九个项目的数组初始化了状态,该项目代表板上的网格项目。为了渲染游戏的UI,我们将为每个网格项目提供一个正方形。
在src/components/GameBoard.tsx
中,使用useContext
访问上下文的数据并为selections
阵列中的每个项目渲染Square
组件:
// src/components/GameBoard.tsx
import React, { useContext } from 'react'
import Square from './Square'
import { GameStateContext } from '../contexts/GameState'
const GameBoard: React.FC = () => {
const { selections } = useContext(GameStateContext)
return <div className="gameboard">
{
selections.map(
(selection, i) =>
<Square
key={i}
user={selection}
onClick={null}
/>
)
}
</Container>
}
export default GameBoard
对于数组中的每个项目,您现在正在渲染Square
并将选择数据传递到该正方形。最终,这将保留单击该正方形的用户的名称。
如果我们看屏幕,我们将看到一个非常无组织的网格:
让我们通过将游戏板包装在App.tsx
中的新的div
并在App.css
中添加一些样式来解决此问题:
// src/App.tsx
// ...
function App() {
return (
<GameStateProvider>
<div className="container">
<GameBoard/>
</div>
</GameStateProvider>
)
}
export default App
/* src/App.css */
/* ... */
.container {
display: flex;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background: white;
font-family: roboto;
}
,通过这些调整,您现在应该看到一个格式的TIC TAC TOE GRID!
我们现在正在渲染TIC TAC TOE网格,并可以访问全球状态。您有所有需要开始使此电网功能功能的作品!
使玩家能够选择正方形
要开始,我们需要添加一种方法,并使其可以通过全局状态访问,该全局状态在用户单击正方形时会突变selections
数组。单击正方形也应将转弯切换到下一个球员。
在src/contexts/GameContext.tsx
中进行以下更改,以添加一个基于当前玩家选择正方形的函数,然后将转弯传递给下一个玩家:
// src/contexts/GameContext.tsx
import React, { createContext, useState } from 'react'
export type GameState = {
users: string[]
activeUser: string
selections: Array<string | null>
makeSelection: (squareId: number) => void // 👈🏻
}
export const GameStateContext = createContext<GameState>({
users: ["x", "o"],
activeUser: null,
selections: [],
makeSelection: null, // 👈🏻
})
export const GameStateProvider: React.FC = ({ children }) => {
const [activeUser, setActiveUser] = useState("x")
const [selections, setSelections] = useState<Array<string | null>>(
Array(9).fill(null)
)
// Allows a user to make a selection 👇🏻
const makeSelection = (squareId: number) => {
// Update selections
setSelections(selections => {
selections[squareId] = activeUser
return [...selections]
})
// Switch active user to the next user's turn
setActiveUser(activeUser === 'x' ? 'o' : 'x')
}
return <GameStateContext.Provider value={{
users: ["x", "o"],
activeUser,
selections,
makeSelection // 👈🏻
}}>
{children}
</GameStateContext.Provider>
}
请注意手指向的位置!这些是所做的更改。
这是一个大型代码,所以让我们看看发生了什么变化:
-
GameState
类型需要一个名为makeSelection
的新属性来定义我们添加的功能 -
GameStateContext
需要对makeSelection
的初始值 - 提供商定义了
makeSelection
的功能 -
makeSelection
函数已提供给提供商的value
属性,将其暴露于使用该上下文的任何组件
makeSelection
函数本身有两件事。它采用squareId
,即selections
数组中的索引,并使用它用当前播放器的名称来更新该索引的值。然后,它将activeUser
设置为哪个玩家不仅会选择。
结果,此功能允许用户选择一个正方形并传递转弯。剩下的就是要使用它。进入GameBoard
组件,然后将其传递给渲染的Square
组件的onClick
处理程序:
// src/components/GameBoard.tsx
// ...
const GameBoard: React.FC = () => {
const {
selections,
makeSelection // 👈🏻
} = useContext(GameStateContext)
return <div className="gameboard">
{
selections.map(
(selection, i) =>
<Square
key={i}
user={selection}
onClick={() => makeSelection(i)} // 👈🏻
/>
)
}
</div>
}
export default GameBoard
现在,当单击网格上的正方形时,将显示当前播放器的符号:
剩下的就是添加功能以检查获胜者并在找到获胜者后重置游戏。
检查获胜者
当玩家选择正方形时,函数应触发,以检查是否有胜利组合。
为此,我们将在GameContextProvider
组件中使用useEffect
。每当selections
发生变化时,效果都会发射为获胜者触发支票。
进行以下更改以实现这一目标:
// src/contexts/GameState.tsx
import React, { createContext, useState, useEffect } from 'react'
// ...
export const GameStateProvider: React.FC = ({ children }) => {
const [selections, setSelections] = useState<Array<string | null>>(
Array(9).fill(null)
)
// ...
// 👇🏻
useEffect(() => {
// checkForWinner()
}, [selections])
return <GameStateContext.Provider value={{
users: ["x", "o"],
activeUser,
selections,
makeSelection
}}>
{children}
</GameStateContext.Provider>
}
记住要导入useEffect
!
每当选择网格项目(或更具体地说,每当selections
变量更改时)都会运行useEffect
回调的代码。
我们需要添加以处理赢家的方法是添加一个新的状态变量,并通过名为winner
的提供商提供它。当我们检查获胜者并找到一个时,播放器将存储在该状态变量中。
添加以下内容:
// src/contexts/GameState.tsx
import React, { createContext, useState, useEffect } from 'react'
// ...
export type GameState = {
users: string[]
activeUser: string
selections: Array<string | null>
makeSelection: (squareId: number) => void
winner: string | null // 👈🏻
}
export const GameStateProvider: React.FC = ({ children }) => {
// ...
const [winner, setWinner] = useState(null) // 👈🏻
// ...
return <GameStateContext.Provider value={{
users: ["x", "o"],
activeUser,
selections,
makeSelection,
winner // 👈🏻
}}>
{children}
</GameStateContext.Provider>
}
现在有一个状态可以跟踪是否选择了赢家,以及谁是谁。处理获胜者的最后一块是读取游戏板并找到三个匹配选择的行的实际功能。
添加以下函数并取消点击useEffect
回调的内容:
// src/contexts/GameState.tsx
import React, { createContext, useState, useEffect } from 'react'
// ...
export const GameStateProvider: React.FC = ({ children }) => {
// ...
const checkForWinner = () => {
const winningCombos = [
[0,1,2],
[3,4,5],
[6,7,8],
[0,3,6],
[1,4,7],
[2,5,8],
[0,4,8],
[6,4,2]
]
winningCombos.forEach( combo => {
const code = combo.reduce(
(acc, curr) => `${acc}${selections[curr]}`,
''
)
if ( ['xxx', 'yyy'].includes(code) )
setWinner(code[0])
}
)
}
useEffect(() => {
checkForWinner()
}, [selections])
return <GameStateContext.Provider value={{
users: ["x", "o"],
activeUser,
selections,
makeSelection,
winner
}}>
{children}
</GameStateContext.Provider>
}
当玩家赢得比赛时,winner
状态变量将被更新以包含获胜播放器的符号。目前,没有迹象表明有赢家。让我们解决这个问题并结束游戏。
展示赢家并重置董事会
要显示获胜者,我们可以使用新的winner
变量,该变量可通过上下文提供商访问。
在GameBoard
组件中,添加一个useEffect
,该useEffect
观察更改winner
变量。当该变量更新并包含获胜者时,显示一个警报,表示哪个播放器赢了:
// src/components/GameBoard.tsx
// 👇🏻
import React, { useContext, useEffect } from 'react'
// ...
const GameBoard: React.FC = () => {
// 👇🏻
const { selections, makeSelection, winner } = useContext(GameStateContext)
// 👇🏻
useEffect(() => {
if ( winner !== null ) {
alert(`Player ${winner.toUpperCase()} won!`)
}
}, [winner])
return <div className="gameboard">
{/* ... */}
</div>
}
export default GameBoard
不要忘记导入useEffect
!
如果玩家选择三个,您现在应该看到一个警报,表示用户赢了!
一旦解散该警报,游戏板就不会改变。目前无法重新开始(除了刷新浏览器外)。
在GameState
上下文中添加并公开一个新功能,该函数将状态变量重置为其原始值:
// src/contexts/GameState.tsx
import React, { createContext, useEffect, useState } from 'react'
export type GameState = {
// ...
reset: () => void
}
export const GameStateContext = createContext<GameState>({
// ...
reset: null
})
export const GameStateProvider: React.FC = ({ children }) => {
// ...
const reset = () => {
setSelections(Array(9).fill(null))
setWinner(null)
setActiveUser('x')
}
return <GameStateContext.Provider value={{
// ...
reset
}}>
{children}
</GameStateContext.Provider>
}
此功能将完全重置游戏,应在GameBoard
组件中的alert
之后直接运行:
// src/components/GameBoard.tsx
import React, { useContext, useEffect } from 'react'
const GameBoard: React.FC = () => {
// 👇🏻
const { selections, makeSelection, winner, reset } = useContext(GameStateContext)
useEffect(() => {
if ( winner !== null ) {
alert(`Player ${winner.toUpperCase()} won!`)
// 👇🏻
reset()
}
}, [winner])
return <div className="gameboard">
{/* ... */}
</div>
}
export default GameBoard
现在,当您解散alert
时,您应该看到游戏板已重置并准备好进行新游戏!
闭幕词
我真的很喜欢这项任务,因为乍一看似乎很简单,它使面试官可以看到您了解诸如:
的概念- 可重复使用的组件
- 全球国家管理
- jsx as>
- 基本逻辑
- ...等等!
如果您在上面的教程中遇到麻烦,也可以参考完整项目的GitHub repository。
非常感谢您的阅读和好运采访!