UI 面试问题备忘单

一份备忘单,可用于在前端面试中改进您必须构建或设计的用户界面。

作者
Ex-Meta Staff Engineer

以下是一些提示,可用于改进您在前端面试中必须构建/设计的用户界面。这些可以应用于 用户界面编码面试前端系统设计面试

一般

  • 分解问题: 将问题分解为相互构建的阶段/里程碑,并逐步编写代码。
  • 频繁测试: 在完成每个功能后,在浏览器中测试 UI,以便尽早发现错误。 尽早发现的错误更容易修复。 在继续下一个功能之前,请确保当前功能正常工作。
  • 如果可能,使用 JavaScript 框架: 如果您选择使用 Vanilla JavaScript 构建复杂的 UI,您的生活将会非常艰难,因为代码会变得非常冗长且混乱。 我们建议尽可能使用 JavaScript 框架/库(例如 React、Vue、Angular 等)构建应用程序和游戏。
  • 提前思考并相应地计划: 考虑您的面试官可能要求您接下来添加哪些功能,方法是借鉴您在构建实际应用程序、使用库或练习面试问题方面的经验。 以一种易于添加新功能的方式设计您的代码。 大部分时间,您会被要求将您的 UI 组件化,以便可以在页面上显示多个组件实例——模块化并避免依赖全局状态或污染全局命名空间。

组件组织

您如何组织代码?

  • 采用 容器/表现模式: 为了实现良好的解耦,渲染代码应该与数据源无关。 将组件分成一个提供数据的外层和一个根据数据渲染视图的内部无状态组件。 这使得视图可以轻松地从本地组件/应用程序状态切换到从网络加载的数据,反之亦然,您只需更改外部组件,内部组件即可按原样使用。 这种分离也方便了表现组件的测试,因为所需的数据可以很容易地被模拟。
  • 将应用程序分解为更小的组件: 如果 UI 有多个部分,将 UI 分解为更小的组件,并确定每个组件所需的 props/state。
  • 最小的 API 表面积: 不要将多余的数据传递给不需要它们的内部组件。
  • 组件实例化: 当被要求构建 UI 组件时,定义 API(通常是函数)以允许创建组件的多个独立实例以及可配置的选项和默认值。 避免编写阻止创建多个 UI 组件实例的代码(例如,依赖全局变量)。
    • Vanilla JavaScript: 创建一个接受 DOM 元素(容器元素)和选项对象的函数。 在该函数中,您可以动态创建 DOM 元素并附加到容器元素。 组件 API 的另一个灵感来源是 jQuery UI,但它依赖于 jQuery。
    • 其他 JavaScript UI 框架: 大多数现代 JavaScript UI 框架(例如 React、Vue、Angular)默认情况下会迫使您从组件的角度进行思考。

状态设计

状态是 UI 中随时间变化的数据,通常是由于用户交互或后台事件(网络请求响应、时间流逝、WebSocket 事件)引起的。

面试中的大多数 UI 问题都需要状态,并且良好地设计状态至关重要。

  • 确定 UI 中所需的最少状态: 状态越小,代码就越容易阅读和理解 -> 出现错误的概率越低。 确定基本状态与派生状态。 派生状态是可以从基本状态计算出来的状态。 通过即时派生状态,您可以减少状态值不同步的可能性。
  • 分离渲染代码与数据管理代码: 使 UI 成为数据的函数,并将代码分成两部分(渲染代码与数据管理代码),以提高可读性。 如果您使用 JavaScript 框架(例如 React),您或多或少会被迫这样做。
  • 对复杂的状态交互使用状态-reducer 模式: 如果问题需要许多状态字段,并且某些操作需要同时更改多个字段,请使用 reducer 来合并状态更新逻辑。 状态-reducer 模式最初由 Redux 推广,它鼓励您确定 UI 的状态、可以采取的操作以及如何将操作与状态结合起来以推导出下一个状态。 如果您使用 React,您可以通过 useReducer React hook 实现此模式。 Redux 通常对于面试来说是过度的,而 useReducer 应该足够了。

React 的 关于 “管理状态” 的文档是关于如何正确设计和使用组件状态的绝佳资源。提到的一些想法并非特定于 React,可以应用于任何 UI 框架。

JavaScript

您的 JavaScript 是否使用现代语言语法和良好实践,同时避免不良实践?

  • 使用样式指南:使用 JavaScript 样式指南,如 Airbnb 的 JavaScript 样式指南。在开发过程中,ESLint 等静态分析工具可以帮助您执行其中一些良好实践。但是,这些工具可能在面试期间不可用。尝试习惯于在没有工具帮助的情况下编写具有良好编码风格的代码。
  • 不要动全局环境:这适用于 Vanilla JavaScript 场景。避免通过声明全局变量和全局函数来污染全局范围。编写一个立即调用的函数表达式 (IIFE) 并将所有自定义代码放入其中。

HTML

您是否使用正确的标签和正确的属性编写语义 HTML?

  • 语义标签: 使用标题标签表示标题,使用按钮标签表示交互元素,使用列表标签表示顺序元素,等等。 不要对所有内容都使用 <div>
  • 标题层次结构: 确保标题标签具有层次结构,并且 DOM 中只有一个 <h1>
  • 交互元素: 使用 <button> 表示需要交互的元素。 不要向 <div><span> 添加点击处理程序,这是一个巨大的危险信号,表明缺乏对可访问性的考虑。

表单

表单本身很复杂,值得单独讨论。

  • 链接标签和输入<input> 应该使用 idfor 链接到 <label>
  • 将输入包装在表单中<input> 应该包装在 <form> 中,以便单击按钮和按 Enter 键将提交表单。 如果网络请求是使用 Ajax 发出的,请记住添加 event.preventDefault()
  • 输入应具有适当的类型<input> 应该具有适当的 type,如 emailpasswordnumber 等。
  • 利用原生 HTML 表单验证:如果可能,使用 required 属性与其他属性(如 patternminmax 等)结合使用。

CSS/样式

您的 CSS 是否以可扩展且易于理解的方式编写?

  • 编写 Vanilla CSS:学习编写 CSS,而不依赖于 SassLess 等预处理器。 并非所有环境都允许使用处理器,并且面试问题可能很小,实际上并没有从 CSS 预处理器带来的功能中受益。 CSS 处理器最有用的功能是使用变量,该变量可通过 CSS 自定义属性(变量)以原生方式使用。
  • 采用 CSS 命名约定:在编写类时,考虑采用 CSS 命名方法,例如 Block Element Modifier
  • 避免在组件中使用 #id 选择器:在构建旨在重用的 UI 组件(例如按钮、选项卡、菜单、模态等)时,避免在 HTML 中使用 #id 选择器,因为 id 旨在全局唯一,但您可以拥有该组件的多个实例。
  • 组织您的 CSS:阅读有关如何在大型项目中组织您的 CSS 以及如何拥有CSS 的可扩展和模块化架构

用户体验

您的 UI 是否提供了出色的用户体验?

  • 移动友好性:检查您是否需要使您的 UI 在移动设备上运行良好。
    • CSS 媒体查询可用于在移动设备上呈现不同的布局。
    • 使按钮等交互元素足够大以供按压(建议至少 44 x 44 像素)并充分间隔。
  • 错误状态:及时、清晰地反映错误 — 表单验证错误、网络请求错误。
  • 处理不同尺寸的图像渲染:使您的 UI 适用于渲染所有尺寸/尺寸的图像,同时保留原始纵横比。
    • 将 CSS background-imagebackground-position: contain 结合使用,使图像适合您定义的区域。 如果可以裁剪图像(例如用于渐变背景),请使用 background-position: cover
    • <img> 标签具有类似的 object-fit 属性,具有 containcover 值。
  • 乐观更新:高级技术,即使网络请求仍在等待,也会反映成功状态。 如果请求失败,请恢复 UI 更改并显示错误消息。

网络

您的 UI 是否处理网络请求和条件的不可预测性?

  • 反映网络请求状态:如果 UI 涉及发出网络请求,请清楚地显示请求的待处理/成功/失败状态
    • 待处理:禁用字段/按钮,显示微调器。
    • 错误:显示错误消息。
    • 成功:更新 UI 和/或显示成功消息。
  • 竞争条件:一个常见的原因是由于并行网络请求,其中响应顺序不能保证。 稍后发出的请求可能会更早地收到响应。 如果您的 UI 容易受到这种情况的影响,您可以跟踪最新的请求并忽略较早请求的结果。 或者,使其 UI 无法一次触发多个网络请求,例如,通过在单击后禁用触发网络请求的元素。
  • 防止重复请求:提交后应禁用按钮,以避免发出重复的网络请求。
  • 合并请求:如果 UI 发出了太多网络请求,您可以:
    • 去抖动/节流:限制触发的网络请求的数量。
    • 批量请求:将请求组合在一起,只发出一个请求。 这需要服务器端支持这种格式。
  • 缓存:如果最近发出了具有相同参数的请求,您是否可以重用之前的响应并节省网络往返?
  • 请求超时:如果请求在规定的持续时间后未收到响应,您可能希望人为地显示请求已失败(超时)。
  • 乐观更新:高级技术,即使网络请求仍在等待,也会反映成功状态。 如果请求失败,请恢复 UI 更改并显示错误消息。

可访问性 (a11y)

在 UI 中处理可访问性是一个很大的优势,在某些情况下是高级工程师的要求。

  • 您是否只能使用键盘使用 UI?
  • 您可以使用屏幕阅读器使用您的 UI 组件吗?
  • 您的 UI 组件可以在没有颜色的情况下工作吗?
  • 您的 UI 组件可以在没有声音的情况下工作吗?

来源:Web 的可访问 UI 组件

  • 屏幕阅读器、ARIA 角色、状态和属性
    • 为未使用自定义 HTML 标签构建的自定义构建元素添加正确的 aria-role
    • 使用 aria-label 描述未显示文本的元素(例如,仅限图标的按钮)。
    • 通过 aria-describedby/aria-errormessage 将错误消息元素与负责它们的元素链接起来。
    • 图像 alt 文本:将 alt 属性添加到 <img> 元素,以便屏幕阅读器可以描述图像。
  • 键盘交互
    • tabindex 属性添加到您希望通过键盘制表符聚焦的元素。
    • 元素可以通过键盘触发。
    • 检查焦点顺序是否有意义。
  • 视觉问题
    • 颜色对比度:文本/图像与背景之间有足够的颜色对比度。
    • 元素大小:字体大小、交互式元素大小应足够大,以适合其预期介质。

Google 的 web.dev 有一个关于可访问性的免费深入课程,我们强烈推荐。

边缘情况

在面试中,你可能没有足够的时间来处理代码中的所有边缘情况,但最好在面试中提及它们以获得加分。

  • 处理长字符串:UI 中的字符串(例如标题/按钮标签)可能导致 UI 行为异常,例如溢出并影响周围元素的位置。长字符串可能是用户输入或翻译的结果。
  • 空状态:显示空状态消息/占位符以指示内容缺失,例如当列表为空时。什么都不显示可能会让用户认为有一个待处理的网络请求,并且数据仍在获取中。
  • 列表中项目过多:在单个页面上显示太多项目会导致较差的 UX(用户必须滚动很多)以及响应能力和内存消耗方面的性能不佳。
    • 分页:将长列表项分成多个页面。
    • 虚拟列表:仅渲染动态列表中可见的行内容,而不是整个列表。
    • 截断多余的内容并显示省略号。word-break CSS 属性将派上用场。
    • 将内容限制为前 X 个字符/单词,并将多余的内容隐藏在“显示更多”按钮后面。

性能

  • 节流/防抖:节流和防抖是限制速率的技术,以防止不必要的操作。此技术可用于对时间要求不高的操作,例如网络请求和滚动/调整大小的事件回调。
  • 缓存:重复计算/网络请求的结果可以在浏览器内存/存储中缓存,而无需重复。
  • 按需加载:仅在需要时延迟加载数据/组件代码,而不是在开始时全部加载。
  • 预取/预加载数据:在需要之前预取/预加载数据以减少网络延迟,以便更新立即显示。
  • 列表中项目过多:请参阅上面“边缘情况”下的要点。

安全

  • 跨站点脚本 (XSS):如果内容来自用户,则在将内容呈现到 DOM 时,避免分配给 Element.innerHTML 或 React 的 dangerouslySetInnerHTML 以防止跨站点脚本,分配给 Node.textContent 或使用实验性的 Element.setHTML() 方法。请参阅 OWASP 的 XSS 预防备忘单
  • “URL 上下文”的输出编码:如果用户提供的输入可以在 URL 查询参数中使用,请使用 encodeURIComponent 以防止意外值成为 URL 的一部分(例如额外的查询参数)。
  • 跨站点请求伪造:请参阅 OWASP 的 XSS 预防备忘单

国际化 (i18n)

你的 UI 适用于多种语言吗?添加对更多语言的支持有多容易?

  • 避免用特定语言硬编码标签:某些 UI 组件在其内部有标签字符串(例如,图像轮播有 prev/next 按钮的标签)。最好通过将这些标签字符串作为组件 props/options 的一部分来允许自定义它们。
  • UI 可以呈现长字符串:请参阅上面关于呈现长字符串的部分。
  • 从右到左的语言:某些语言(例如阿拉伯语、希伯来语)是从右到左阅读的,UI 必须水平翻转。使用 CSS 逻辑属性 使你的布局适用于不同的书写模式