news 2026/10/22 2:12:23

iSpring Junior 前端测试任务:用 React Hooks 实现单八度交互式钢琴应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iSpring Junior 前端测试任务:用 React Hooks 实现单八度交互式钢琴应用
  • 教程

【免费下载链接】ru-test-assignments

Тестовые задания для самостоятельного выполнения от разных it компаний

项目地址:https://gitcode.com/gh_mirrors/ru/ru-test-assignments
点击查看免费下载

导读

本文以 iSpring 公司(iSpring Solutions)面向Junior 前端开发者的经典测试任务《Игра "Пианино"(钢琴游戏)》为核心,完整解读任务的功能需求与技术约束,并给出基于 React 函数式组件 + Hooks、TypeScript、原生 CSS/HTML 与 Web Audio API 的落地方案。读完本文,你将掌握"如何零第三方依赖地实现一个可交互、可发声、支持鼠标与键盘双输入的单八度钢琴",并理解该任务背后考察的 React 声明式思维、事件处理与组件化设计能力。该任务文档位于仓库 other/ispring/PIANO_J.md,是 iSpring 前端测试任务系列 frontend.md 中的 Junior 档位题目。

一、任务背景:iSpring 前端测试任务体系

iSpring 在招聘前端开发者时采用"独立完成测试任务"的方式筛选候选人。在仓库 other/ispring/frontend.md 中可以看到,iSpring 前端测试任务按难度划分为多个档位:

  • Junior 档:本文核心《Пианино(钢琴)》,以及 15.md("Пятнашки" 数字推盘游戏);
  • Middle 档:PIANO_M.md(带录音与回放能力的钢琴进阶版)、DRAG.md(拖拽动物到栖息地的 Drag to area 游戏)、PLAYER.md(自定义控制面板的视频播放器)。

Junior 与 Middle 的同一主题(钢琴)形成了清晰的难度递进:Junior 只要求"按下→发声→视觉变化",Middle 则额外要求录音、计时、回放。本文聚焦 Junior 档,但会顺带指出进阶方向,方便读者规划成长路径。

二、功能需求逐条拆解

根据 PIANO_J.md 的"Задание(任务)"部分,钢琴游戏必须满足以下三条核心功能:

2.1 琴体范围:一个八度(1 октава)

Пианино должно состоять из 1 октавы

钢琴必须由一个八度组成。一个标准八度在十二平均律下包含 12 个半音,对应钢琴白键与黑键的布局,例如从 C4 到 B4:

  • 白键(自然音):C4、D4、E4、F4、G4、A4、B4,共 7 个;
  • 黑键(变化音):C#4/D♭4、D#4/E♭4、F#4/G♭4、G#4/A♭4、A#4/B♭4,共 5 个。

实现时建议用一份"键位定义表"驱动渲染,将音符名、是否黑键、对应键盘按键等数据集中声明,例如:

type PianoKey = { id: string; // 例如 'C4'、'C#4' note: string; // Web Audio API 频率计算用 isBlack: boolean; // 黑键/白键 keyboardKey?: string; // 绑定的键盘按键,如 'a'、'w' };

2.2 双输入控制:鼠标 + 键盘

Управление клавишами пианино должно быть доступно с помощью мыши и клавиатуры

  • 鼠标控制:点击某个琴键应触发发声;在钢琴应用中,常见的进阶交互是"按下不放持续发声、松开停止",因此更推荐用onPointerDown/onPointerUp(或onMouseDown/onMouseUp)成对处理,而不是onClick,以获得类似真实钢琴的按压体验;
  • 键盘控制:为每个琴键绑定一个电脑键盘按键。典型映射方案是使用键盘中下方两行字母键模拟白键、上方一行模拟黑键,例如:
电脑按键音符键型
aC4白键
wC#4黑键
sD4白键
eD#4黑键
dE4白键
fF4白键
tF#4黑键
gG4白键
yG#4黑键
hA4白键
uA#4黑键
jB4白键

键盘事件需要挂在全局(window)而非单个按键元素上,通过keydown/keyup事件配合event.code或event.key判断按键,并注意在keydown中过滤重复触发(event.repeat),避免长按按键时连续触发发声。

2.3 发声与视觉反馈

При нажатии на клавишу проигрывается соответствующий звук ноты,а также клавиша визуально изменяется

按下琴键必须同时完成两件事:

  1. 播放对应音符的声音:任务文档提供了音符音频资源(外部链接,1maR08OXPPaCC1HQJP0k-zHJLfKOUbFWq),即每键一个音频文件。若资源不可用时,也可以使用浏览器内置的Web Audio API以振荡器(OscillatorNode)合成音色,这是零第三方依赖的推荐兜底方案;
  2. 视觉变化:被按下的琴键需要在视觉上明显区别于未按下的键(如改变背景色、亮度、阴影或 transform 缩放),且要在松开后恢复原状。

三、技术要求解读:一份"零依赖 + React Hooks"的约束清单

任务在 PIANO_J.md 的"Требования(要求)"部分给出了严格的技术栈约束,这也是 iSpring 前端任务系列的通用约束:

约束项原文要求解读
语言TypeScript,或 vanilla JS + 组件类型检查推荐直接使用 TypeScript,用类型约束键位数据与组件 Props,体现类型意识
样式/结构原生 CSS 与 HTML,禁止预处理器与模板引擎不引入 Sass/Less、JSX 之外的模板方案,考验原生 CSS 布局能力
框架React 函数式风格 + Hooks必须用函数组件与useState/useEffect/useRef/useCallback等 Hooks 实现,而非 class 组件
依赖除 React 外不得使用任何第三方库音频、状态、事件全部手写
加分项使用 state manager(自选)可选引入 Redux/Zustand 等,但需自证必要性

这些约束的共同指向是:考察候选人在不借助生态库的情况下,能否用 React Hooks 干净地组织状态与副作用。这与 iSpring 其他前端任务(如 PLAYER.md 的播放器、15.md 的推盘游戏)的约束完全一致,说明这是 iSpring 前端筛选的标准化基线。

四、推荐实现方案:从数据到组件的分层设计

4.1 架构总览

在零第三方库约束下,推荐将应用拆为三层:

  1. 数据层:静态的PIANO_KEYS键位配置数组(12 个音符的元数据);
  2. 状态层:一个自定义 Hook(如usePiano)集中管理"当前哪些键处于按下状态",并提供pressNote/releaseNote动作与音频播放副作用;
  3. 视图层:Piano(琴体容器)、PianoKey(单个琴键)两个组件,前者负责键盘全局事件与键位渲染,后者负责鼠标事件与按下态的视觉表现。

4.2 自定义 Hook:usePiano

核心状态用Set<string>(或string[])记录当前被按下的音符 id,保证按下同一键是幂等的:

import { useCallback, useEffect, useRef, useState } from 'react'; export function usePiano(notes: string[]) { const [activeNotes, setActiveNotes] = useState<Set<string>>(() => new Set()); const audioCtxRef = useRef<AudioContext | null>(null); const getCtx = useCallback(() => { if (!audioCtxRef.current) { audioCtxRef.current = new AudioContext(); } return audioCtxRef.current; }, []); const playNote = useCallback((note: string) => { // 1. 标记按下状态(视觉反馈) setActiveNotes(prev => { if (prev.has(note)) return prev; const next = new Set(prev); next.add(note); return next; }); // 2. 发声:Web Audio 振荡器,按下即触发 const ctx = getCtx(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'triangle'; osc.frequency.value = noteToFrequency(note); gain.gain.setValueAtTime(0.2, ctx.currentTime); osc.connect(gain).connect(ctx.destination); osc.start(); // 记录 osc/gain,供 release 时停止(此处以 ref 保存映射,简化示意) }, [getCtx]); const stopNote = useCallback((note: string) => { setActiveNotes(prev => { if (!prev.has(note)) return prev; const next = new Set(prev); next.delete(note); return next; }); // 在真实实现中:取出该 note 对应的 osc,调用 stop() 并断开节点 }, []); // 全局键盘监听 useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.repeat) return; const note = keyToNote(e.key); if (note) playNote(note); }; const onKeyUp = (e: KeyboardEvent) => { const note = keyToNote(e.key); if (note) stopNote(note); }; window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); }; }, [playNote, stopNote]); return { activeNotes, playNote, stopNote }; }

其中noteToFrequency将音符名换算为频率(标准公式:f = 440 * 2^((midi - 69) / 12)),例如 C4(midi 60)≈ 261.63 Hz;keyToNote维护上文"电脑按键 → 音符"的映射表。

说明:仓库 other/ispring/PIANO_J.md 只给出任务描述,并未附带参考实现,上述代码是基于需求与 React Hooks 最佳实践的推荐写法,可直接作为答题起点。若使用任务附带的音频文件资源,则playNote改为通过new Audio(src).play()播放对应 mp3,并同样在stopNote中暂停。

4.3 视图层:Piano 与 PianoKey

单个琴键组件接收"音符、是否黑键、是否按下"三个 Props,按下态由父组件通过activeNotes.has(note)计算得出,从而实现状态提升 + 单向数据流:

type PianoKeyProps = { note: string; isBlack: boolean; active: boolean; onPress: (note: string) => void; onRelease: (note: string) => void; }; function PianoKey({ note, isBlack, active, onPress, onRelease }: PianoKeyProps) { return ( <div className={`piano-key ${isBlack ? 'black' : 'white'} ${active ? 'active' : ''}`} onPointerDown={() => onPress(note)} onPointerUp={() => onRelease(note)} onPointerLeave={() => onRelease(note)} > {!isBlack && <span className="key-label">{note}</span>} </div> ); }

视觉反馈完全由 CSS 类驱动,原生 CSS 即可实现,无需任何预处理:

.piano { display: flex; position: relative; } .piano-key { position: relative; border: 1px solid #333; transition: background 0.08s, transform 0.08s; } .piano-key.white { width: 56px; height: 200px; background: #fff; } .piano-key.black { width: 34px; height: 120px; margin-left: -17px; margin-right: -17px; z-index: 1; background: #222; } .piano-key.white.active { background: #ffe08a; transform: translateY(2px); } .piano-key.black.active { background: #ff8a3d; transform: translateY(2px); }

黑键通过负 margin 与更高的z-index实现"骑跨"在白键之间的经典钢琴布局,active类同时驱动背景色变化与微小平移,形成按压感。

五、实现中的关键细节与踩坑点

结合需求约束,以下细节值得在答题与自测时特别关注:

  1. AudioContext 的启动限制:现代浏览器要求 AudioContext 必须由用户手势(点击/按键)触发后才能开始播放。因此AudioContext应在第一次playNote(即用户手势内)时惰性创建,而不是在页面加载时创建,否则会被浏览器静音拦截;
  2. keydown 防重复:长按键盘按键会连续触发keydown,必须检查e.repeat以避免同一音符被反复触发、声音重叠失控;
  3. 鼠标与键盘的状态一致性:两种输入最终都应写入同一个activeNotes状态,保证视觉反馈与发声逻辑完全统一,这也是将逻辑收敛到usePiano的原因;
  4. 清理副作用:useEffect注册的window事件监听必须在 cleanup 中移除,避免组件卸载后事件泄漏(这在 React StrictMode 双调用下尤其重要);
  5. 无障碍与健壮性(加分项):可以为琴键添加role="button"与aria-pressed={active},或对键盘布局给出可视提示,体现工程成熟度。

六、验收自测清单

完成实现后,可按下表逐项自测,对应 PIANO_J.md 的每一条需求:

验收项对应需求自测方法
琴体为单八度 12 键(7 白 + 5 黑)1 октава视觉核对键数;点击白键/黑键均发声
鼠标点击即发声且键位高亮鼠标控制 + 视觉变化逐个点击 12 键,确认声音正确、高亮明显、松开后恢复
键盘按键映射触发同样行为键盘控制按映射表逐个按键测试,长按无重复触发
按下与松开状态同步状态一致性混合鼠标与键盘操作,观察activeNotes与 UI 一致
无第三方库依赖约束检查package.json依赖仅含 React/ReactDOM(及类型包)

七、进阶方向:从 Junior 到 Middle

任务文档在同一系列中还提供了钢琴的 Middle 版本 PIANO_M.md,在 Junior 基础上追加了:

  • 录音模式:记录用户弹奏的旋律,需同时记录按下时刻与按住时长;
  • 回放模式:按原时间轴与时长回放,回放期间琴键同样要视觉变化;
  • (可选)多录音列表:保存多条旋律并可任选回放。

对照本文的方案,录音只需在playNote/stopNote时记录timestamp与音符组成事件流(如{ note, startTime, duration }),回放则用setTimeout/requestAnimationFrame按时间轴依次触发playNote/stopNote。可以以本文 Junior 版为基础自然演进到 Middle 版,这也正是该系列任务设计的递进用意。

八、小结

iSpring 的《Пианино》Junior 测试任务虽然界面简单,却精准覆盖了前端工程师的基础能力面:组件化拆分、Hooks 状态管理、DOM 事件(鼠标 + 键盘)、Web Audio 发声、原生 CSS 布局、零依赖约束下的工程取舍。以"键位数据驱动渲染 + 单一状态源 + 双输入统一处理"的设计实现,既能满足全部需求,也能在面试中清晰展示你的 React 思维。仓库中的相关任务(15.md、PLAYER.md、DRAG.md)共享同一套技术约束,练熟本题后可以一并挑战,形成完整的 iSpring 前端测试任务实战闭环。

  • 教程

【免费下载链接】ru-test-assignments

Тестовые задания для самостоятельного выполнения от разных it компаний

项目地址:https://gitcode.com/gh_mirrors/ru/ru-test-assignments
点击查看免费下载

相关推荐

上一篇:CANN/ge动态分档特性详解
下一篇:jcode命令风险分类系统:危险命令如何被识别与拦截?新手完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 9:39:49

openJiuwen agent-core 工作流表达式条件判断 ExpressionCondition 完全指南

人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习 【免费下载链接】agent-core openJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力 项目地址&#xff1a; https://gitcode.com/openJiuwen/agent-core 点击查看 免费下载 本篇技术指南…

作者头像 李华
网站建设 2026/10/18 4:43:52

PMIC+MCU协同设计:PCA9422与PIC18F85K22的嵌入式电源管理方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 1:49:54

C99标准库24个头文件系统梳理:从入门到精通的必备指南

C 语言这门手艺&#xff0c;入门容易&#xff0c;用精很难。很多人写了几年 C 代码&#xff0c;遇到一个编译报错还是靠搜索引擎碰运气&#xff0c;根本原因就是对标准库这套“官方工具箱”没有系统性的认知。C99 标准把标准库头文件做了比较清晰的归类&#xff0c;一共 24 个头…

作者头像 李华
网站建设 2026/10/15 7:57:00

Oracle9i补丁安装与维护:解析p4547809_92080_WINNT.zip

简介&#xff1a;面向Windows 32位环境的Oracle 9i数据库安装包&#xff08;补丁号p4547809_92080&#xff09;&#xff0c;为数据库管理员、系统运维人员及需要维护旧系统的技术人员提供完整部署条件&#xff0c;也适合初学者对照学习关系型数据库原理。压缩包共529个文件、24…

作者头像 李华