今天我们将学习如何使用Next.js和Netlify建立一个网球小游戏应用程序。这个技术栈已经成为我在许多项目中的首选。它允许快速开发和轻松部署。
不多说了,让我们开始吧
我们在使用什么
- Next.js
- Netlify
- TypeScript
- 尾风CSS
为什么是Next.js和Netlify
你可能认为这是一个简单的应用程序,可能不需要React框架。事实上,Next.js为我提供了大量开箱即用的功能,让我可以直接开始编写应用程序的主要部分。像webpack配置、getServerSideProps ,以及Netlify的自动创建无服务器函数就是几个例子。
Netlify还使Next.js git repo的部署变得非常容易。稍后会有更多关于部署的内容。
我们要构建的东西
基本上,我们将建立一个小游戏,随机向你显示一个网球运动员的名字,你必须猜出他们来自哪个国家。它由五轮比赛组成,并记录你猜对了多少的分数。
我们在这个应用程序中需要的数据是一个球员的名单,以及他们的国家。起初,我想查询一些实时API,但转念一想,决定只使用本地的JSON文件。我从RapidAPI中获取了一个快照,并将其包含在启动程序库中。
最终的产品看起来是这样的。

初始版本浏览
如果你想跟着做,你可以克隆这个仓库,然后去start 分支。
git clone git@github.com:brenelz/tennis-trivia.git
cd tennis-trivia
git checkout start
在这个初始版本中,我继续写了一些模板,让事情顺利进行。我使用npx create-next-app tennis-trivia 命令创建了一个 Next.js 应用程序。然后,我继续手动更改了几个 JavaScript 文件,以.ts 和.tsx 。令人惊讶的是,Next.js会自动识别出我想使用TypeScript。这真是太容易了!我还以这篇文章为指导,继续配置了Tailwind CSS。
说得够多了,让我们来写代码吧
初始设置
第一步是设置环境变量。对于本地开发,我们用一个.env.local 文件来做这个。你可以从启动器 repo中复制.env.sample 。
cp .env.sample .env.local
注意它目前有一个值,就是我们应用程序的路径。我们将在我们应用程序的前端使用它,所以我们必须在它前面加上NEXT_PUBLIC_ 。
最后,让我们使用下面的命令来安装依赖项并启动开发服务器。
npm install
npm run dev
现在我们访问我们的应用程序:http://localhost:3000 。我们应该看到一个相当空的页面,只有一个标题。

创建用户界面标记
在pages/index.tsx ,让我们在现有的Home() 函数中添加以下标记。
export default function Home() {
return (
<div className="bg-blue-500">
<div className="max-w-2xl mx-auto text-center py-16 px-4 sm:py-20 sm:px-6 lg:px-8">
<h2 className="text-3xl font-extrabold text-white sm:text-4xl">
<span className="block">Tennis Trivia - Next.js Netlify</span>
</h2>
<div>
<p className="mt-4 text-lg leading-6 text-blue-200">
What country is the following tennis player from?
</p>
<h2 className="text-lg font-extrabold text-white my-5">
Roger Federer
</h2>
<form>
<input
list="countries"
type="text"
className="p-2 outline-none"
placeholder="Choose Country"
/>
<datalist id="countries">
<option>Switzerland</option>
</datalist>
<p>
<button
className="mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"
type="submit"
>
Guess
</button>
</p>
</form>
<p className="mt-4 text-lg leading-6 text-white">
<strong>Current score:</strong> 0
</p>
</div>
</div>
</div>
);
这就构成了我们的用户界面的支架。正如你所看到的,我们使用了很多来自Tailwind CSS的实用类,使事情看起来更漂亮。我们也有一个简单的自动完成的输入和一个提交按钮。在这里,你将选择你认为玩家来自的国家,然后点击按钮。最后,在底部,有一个分数,根据正确或错误的答案而改变。
设置我们的数据
如果你看一下data 文件夹,应该有一个tennisPlayers.json ,里面有我们这个应用程序需要的所有数据。在根目录下创建一个lib 文件夹,并在其中创建一个players.ts 文件。记住,.ts 的扩展名是必需的,因为这是一个 TypeScript 文件。让我们定义一个符合我们JSON数据的类型。
export type Player = {
id: number,
first_name: string,
last_name: string,
full_name: string,
country: string,
ranking: number,
movement: string,
ranking_points: number,
};
这就是我们如何在TypeScript中创建一个类型。我们在左边有属性的名称,在右边有它的类型。它们可以是基本类型,甚至是其他类型本身。
从这里开始,让我们创建具体的变量来代表我们的数据。
export const playerData: Player[] = require("../data/tennisPlayers.json");
export const top100Players = playerData.slice(0, 100);
const allCountries = playerData.map((player) => player.country).sort();
export const uniqueCountries = [...Array.from(new Set(allCountries))];
需要注意的几件事是,我们说我们的playerData 是一个Player 类型的数组。这是由冒号跟类型表示的。事实上,如果我们将鼠标悬停在playerData ,我们可以看到其类型。

在最后一行,我们得到了一个独特的国家列表,用于我们的国家下拉菜单。我们将我们的国家传递到一个JavaScript Set中,这样就可以摆脱重复的值。然后我们从中创建一个数组,并将其分散到一个新的数组中。这似乎是不必要的,但这样做是为了让TypeScript高兴。
信不信由你,这确实是我们的应用程序所需要的全部数据
让我们把我们的用户界面变成动态的!
目前我们所有的值都是硬编码的,但让我们改变一下。动态的部分是网球运动员的名字、国家列表和分数。
回到pages/index.tsx ,让我们修改我们的getServerSideProps 函数,以创建一个五个随机球员的列表,以及拉入我们的uniqueCountries 变量。
import { Player, uniqueCountries, top100Players } from "../lib/players";
...
export async function getServerSideProps() {
const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());
const players = randomizedPlayers.slice(0, 5);
return {
props: {
players,
countries: uniqueCountries,
},
};
}
我们返回的props 对象中的任何内容都将被传递给我们的React组件。让我们在我们的页面上使用它们。
type HomeProps = {
players: Player[];
countries: string[];
};
export default function Home({ players, countries }: HomeProps) {
const player = players[0];
...
}
正如你所看到的,我们为我们的页面组件定义了另一种类型。然后我们将HomeProps 类型添加到Home() 函数中。我们再次指定players 是一个Player 类型的数组。
现在我们可以在我们的用户界面中进一步使用这些道具。用{player.full_name} 替换 "Roger Federer"(顺便说一下,他是我最喜欢的网球运动员)。你应该在播放器变量上得到很好的自动完成,因为它列出了我们可以访问的所有属性名称,因为我们定义了这些类型。

再往下看,现在让我们把国家的列表更新为这个。
<datalist id="countries">
{countries.map((country, i) => (
<option key={i}>{country}</option>
))}
</datalist>
现在,我们已经有了三个动态部分中的两个,我们需要解决分数问题。具体来说,我们需要为当前的分数创建一个状态。
export default function Home({ players, countries }: HomeProps) {
const [score, setScore] = useState(0);
...
}
一旦完成,在我们的用户界面中用{score} 替换0。
现在你可以通过访问http://localhost:3000 来检查我们的进展。你可以看到,每次页面刷新时,我们都会得到一个新的名字;当在输入框中输入时,它会列出所有可用的独特国家。
增加一些互动性
我们已经有了一个很好的方法,但我们需要添加一些互动性。
挂上猜测按钮
为此,我们需要有一些方法来了解被选中的国家。我们通过添加更多的状态并将其附加到我们的输入字段来做到这一点。
export default function Home({ players, countries }: HomeProps) {
const [score, setScore] = useState(0);
const [pickedCountry, setPickedCountry] = useState("");
...
return (
...
<input
list="countries"
type="text"
value={pickedCountry}
onChange={(e) => setPickedCountry(e.target.value)}
className="p-2 outline-none"
placeholder="Choose Country"
/>
...
);
}
接下来,让我们添加一个guessCountry 函数,并将其附加到表单提交。
const guessCountry = () => {
if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {
setScore(score + 1);
} else {
alert(‘incorrect’);
}
};
...
<form
onSubmit={(e) => {
e.preventDefault();
guessCountry();
}}
>
我们所做的基本上是比较当前玩家的国家和猜测的国家。现在,当我们回到应用程序,猜对了国家,分数就会如期增加。
添加一个状态指示器
为了使这一切变得更漂亮,我们可以根据猜测是否正确来渲染一些UI。
所以,让我们为状态创建另一块状态,并更新猜测国家的方法。
const [status, setStatus] = useState(null);
...
const guessCountry = () => {
if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {
setStatus({ status: "correct", country: player.country });
setScore(score + 1);
} else {
setStatus({ status: "incorrect", country: player.country });
}
};
然后在玩家名字下面渲染这个UI。
{status && (
<div className="mt-4 text-lg leading-6 text-white">
<p>
You are {status.status}. It is {status.country}
</p>
<p>
<button
autoFocus
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"
>
Next Player
</button>
</p>
</div>
)}
最后,我们要确保当我们处于正确或不正确的状态时,我们的输入栏不会显示。我们通过用下面的方法包装表单来实现这个目的。
{!status && (
<form>
...
</form>
)}
现在,如果我们回到应用中去猜测球员的国家,我们会得到一个漂亮的消息,说明猜测的结果。
在玩家中进展
现在可能是最具挑战性的部分了。我们如何从一个玩家到下一个玩家?
我们需要做的第一件事是将currentStep ,这样我们就可以用一个从0到4的数字来更新它。然后,当它达到5时,我们要显示一个完成的状态,因为这个小游戏已经结束了。
再一次,让我们添加以下状态变量。
const [currentStep, setCurrentStep] = useState(0);
const [playersData, setPlayersData] = useState(players);
...然后将我们之前的播放器变量替换为。
const player = playersData[currentStep];
接下来,我们创建一个nextStep 函数,并将其与用户界面挂钩。
const nextStep = () => {
setPickedCountry("");
setCurrentStep(currentStep + 1);
setStatus(null);
};
...
<button
autoFocus
onClick={nextStep}
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"
>
Next Player
</button>
现在,当我们做出猜测并点击下一步按钮时,我们会被带到一个新的网球运动员那里。再猜一次,我们就会看到下一个,以此类推。
当我们在最后一个球员上按下一步时会发生什么?现在,我们得到一个错误。让我们通过添加一个表示游戏已经完成的条件来解决这个问题。当player变量未被定义时就会发生这种情况。
{player ? (
<div>
<p className="mt-4 text-lg leading-6 text-blue-200">
What country is the following tennis player from?
</p>
...
<p className="mt-4 text-lg leading-6 text-white">
<strong>Current score:</strong> {score}
</p>
</div>
) : (
<div>
<button
autoFocus
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto"
>
Play Again
</button>
</div>
)}
现在我们在游戏结束时看到一个漂亮的完成状态。
再次播放按钮
我们几乎已经完成了对于我们的 "重玩 "按钮,我们想重置游戏的所有状态。我们还想从服务器上获得一个新的玩家列表,而不需要刷新。我们是这样做的。
const playAgain = async () => {
setPickedCountry("");
setPlayersData([]);
const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + "/api/newGame"
);
const data = await response.json();
setPlayersData(data.players);
setCurrentStep(0);
setScore(0);
};
<button
autoFocus
onClick={playAgain}
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto"
>
Play Again
</button>
注意我们正在使用我们之前通过process.env 对象设置的环境变量。我们还通过用我们刚刚检索到的客户端状态覆盖我们的服务器状态来更新我们的playersData 。
我们还没有填写我们的newGame 路由,但这在Next.js和Netlify的无服务器功能中是很容易的。我们只需要编辑pages/api/newGame.ts 中的文件。
import { NextApiRequest, NextApiResponse } from "next"
import { top100Players } from "../../lib/players";
export default (req: NextApiRequest, res: NextApiResponse) => {
const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());
const top5Players = randomizedPlayers.slice(0, 5);
res.status(200).json({players: top5Players});
}
这看起来和我们的getServerSideProps 基本相同,因为我们可以重复使用我们漂亮的辅助变量。
如果我们回到应用程序,注意到 "再玩一次 "按钮如期工作。
改进焦点状态
我们可以做的最后一件事是在每次改变步骤时将焦点设置在国家输入栏上,以改善我们的用户体验。这只是一个很好的提示,对用户来说也很方便。我们用一个ref 和一个useEffect 来做这个。
const inputRef = useRef(null);
...
useEffect(() => {
inputRef?.current?.focus();
}, [currentStep]);
<input
list="countries"
type="text"
value={pickedCountry}
onChange={(e) => setPickedCountry(e.target.value)}
ref={inputRef}
className="p-2 outline-none"
placeholder="Choose Country"
/>
现在我们只需使用回车键和输入一个国家,就可以更容易地进行导航。
部署到Netlify
你可能想知道我们如何部署这个东西。那么,使用Netlify就很简单了,因为它可以检测到一个开箱即用的Next.js应用程序,并自动进行配置。
我所做的就是建立一个GitHub repo,并将我的GitHub账户连接到我的Netlify账户。在那里,我只需选择一个 repo 来部署,并使用所有的默认值。

需要注意的是,你必须添加NEXT_PUBLIC_API_URL 环境变量并重新部署才能生效。

另外请注意,你可以直接点击GitHub repo上的 "Deploy to Netlify "按钮。
结论
呜呼,你成功了!这是一个旅程,我希望你在这一路上学到了一些关于React、Next.js和Netlify的东西。