👥 从候选人到同事:应对竞争激烈的初级招聘挑战📋
你好呀!
TL;DR
在本文中,我们想通过教授一些常见的初级面试技能来强调编程挑战的重要性:构建一个全栈 CRUD 应用(我们将使用Wasp 框架及其技术栈——React、Node 和 Prisma 来实现)。该应用专注于开发一个 Web 应用,用于根据宝可梦的经验值 (XP) 进行交易和评级,并使用PokéAPI获取所需的数据。
我们还将讨论招聘人员在招聘新开发人员时关注的重要因素,例如代码质量、可读性、技术栈和架构选择,以及更现代化的代码生成 AI 方法。如果您想提升编程技能并在招聘过程中取得成功,这篇文章将对您大有裨益!
您可以在此处找到应用程序的部署版本,并在此处找到包含最终代码的存储库!
开始之前
在本文中,我将使用Wasp 框架。它通过其 AI 应用生成器提供了一种启动项目的好方法,并且当您想要创建一个包含所有功能(前端、后端和数据库)的全栈 Web 应用程序时,它提供了非常棒的开发体验。
我目前是 Wasp 团队的成员,由于它也是一个开源项目,所以您也可以做出贡献!
表达支持最简单的方法就是为我们的仓库点赞!🐝 但如果您能访问我们的仓库(无论是贡献代码还是简单测试产品),我们将不胜感激!
介绍
目前,在科技招聘领域,编程挑战已成为测试应聘者技术技能和解决问题能力的常用方法。在这篇博文中,我们将探讨编程挑战在招聘过程中的重要性,并使用Wasp 框架自行创建一个编程挑战,以便您在面对编程挑战时能够提升成绩!
为了达到这个目的,我们会模拟一个初级角色的挑战,并亲自承担。通常我们会有一些时间来做这件事(通常一周左右),但为了公平起见,我会把计时器设置为4小时,以便强制犯一些错误,给自己施加压力。
以下是我的最终统计数据👀。
了解招聘人员的期望和要求对于提高你在就业市场上的成功率至关重要。此外,最后我会从高级开发人员的角度评估这个项目,这样你就可以了解我希望在这个项目中看到的所有改进!
招聘挑战概述
太棒了!现在我们已经设定好了背景,让我来解释一下挑战的内容:
我们将实现一个 Web 应用程序,用户必须能够执行以下操作:
- 有两面,每面有 6 个 Pokémon 插槽
- 在两侧设置神奇宝贝(每个插槽一个神奇宝贝)
- 将双方的当前状态注册为交易
- 查看所有已注册交易的历史记录
- 获取交易状态的实时信息:
- 如果任何一方的每个神奇宝贝的经验值总和与另一方相差 10%,这是不公平的(但用户仍然可以注册)
- 否则,这是公平交易
通过这些挑战,招聘人员希望评估候选人当前的一些技能:
- 代码质量:招聘人员希望评估候选人编写简洁、易懂且结构良好的代码的能力。这包括遵循最佳实践、使用适当的命名约定以及将代码组织成可重用的组件或函数。
- 解决问题的能力:招聘人员希望评估应聘者分析问题、将问题分解成更小的任务并提出有效解决方案的能力。这可能涉及逻辑思维、算法设计和调试技能。
- 技术知识:招聘人员希望评估候选人对与工作相关的编程语言、框架和工具的了解。这包括 JavaScript 等语言、React 或 Node.js 等框架,以及 Git 等工具。
- 沟通能力:招聘人员希望评估候选人与团队成员和利益相关者有效沟通的能力。这包括解释技术概念、开展项目协作以及提供清晰的文档。
我们将在本文中概括讨论所有这些内容,但是,我将在最后留下一些链接,以便进一步了解所有这些主题!
为了完成这项挑战,我们将使用PokéAPI的接口来获取宝可梦的数据和经验值。以下是最终效果的预览(如果你参与编程,可以将其视为你的原型):
在这些图像中,我们可以清楚地看到一个交易区,可以插入神奇宝贝并注册一些交易!
设置我们的 repo
因此,首先,让我们转到Wasp 的 GPT 应用程序生成器,以获得一种启动项目的绝妙方法,因为它使用 GPT 和 Wasp 框架来生成连贯的全栈 Web 应用程序。
打开网页后,我开始描述应用程序的基本功能。我尽量避免描述得太具体(这在AI生成中通常不推荐),但这样做的目的是为了测试AI是否能在提示不完美的情况下给我们一个良好的开端。以下是我使用的提示:
开发一个网页应用,用户可以为两个阵营选择宝可梦:交易区域 A 和交易区域 B。选择后,应用会根据双方经验值总和来评估交易是否公平(10% 或更高的差异被视为不公平)。用户还可以将交易记录添加到历史记录中,并在个人资料页面上查看。
有了这个提示,人工智能就能生成这个结果:
最终成果令人印象深刻,Wasp 框架通过使用配置文件 ( main.wasp
) 和源文件来描述应用的细节和逻辑,简化了 Web 应用的开发。编译 Wasp 项目时,编译器会创建前端、后端和部署的完整源代码。
正是这种方法使 Wasp 能够使用 AI 生成器创建整个项目(这正是我们在这里所做的),同时保持一致性和质量。生成器会检查提示,并根据用户的需求生成组件、路由、操作和查询的代码。现在,让我们检查并评估一些已创建的代码和结构:
- 前端✅:
- 它生成了登录和注册路线和页面(作为参考,这非常棒,以至于我只对它们做了一些小的样式更改;它开箱即用!)✅
- 它生成了三个页面:
- 主页 - 只是一系列引用其他页面的按钮✅
- 个人资料-这是用户个人资料页面✅
- 交易 - 这是用户可以注册新交易的页面✅
- 后端⚠️:
- 它生成了两个操作(它们只是与我们的后端交互的函数):evaluateTrade 和 registerTrade — — 这正是应用程序需要的两个操作✅
- 它生成了两个查询:getPokémon 和 getUserTrade - getUserTrade 从一开始就是完美的,而 getPokémon 需要进行一些调整(它误解了数据库层的一些东西,我们可以在下面检查)⚠️
- 数据库⚠️:
- 最让人头疼的地方就在这里——总体来说,思路是对的,但在这个例子中,由于我们并没有在数据库中创建每一个宝可梦(只是从 API 中获取它们),所以使用宝可梦模型的思路并不正确。其他一切都基本正常!⚠️
执行
所以,只需使用这个AI 生成器,我们就能创建一个令人惊叹的开端,让它运行起来。之后,只需下载生成的应用程序,用它初始化一个存储库,并按照Wasp 的入门文档操作即可。
要安装 Wasp 的框架,我们可以运行以下命令:
` curl -sSL https://get.wasp-lang.dev/installer.sh | sh`
为了获得更好的开发体验,我们也可以下载VS Code 的 Wasp 扩展!
顺便说一句:如果您只想检查代码结果,这里是它的Github 存储库链接!
在设置了基本结构并准备好开发项目之后,就可以开始开发我们的应用程序了!
那么,首先,让我们获取一些宝可梦,因为它们的信息在应用程序中随处可见!例如,我们可以选择使用fetch API来获取每个端点,但我将利用专门针对宝可梦的 npm 库来处理这个问题。
您只需将此依赖项(及其版本)添加到您的 main.wasp 文件即可将其添加到您的 wasp 项目中。
//main.wasp
//adding the dependencies array
app Poketrader {
wasp: {
version: "^0.11.1"
},
title: "Poketrader",
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
},
dependencies: [
("pokedex-promise-v2","^4.1.1")
],
...
}
现在,我们添加了这个依赖项,是时候获取它们本身了,因此为了做到这一点,我将 getPokémon 函数重构为以下内容:
//src/server/queries.js
// a simples async await fetch to get the pokemon data
async function loadPokemons() {
const P = new Pokedex();
const response = await P.getGenerationByName("generation-i");
return response.pokemon_species;
}
export const getPokemon = async () => {
const pokemons = await loadPokemons();
return pokemons;
};
基本上,我们只获取第一代的神奇宝贝(这是我最熟悉的,但如果你愿意,也可以选择其他的🤣)并将它们返回到前端。
在前端使用这个变量,我们可以<select>
用所有神奇宝贝填充我们的选项,并添加一些条件逻辑来处理保存所选神奇宝贝。
//src/client/pages/Trade.jsx
//creating a state to hold all selected pokemons
//mapping all pokemons to options for our selector
//adding the onChange method to insert selected pokemons in our state
const [selectedPokemon, setSelectedPokemon] = useState({
areaA: null,
areaB: null,
});
...
return(
<h3>Trade Area A</h3>
<select
onChange={(e) => {
setSelectedPokemon({ ...selectedPokemon, areaA: e.target.value });
}}
>
<option disabled selected value>
-- select an option --
</option>
{pokemons.map((pokemon) => (
<option key={pokemon.name} value={pokemon}>
{pokemon.name}
</option>
))}
</select>
)
这样就能正确生成我们选择的宝可梦了。代码如下:
之后,我让这个组件更加通用,以便可以在交易区 B 中复用。我还修改了 loadPokémons 函数,使其不仅能检索宝可梦的信息,还能检索它们的图像和经验值。最终结果如下所示:
//src/server/queries.js
//refactoring loadPokemons to also get images and base_experience values
async function loadPokemons() {
const P = new Pokedex();
const response = await P.getGenerationByName("generation-i");
const pokemons = response.pokemon_species;
const pokemonsWithImages = await Promise.all(
pokemons.map(async (pokemon) => {
const pokemonData = await P.getPokemonByName(pokemon.name);
return {
id: pokemonData.id,
name: pokemonData.name,
base_experience: pokemonData.base_experience,
image: pokemonData.sprites.front_default,
};
})
);
return pokemonsWithImages;
}
export const getPokemon = async () => {
const pokemons = await loadPokemons();
return pokemons;
};
因此,我们为每个宝可梦发出了额外的请求,以检索所有必要的数据,包括图像和 base_experience。现在,我们可以在前端组件上调用这些图像,以便将它们显示给用户!
因此,让我们为这两个贸易区创建一个通用组件(因为它们有很多共同点)。首先,创建一个新组件(我们称之为TradeArea.jsx
),然后我们可以添加一些内容。
我们将添加一个标题(对于该区域来说必须是动态的)和一个带有 Pokémon 选项的选择元素。
此外,我们应该添加一个新按钮,用于设置与当前宝可梦的交易区域(即所选宝可梦的数组),同时保留旧宝可梦。我们还应该添加一个部分来显示当前的交易区域,并显示所选宝可梦的图像和名称。
//src/client/components/TradeArea.jsx
// adding those imgs that the back-end now sends
// adding a button to add to the trade area
// also adding a max size of pokémons on each side (6)
import React from "react";
function TradeArea({
pokemons,
tradeAreaName,
setSelectedPokemon,
selectedPokemon,
setTradeArea,
tradeArea,
}) {
const displayName =
tradeAreaName === "areaA" ? "Trade Area A" : "Trade Area B";
return (
<div>
<h3 className="text-xl">{displayName}</h3>
{tradeArea.map((pokemon) => (
<div className="grid grid-flow-col items-center">
<p>
Pokemon: <strong className="capitalize"> {pokemon.name} </strong>
</p>
<img className="w-10" src={pokemon.image} alt={pokemon.name} />
</div>
))}
<div className="grid gap-2">
<select
className="border border-gray-400 rounded-md"
onChange={(e) => {
const targetPokemon = pokemons.find(
(pokemon) => pokemon.name === e.target.value
);
setSelectedPokemon({
...selectedPokemon,
[tradeAreaName]: targetPokemon,
});
}}
>
<option disabled selected value>
-- select an option --
</option>
{pokemons.map((pokemon) => (
<option key={pokemon.name} value={pokemon.name}>
{pokemon.name}
</option>
))}
</select>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={() => {
if (!selectedPokemon[tradeAreaName]) return;
if (tradeArea.length < 6) {
setTradeArea([...tradeArea, selectedPokemon[tradeAreaName]]);
}
}}
>
Add to {displayName}
</button>
</div>
</div>
);
}
export default TradeArea;
它的外观如下(但您可以根据需要改进样式):
现在我们已经获得了宝可梦的基本经验值和图像,可以开始评估这些交易了。这需要将双方每只宝可梦的经验值相加,确定较低的总数,并将其设置为 10% 差额的最大阈值。
为了计算总和,我使用了 reduce 函数。如果交易公平,则返回“公平”;如果不公平,则返回“不公平”。如下所示:
//src/server/actions.js
export const evaluateTrade = async (args) => {
const pokemonAList = args.tradeAreaA;
const pokemonBList = args.tradeAreaB;
const totalA = pokemonAList.reduce(
(acc, pokemon) => acc + pokemon.base_experience,
0
);
const totalB = pokemonBList.reduce(
(acc, pokemon) => acc + pokemon.base_experience,
0
);
const difference = Math.abs(totalA - totalB);
const lowerTotal = Math.min(totalA, totalB);
const fairThreshold = lowerTotal * 0.1;
if (difference < fairThreshold) {
return "fair";
} else {
return "unfair";
}
};
现在,在我们的前端,我已经实现了一项功能,每次贸易区域发生变化时,都会使用 useEffect 钩子重新计算公平性。
//src/client/pages/Trade.jsx
// if the tradeAreaA or tradeAreaB changes, useEffect will execute handleEvaluteTrade()
const handleEvaluateTrade = async () => {
const result = await evaluateTradeFn({
tradeAreaA: tradeAreaA,
tradeAreaB: tradeAreaB,
});
setFairness(result);
};
useEffect(() => {
if (tradeAreaA.length > 0 && tradeAreaB.length > 0) {
handleEvaluateTrade();
}
}, [tradeAreaA, tradeAreaB]);
并且我让“公平”和“不公平”这两个词(采用不同的样式 - 绿色代表公平,红色代表不公平)也出现在用户界面中。
//src/client/pages/Trade.jsx
<h2
className={`text-2xl mb-4 font-bold ${
fairness === "fair" ? "text-green-500" : "text-red-500"
} `}
>
Trade {fairness && `- is ${fairness}`}
</h2>
到这里,我们的应用已经完成了将近 60%!它目前允许用户检索宝可梦,将它们添加到两个不同的交易区域,并判断交易是否公平。现在,为了完成最后的 40%,我们需要实现在用户历史记录中记录交易的功能。
要做到这一点:
- 首先,添加一个将调用该
registerTrade
函数的按钮。
//src/client/pages/Trade.jsx
<button
onClick={handleRegisterTrade}
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mt-4"
>
Register Trade
</button>
- 然后,我们定义注册交易的函数:
//src/client/pages/Trade.jsx
import registerTrade from "@wasp/actions/registerTrade";
const handleRegisterTrade = async () => {
await registerTrade({
tradeAreaA: tradeAreaA,
tradeAreaB: tradeAreaB,
fairness: fairness,
});
setTradeRegisteredStatus(true);
};
现在,我们只需要对数据库进行一些小改动,以支持这些 tradeArea 对象。我们还需要验证将它们注册到数据库中的函数。
首先,让我们对数据库进行以下更改:
//main.wasp
//added the tradeAreaA and tradeAreaB fields as String type (we will JSON.stringify()
//they later)
entity Trade {=psl
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
tradeAreaA String
tradeAreaB String
fairness String @default("unfair")
psl=}
然后,让我们使用命令运行新的迁移wasp db migrate-dev
。
我在 Trade 实体上创建了两个 String 类型的交易区域。我们选择使用 String 类型,因为我们使用的数据库是 SQLite,而 SQLite 不支持 JSON 对象类型。为了解决这个问题,我们会先将对象转换为字符串,然后再进行解析。虽然这并非最佳方案,但我将在后续主题中以高级开发人员的视角更详细地讨论这个问题以及其他可能的解决方案。
现在,让我们讨论一下负责在数据库中注册交易的功能的变化:
//src/server/actions.js
//here we convert the trade areas to string and save to our db!
export const registerTrade = async (args, context) => {
const pokemonAList = args.tradeAreaA;
const pokemonBList = args.tradeAreaB;
const { user, entities } = context;
const stringifiedPokemonAList = JSON.stringify(pokemonAList);
const stringifiedPokemonBList = JSON.stringify(pokemonBList);
if (!user) {
throw new HttpError(401);
}
const trade = await entities.Trade.create({
data: {
tradeAreaA: stringifiedPokemonAList,
tradeAreaB: stringifiedPokemonBList,
fairness: args.fairness,
user: { connect: { id: user.id } },
},
});
return trade;
};
在这里,我们可以观察到我们正在“字符串化”我们的 JSON,如前所述,并将其全部注册为交易!
最后,我们可以在 UI 中验证这一点。
什么?你还指望什么呢?这只是个按钮而已(虽然我们知道后端没那么简单,哈哈哈)。之后,我对个人资料页面的AI生成的代码做了一些小修改,比如解析JSON和改进一些CSS。幸运的是,AI生成的用于获取用户交易的代码完全正确,所以不需要再修改了!
//src/client/pages/Profile.jsx
//map all trades, for each one parse the tradeAreas and render them on the screen
{trades.map((trade) => {
const tradeAreaA = JSON.parse(trade.tradeAreaA);
const tradeAreaB = JSON.parse(trade.tradeAreaB);
return (
<div
className={`mb-4 border rounded shadow p-4 ${
trade.fairness === "fair" ? "bg-green-300" : "bg-red-300"
}`}
>
<div className="mb-2">
<span className="font-bold">Trade ID: </span>
<span className="capitalize">{trade.id}</span>
</div>
<div className="mb-2">
<span className="font-bold">Trade Area A: </span>
<span className="capitalize">
{tradeAreaA.map((pokemon) => {
return pokemon.name + " ";
})}
</span>
</div>
<div className="mb-2">
<span className="font-bold">Trade Area B: </span>
<span className="capitalize">
{tradeAreaB.map((pokemon) => {
return pokemon.name + " ";
})}
</span>
</div>
<div className="mb-2">
<span className="font-bold">Fairness: </span>
<span>{trade.fairness}</span>
</div>
</div>
);
})}
最后,我们可以在 UI 上查看用户的历史记录。本次挑战的范围到此结束!
太棒了!现在我们已经完成了所有事情,让我们来看看整个应用程序:
看起来真不错!让我们做一些小改动,让它准备好部署:
让我们首先在这里添加 PostgresSQL 作为我们的主要数据库!
//main.wasp -> in the db section, add the PostgresSQL system!
db: {
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"],
},
system: PostgreSQL,
},
auth: {
userEntity: User,
之后,我们需要通过删除旧迁移并运行来重新运行迁移wasp db migrate-dev
,最后,我们只需按照 Wasp 文档上的部署指南进行操作即可。
从高级开发人员的角度进行评估
现在我们有了应用程序的最终版本,让我们从更高级的开发人员的角度对其进行评估,以确定需要改进的地方,并预测我们可能收到的反馈类型:
-
前端:
- 好的:
- 整个 UI 的一致性:UI 保持一致的样式,按钮和颜色含义正确应用(绿色表示成功,红色表示错误/危险情况)。
- 代码库可重用性:TradeArea 组件成功实现可重用,展示了其他复杂组件转变为可重用组件的潜力。
- 良好的代码质量和结构:函数和变量满足其预期目的并具有适当的名称(要检查命名的重要性,您可以阅读此处)。
- 以下是一些示例:
loadPokémons()
,,都是很好的命名示例,我们只需阅读它们的名称就可以了解函数registerTrade()
的setSelectedPokémon()
基本功能。
- 以下是一些示例:
- 响应能力:UI 采用移动优先的方法设计(感谢TailwindCSS)。
-
坏的:
- 缺乏针对复杂行为的单元测试。
-
考虑使用更加用户友好的方法来显示所有 Pokémon 选项,而不是简单地选择 200 个选项。
- 例如:有可供使用的替代选择组件。
- 您可以看到,此选择附带一个输入(如果用户想要列表末尾的神奇宝贝,这将为用户带来更好的用户体验)
-
前端目前解析的是来自后端的 JSON.stringify 数据。如果后端能够发送已正确解析的数据就更好了。
- 好的:
-
后端和数据库:
- 好的:
- 范围划分清晰:结构组织良好,功能定位恰当。
- 错误处理:有一些错误处理程序,增强了应用程序的弹性。
- 我们有一些私人插入验证(例如
if (!user){ throw new HttpError(401); }
) - 还有一些身份验证方法通过 Wasp 框架自行提供的基本身份验证。
- 我们有一些私人插入验证(例如
- 有效使用 async/await 和 promise 处理。
- 坏的:
- 数据库选择不理想:我们的数据库中缺少 JSON,这是一个缺点,但值得称赞的是,我们找到了一个创造性的解决方案来解决这个问题。
- 缺乏单元测试。
- 好的:
总的来说,我们的应用程序简洁但运行良好。用户界面和用户体验显然经过了深思熟虑,这是一个优点。候选人在解决遇到的问题时也展现了创造力。然而,如果候选人能够加入单元测试并考虑更合适的数据库选择,那就更好了。总而言之,总体来说,做得不错!👍
结论
如果你还没有给Wasp 的仓库点赞,我建议你去点一下!这是一个很棒的仓库,可以用来测试全栈应用程序!
在本次测试中,我们通过创建一款令人印象深刻的应用程序成功完成了一项挑战。该应用程序允许用户交易宝可梦并检查交易是否公平,这正是我们挑战的范围。
我们的代码始终如一地处理数据存储和检索,并且我们也注重了样式设计。虽然我们可以通过添加测试用例和使用更好的数据库来改进实现,但我们的创造力和解决问题的能力显而易见,这在大多数情况下已经足够了(记住:没有完美的应用程序)。
重要的是要记住,即使你正在处理一个小小的招聘挑战,关注UI、UX和代码质量也会极大地提升你的交付成果。当然,技术挑战并非一切;你还需要具备良好的沟通能力和其他软技能(我会在下方留下一些链接,你可以在那里找到一些额外的资源)。
嘿!既然你看到最后了,也请在下方留言,说说你对这个项目的评价吧!我很乐意听到你的评论!
额外的资源和进一步的学习
- 学习一些软技能并提高你的沟通技巧:如何与人打交道:沟通
- UI/UX 法则:UX 法则
- 对于设计模式和代码结构的想法:重构大师
- 正确命名的重要性:为什么命名是编写干净代码的首要技能🧼🧑💻