跳到主要内容

HTML 热门面试题

使用说明

这组题共 45 道,覆盖 HTML 分类最常问的语义化、可访问性、资源加载、表单、SEO 与 Web Components。口头回答时先说结论,再补使用场景和取舍,不需要背规范原文。

Q1: 什么是 HTML 语义化?为什么重要?

答案

语义化就是根据内容职责选择合适的标签,例如导航用 nav、正文用 main、独立内容用 article,而不是全部使用 div

  • 它让浏览器、搜索引擎和屏幕阅读器更容易理解页面结构。
  • 代码对开发者也更可读,但语义标签不会自动带来正确样式和无障碍体验。
  • 选择标签应看内容含义,不要为了“看起来高级”滥用 sectionarticle

Q2: divsectionarticle 应该怎么选?

答案

  • <div>无语义,纯粹的容器,用于布局和 CSS 分组
  • <section>有主题的区块,通常有标题,表示页面中的一个逻辑部分
  • <article>独立完整的内容,可以脱离上下文独立存在(如一篇博客、一条评论)

判断标准:内容能独立分享/订阅吗?能 → <article>;是页面的一个主题区块?→ <section>;纯布局需要?→ <div>

Q3: DOCTYPE 有什么作用?

答案

<!DOCTYPE html> 是一条文档类型声明,告诉浏览器使用哪种标准来解析文档。

作用:触发浏览器的标准模式(Standards Mode) 进行渲染。

不写 DOCTYPE 的后果:浏览器会进入怪异模式(Quirks Mode),表现如下:

特性标准模式怪异模式
盒模型content-boxwidth 不含 padding/border)border-boxwidth 包含 padding/border)
图片底部间隙inline 元素有基线对齐间隙无间隙
百分比高度需父元素有明确高度可以不需要
CSS 解析严格按规范兼容旧浏览器的非标准行为
检测当前渲染模式
console.log(document.compatMode);
// "CSS1Compat" -> 标准模式
// "BackCompat" -> 怪异模式
面试关键点
  • <!DOCTYPE html> 不是 HTML 标签,而是一条给浏览器的指令
  • HTML5 的 DOCTYPE 不需要引用 DTD(HTML4 需要)
  • 必须写在文档的第一行,前面不能有任何字符(包括空行和注释)

Q4: asyncdefer 有什么区别?

答案

二者都能避免普通外部脚本阻塞 HTML 下载,但执行时机和顺序不同。

  • async 下载完立即执行,不保证多个脚本的执行顺序,适合独立统计脚本。
  • defer 在文档解析完成后、DOMContentLoaded 前按文档顺序执行,适合有依赖的应用脚本。
  • type="module" 默认具有类似 defer 的行为,还支持模块依赖和严格模式。

Q5: DOMContentLoadedload 有什么区别?

答案

DOMContentLoaded 表示 HTML 已解析并构建 DOM,不等待图片等普通资源;load 要等页面及其依赖资源基本加载完成。

初始化 DOM 交互通常监听前者;确实依赖图片尺寸或全部资源时才使用后者。现代框架一般通过挂载时机管理,不需要到处手写全局监听。

Q6: 图片的 alt 应该怎么写?

答案

alt 要表达图片在当前上下文中的信息或功能,而不是机械描述文件名。

  • 信息图片写等价的简短文本。
  • 作为按钮的图片描述动作,例如“搜索”,不要只写“放大镜”。
  • 纯装饰图片使用空字符串 alt="",让屏幕阅读器跳过;不能省略后让它朗读文件名。

Q7: 原生 HTML 如何提升可访问性?

答案

优先使用有内建语义和键盘行为的原生元素,再补充标签关联、焦点顺序和状态提示。

  • 点击操作用 button,导航用 a,不要用带 onClickdiv 冒充。
  • 表单用 label 关联控件,错误信息要能被辅助技术感知。
  • 只有原生语义不足时才使用 ARIA;ARIA 能补语义,但不会自动实现键盘交互。

Q8: htmllang 属性为什么重要?

答案

lang 声明页面主要语言,帮助屏幕阅读器选择正确发音规则,也会影响浏览器翻译、拼写检查、搜索理解和 CSS :lang() 匹配。

应使用有效的 BCP 47 语言标签,例如简体中文常写 zh-CN,具体内容切换语言时可在局部元素上覆盖。它不能替代字符编码声明,也不会自动翻译页面。

服务端渲染多语言页面时要保证 lang 与实际内容一致;仅写一个固定值却输出另一种语言,会比缺失更误导辅助技术。

Q9: 表单原生校验和业务校验怎么分工?

答案

原生属性如 requiredtypeminpattern 适合提供基础约束和移动端输入体验,复杂业务规则由应用层处理。

前端校验用于即时反馈,服务端仍必须重新校验,因为请求可以绕过浏览器。错误提示要定位到字段,并保留用户输入。

Q10: CanvasSVG 怎么选择?

答案

两者的核心差异是渲染模型:

  • SVG:每个图形都是 DOM 节点,天然支持事件、可访问性和 CSS,缩放不失真,适合元素数量可控、需要精细交互的图标、流程图和图表。
  • Canvas:把绘制结果写进位图,没有逐图元 DOM,适合大量对象、高频重绘、像素处理、游戏和自定义渲染器。

选择时看真实数据规模、更新频率、命中检测、文本可访问性、导出和团队工具链,不要背“超过几百或几万个元素”的固定分界。复杂应用也可混合:Canvas 绘制主体,DOM/SVG 承载标签、焦点和交互层。

Canvas 还要自行处理 DPR、重绘和命中检测;SVG 节点过多时则会增加 DOM、样式和布局成本。

Q11: 什么是响应式图片?

答案

响应式图片让浏览器根据视口、像素密度和格式支持选择合适资源,常用 srcsetsizespicture

  • srcset 提供候选图片,sizes 描述图片实际展示宽度。
  • picture 可做艺术裁剪或 AVIF/WebP 格式回退。
  • 还要设置宽高或 aspect-ratio,减少图片加载造成的布局偏移。

Q12: preloadprefetch 和懒加载有什么区别?

答案

preload 提前加载当前页面确定很快会用到的高优先级资源;prefetch 为未来导航准备低优先级资源;懒加载则延后非关键资源。

预加载不是越多越好,错误使用会与首屏关键资源抢带宽。图片和 iframe 可结合原生 loading="lazy",首屏主图通常不应懒加载。

Q13: iframe 有哪些安全注意点?

答案

iframe 会引入新的页面上下文和第三方代码风险,应通过来源、能力和通信三层限制。

  • 使用 sandbox 只开放必要能力,谨慎组合脚本与同源权限。
  • 用 CSP、frame-ancestors 或相关响应头控制谁能嵌入页面。
  • postMessage 通信必须校验 origin、消息结构和业务权限,不能使用任意来源通配。

Q14: Web Components 包含哪些核心能力?

答案

Web Components 由三大核心技术组成:

  1. Custom Elements:允许开发者自定义 HTML 标签,继承 HTMLElement,通过 customElements.define() 注册。提供 connectedCallbackdisconnectedCallbackattributeChangedCallbackadoptedCallback 四个生命周期回调。

  2. Shadow DOM:为自定义元素提供独立的 DOM 树和样式作用域。通过 attachShadow({ mode: 'open' | 'closed' }) 创建。内部样式不会泄漏到外部,外部样式也不会影响内部(CSS 自定义属性除外),实现浏览器原生级别的样式隔离。

  3. HTML Templates & Slots<template> 标签定义不被渲染的模板片段,在 JavaScript 中通过 cloneNode(true) 复用。<slot> 提供内容分发机制(类似 Vue 的插槽),支持默认 slot 和具名 slot。

这三者配合使用,可以创建封装良好、可复用、与框架无关的自定义组件。

Q15: HTML 层面如何改善 SEO?

答案

保证重要内容可被抓取并具有清晰结构,包括正确的标题层级、唯一标题与描述、语义化正文、可跟随链接和图片替代文本。

同时配置 canonical、结构化数据和多语言标记,并确保关键内容不是必须等待复杂客户端交互才出现。SEO 最终还受性能、内容质量和服务端状态码影响。

Q16: <template> 元素适合什么场景?和隐藏 DOM 有什么区别?

答案

<template> 用来声明一段暂不渲染、之后再实例化的 HTML。它的子节点存放在 template.content 这个 DocumentFragment 中,不参与当前页面的布局、绘制、脚本执行和资源加载行为。

常见用法是原生组件模板、重复列表片段和 Web Components:

const template = document.querySelector<HTMLTemplateElement>('#user-row')!;
const fragment = template.content.cloneNode(true);
document.querySelector('#list')!.append(fragment);

它和 display: none 不同:后者的节点已经存在于当前 DOM 中,只是不生成布局盒;template 的内容还没有被实例化。使用时通常需要 cloneNode(true),否则直接移动 content 会消耗其中的节点。

Q17: HTML5 新增了哪些语义标签?有什么作用?

答案

HTML5 新增的语义标签主要包括:

标签作用
<header>页面或区块的头部,通常包含 logo、标题、导航
<nav>导航链接区域
<main>页面主内容区(整个页面只能有一个
<article>独立的、可复用的内容块(如一篇博客文章)
<section>按主题分组的内容区域
<aside>与主内容相关但非核心的附属内容(侧边栏)
<footer>页面或区块的底部,通常包含版权、联系信息
<figure> / <figcaption>独立的媒体内容及其说明
<details> / <summary>原生的折叠/展开交互
<mark>标记/高亮文本
<time>机器可读的时间/日期

作用

  1. SEO 优化:搜索引擎能更准确地理解页面结构和内容权重
  2. 可访问性:屏幕阅读器可通过语义标签提供更好的导航体验(如按区域跳转)
  3. 代码可读性:开发者看到标签名就能理解内容含义,无需依赖 class 名
  4. 维护性:结构清晰,减少沟通成本

详细内容请参考 语义化与可访问性

Q18: viewport meta 标签各参数什么意思?不设置会怎样?

答案

viewport meta 标签的完整格式:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover" />

各参数含义:

  • width=device-width:视口宽度等于设备屏幕宽度(逻辑像素)
  • initial-scale=1.0:初始缩放比例为 1,即不缩放
  • maximum-scale=5.0:最大允许缩放到 5 倍
  • minimum-scale=1.0:最小缩放比例
  • user-scalable=yes:允许用户手动缩放
  • viewport-fit=cover:视口覆盖整个屏幕,用于 iPhone 刘海屏适配

不设置 viewport 的后果

  1. 浏览器使用默认布局视口(通常 980px),页面在手机上显示很小
  2. 用户需要双指缩放才能阅读文字
  3. 媒体查询中的 max-width 断点不会生效
  4. 响应式布局完全失效
警告

不要设置 user-scalable=nomaximum-scale=1,这会导致 Lighthouse 无障碍扣分,也违反 WCAG 2.1 标准。

Q19: HTML 表单有哪些常用的 input 类型?各自的作用?

答案

HTML5 提供了超过 20 种 input 类型,面试中重点掌握以下几类:

文本输入类text(单行文本)、password(密码掩码)、search(搜索框,部分浏览器显示清除按钮)、tel(电话号码,移动端弹出拨号键盘)。

格式校验类email(自带邮箱格式验证)、url(自带 URL 格式验证)、number(数值输入,支持 min/max/step)。

日期时间类date(日期选择器)、time(时间选择器)、datetime-local(日期时间选择器)。

选择类checkbox(复选框)、radio(同 name 的单选按钮互斥)、file(文件选择,支持 acceptmultiple)、color(颜色选择器)、range(滑块)。

特殊类型hidden(隐藏字段,不显示但会提交,常用于 CSRF token)。

面试关键点:选择正确的 type 不仅提供原生校验,更重要的是优化移动端键盘体验提升可访问性

Q20: 表单中的 button 为什么要显式声明 type

答案

button 位于表单内部且未声明 type 时,默认行为通常是 submit。如果它只是“打开弹窗”“添加一行”或“切换密码可见性”,点击后可能意外提交表单。

建议按意图显式书写:

  • 提交表单:<button type="submit">
  • 普通操作:<button type="button">
  • 重置表单:<button type="reset">,但实际产品中要慎用,避免用户误清空

不要只在点击事件里调用 preventDefault() 补救,因为键盘提交、原生校验和无 JavaScript 场景仍需要正确的 HTML 语义。

Q21: 原生 <dialog> 有什么价值?使用时要注意什么?

答案

<dialog> 为模态框提供了原生语义和 API。调用 showModal() 后,浏览器会把它放入 Top Layer,并让页面其他区域进入不可交互状态;close() 可关闭并设置 returnValue

使用时仍要补齐业务体验:

  1. 提供可理解的标题、关闭按钮和取消路径。
  2. 打开后把初始焦点放到合适控件,关闭后恢复到触发元素。
  3. 处理 cancel 事件,例如用户按 Escape。
  4. 不要在内部嵌套另一个模态交互;复杂堆叠需要统一管理。
  5. ::backdrop 设置遮罩,但不要仅靠视觉遮罩表达模态状态。

dialog 能减少手写焦点隔离的工作,但不能自动替你完成文案、校验和异步提交状态管理。

Q22: hiddenaria-hiddeninert 分别解决什么问题?

答案

三者作用层次不同,不能互相替代:

能力hiddenaria-hidden="true"inert
是否显示不显示仍可显示仍可显示
是否进入可访问性树
是否可聚焦、可交互可能仍可以
典型用途完全隐藏内容隐藏重复或装饰性语义暂时禁用一整棵子树

不要把 aria-hidden="true" 放在仍包含焦点的区域,否则视觉用户可以操作、屏幕阅读器却无法感知。弹窗打开时,如果不用原生 dialog,可以用 inert 禁用背景区域。

Q23: HTML 的 videoaudio 有哪些常用属性?自定义播放器要处理什么?

答案

常用属性包括 srccontrolspreloadautoplaymutedloop;视频还常用 posterplaysinline、宽高和 track 字幕。preload 只是提示,浏览器可根据网络与策略调整。

自定义播放器不是只隐藏 controls 再做几个按钮,还要处理:

  • play() 返回 Promise,自动播放可能因用户激活、静音状态和浏览器策略被拒绝。
  • 播放、暂停、等待、进度、音量、倍速、全屏、画中画和错误状态。
  • 键盘操作、焦点、按钮名称、字幕与屏幕阅读器反馈。
  • seek 范围、移动端内联播放、后台切换和媒体会话。
  • 流媒体场景还需 HLS/DASH/MSE、清晰度、缓冲和监控。

浏览器常允许静音自动播放,但不能把“autoplay 必须与 muted 绑定”当成所有环境的绝对语法规则。

Q24: Open Graph 协议是什么?有什么作用?

答案

Open Graph 是一组页面元数据约定,许多社交和即时通信平台会读取 og:titleog:descriptionog:imageog:urlog:type 等字段生成链接预览卡片。

实践中要注意:

  • 标题、描述和图片应与页面真实内容一致,并使用抓取服务可访问的绝对 URL。
  • 图片尺寸、格式和体积按主要平台要求测试,不要只准备一张超大图。
  • og:url 通常与 canonical 策略协调,避免分享统计分散。
  • 不同平台有缓存和私有扩展,更新后可能需要平台调试工具重新抓取。
  • Open Graph 影响分享展示,不直接保证 SEO 排名或固定点击率提升。

Twitter Card 等协议可作为补充,但应避免多套字段内容互相矛盾。

Q25: 表单控件必须写在 form 元素内部吗?form 属性有什么用?

答案

不必须。支持表单关联的控件可以通过 form="表单 id" 归属到文档中其他位置的 form,提交时仍会参与该表单的数据收集和原生校验。这适合按钮或筛选控件因布局需要放在表单外部的场景。

button、部分 input 还可以通过 formactionformmethodformenctype 等属性覆盖所属表单的提交行为,例如同一表单提供“保存草稿”和“正式提交”。

需要注意:

  • 表单要有唯一且稳定的 id,控件的 form 值与它匹配。
  • 只有具有 name 且未被禁用的成功控件通常才会进入提交数据。
  • 视觉上分散的控件仍要保持清晰的标签、错误提示和键盘焦点顺序。

Q26: Canvas 如何实现高清屏适配?

答案

高清屏(Retina)上 1 个 CSS 像素对应多个物理像素。window.devicePixelRatio(简称 dpr)表示这个倍率关系。

核心思路是让 Canvas 的实际像素比 CSS 显示尺寸大 dpr 倍:

高清适配方案
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const dpr = window.devicePixelRatio || 1;

// 期望的 CSS 显示大小
const cssWidth = 300;
const cssHeight = 200;

// 1. canvas 实际像素 = CSS尺寸 * dpr
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;

// 2. CSS 显示尺寸不变
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;

// 3. 缩放 context,让绘图坐标与 CSS 像素对齐
const ctx = canvas.getContext('2d')!;
ctx.scale(dpr, dpr);

关键点

  1. canvas.width/height 设置的是画布的像素分辨率
  2. canvas.style.width/height 设置的是CSS 显示大小
  3. 分辨率放大 dpr 倍 + 显示大小不变 = 用更多像素渲染同样面积 = 高清
  4. ctx.scale(dpr, dpr) 确保后续绑定代码中的坐标值仍然基于 CSS 像素

Q27: Shadow DOM 如何实现样式隔离?open 和 closed 模式有什么区别?

答案

样式隔离原理

Shadow DOM 创建了一个与主文档隔离的 DOM 子树。浏览器在匹配 CSS 选择器时,不会跨越 Shadow DOM 边界

  • 外部样式表中的选择器(如 div { color: red }不会匹配 Shadow DOM 内部的元素
  • Shadow DOM 内部的样式不会泄漏到外部
  • 例外:CSS 自定义属性(--my-color)可以穿透 Shadow 边界,这是设计留的样式定制入口

可以通过 ::part() 伪元素选择器对组件内部暴露了 part attribute 的元素进行样式化。

open vs closed

// open - 外部可通过 element.shadowRoot 访问内部 DOM
this.attachShadow({ mode: 'open' });
document.querySelector('my-el')!.shadowRoot; // ShadowRoot

// closed - 外部无法访问
this.attachShadow({ mode: 'closed' });
document.querySelector('my-el')!.shadowRoot; // null
对比项openclosed
外部访问element.shadowRoot 可用返回 null
调试便利性DevTools 可直接查看需要内部方法暴露
安全性较低,外部可操作内部 DOM较高,但无法完全防止
推荐场景绝大多数场景严格封装需求(如支付表单)
警告

即使使用 closed 模式,也不能作为安全机制依赖。攻击者可以通过重写 Element.prototype.attachShadow 等方式绕过限制。真正的安全应依赖服务端验证。

Q28: 什么是 ARIA?什么时候需要使用?

答案

ARIA(Accessible Rich Internet Applications)是一组 HTML 属性,用于给自定义组件补充语义信息。

使用原则:优先用原生 HTML(<button><input><select>),只在原生元素无法满足时才用 ARIA。比如自定义 Tab 组件需要 role="tablist"role="tab"aria-selected 等属性。

常见属性:role(角色)、aria-label(标签)、aria-expanded(展开状态)、aria-hidden(隐藏)、aria-live(动态内容播报)。

Q29: 表单的 autocomplete 应该怎样配置?

答案

autocomplete 不只是“开或关”,它通过标准 Token 告诉浏览器字段的真实含义,例如 nameemailcurrent-passwordnew-passwordone-time-codestreet-address

正确配置有三类收益:

  • 浏览器和密码管理器能准确填充,减少输入成本。
  • 移动端可以提供更合适的输入建议。
  • 登录、注册和修改密码表单能区分现有密码与新密码,避免错误覆盖。

字段还应有稳定的 name、关联的 label 和正确的 type。不要依赖 autocomplete="off" 阻止密码管理器,也不要用随机字段名对抗自动填充;安全依赖鉴权、CSRF 防护和敏感操作确认。

Q30: 结构化数据和普通 meta 标签有什么区别?

答案

普通 meta 标签主要描述页面级信息,例如标题摘要、分享卡片和抓取策略;结构化数据用 Schema.org 词汇描述“这个内容是什么”,例如文章、商品、职位或面包屑。

前端通常用 JSON-LD 嵌入:

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "前端性能优化实践"
}
</script>

结构化数据应与页面可见内容一致,不能为了富摘要伪造评分或价格。它帮助搜索引擎理解内容,但不保证一定展示富结果;上线前要用搜索引擎提供的校验工具检查字段、URL 和服务端输出。

Q31: FormData API 怎么用?常见场景有哪些?

答案

FormData 是浏览器原生的键值对数据接口,主要用于构造表单数据并通过 fetch / XMLHttpRequest 提交。

创建方式

// 从 form 元素自动收集
const formData = new FormData(document.querySelector('form')!);

// 手动构建
const data = new FormData();
data.append('name', '张三');
data.append('avatar', fileInput.files![0]);

常用方法get()getAll()set()append()delete()has()entries()。其中 set() 会覆盖同名键,append() 会追加。

常见场景

  1. 文件上传FormData 天然支持 File 对象,配合 fetch 时浏览器自动设置 multipart/form-data 编码
  2. 收集表单数据new FormData(form) 一次性获取所有带 name 的控件值
  3. 转换为 JSONObject.fromEntries(formData.entries()) 转为普通对象后 JSON.stringify

面试关键点:使用 fetch 发送 FormData不要手动设置 Content-Type,否则 boundary 会丢失导致服务端解析失败。

Q32: 如何用 Canvas 实现一个简单的动画?

答案

Canvas 动画的核心是 "清空 -> 更新 -> 绘制 -> 循环" 四步。

Canvas 动画框架
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

// 状态
let x = 0;
const speed = 2;

function animate(): void {
// 1. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 2. 更新状态
x += speed;
if (x > canvas.width) x = 0; // 循环

// 3. 绘制
ctx.beginPath();
ctx.arc(x, canvas.height / 2, 20, 0, Math.PI * 2);
ctx.fillStyle = '#4285f4';
ctx.fill();

// 4. 下一帧
requestAnimationFrame(animate);
}

animate();

要点

  • 使用 requestAnimationFrame 而非 setInterval/setTimeout,浏览器会在下一次重绘前调用回调,通常为 60fps
  • 每帧必须先 clearRect 清空画布,否则之前绘制的内容会残留
  • 复杂动画可以用时间差(deltaTime)而非固定速度,以应对帧率波动

Q33: Custom Elements 的生命周期回调有哪些?分别在什么时候触发?

答案

Custom Elements 有四个生命周期回调:

lifecycle-demo.ts
class LifecycleDemo extends HTMLElement {
static get observedAttributes(): string[] {
return ['theme', 'size'];
}

constructor() {
super();
// 1. 元素被创建时(通过 new 或 HTML 解析)
// 注意:此时不应访问 attribute 或子元素
console.log('constructor: 元素被创建');
}

connectedCallback(): void {
// 2. 元素被插入到 DOM 时
// 适合:初始渲染、添加事件监听、发起请求
console.log('connected: 插入 DOM');
}

disconnectedCallback(): void {
// 3. 元素从 DOM 中移除时
// 适合:清理定时器、移除事件监听、断开连接
console.log('disconnected: 从 DOM 移除');
}

attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
// 4. observedAttributes 中声明的 attribute 变化时
// 仅监听 observedAttributes 返回的属性
console.log(`attribute changed: ${name} ${oldValue} -> ${newValue}`);
}

adoptedCallback(): void {
// 5. 元素被 document.adoptNode() 移到新文档时(如 iframe)
// 极少使用
console.log('adopted: 移到新文档');
}
}

触发顺序constructor -> attributeChangedCallback(如果有初始 attribute)-> connectedCallback

关键注意点

  • constructor 中不能访问 attribute 和子元素,初始化渲染应在 connectedCallback
  • attributeChangedCallback 只响应 observedAttributes 声明的 attribute
  • connectedCallback 可能被多次调用(元素被移动时)
  • disconnectedCallback 适合做资源清理,类似 React 的 useEffect 返回的清理函数

Q34: 如何让一个自定义组件(如 Dropdown)支持键盘操作?

答案

  1. 触发按钮用 <button>(原生键盘支持),设置 aria-expandedaria-haspopup
  2. 选项列表用 role="listbox",每个选项 role="option" + aria-selected
  3. 监听 onKeyDownArrowDown/ArrowUp 移动焦点,Enter 选中,Escape 关闭
  4. tabIndex 管理哪个选项可聚焦(roving tabindex 模式)
  5. 打开时焦点进入列表,关闭时焦点回到触发按钮

Q35: data-* 自定义属性怎么用?在 JS/React 中如何获取?

答案

data-* 属性允许在 HTML 元素上存储自定义数据,不影响页面展示,也不会被浏览器特殊处理。

命名规则

  • HTML 中使用连字符命名:data-user-id
  • JS 中通过 dataset 对象以驼峰形式读取:dataset.userId

原生 JS 获取

dataset API
const el = document.querySelector('[data-user-id]') as HTMLElement;

// 读取(自动驼峰转换)
const userId: string | undefined = el.dataset.userId;
const role: string | undefined = el.dataset.role;

// 写入
el.dataset.active = 'true';
// DOM 上会生成 data-active="true"

// 删除
delete el.dataset.active;

// 也可以用 getAttribute/setAttribute
el.getAttribute('data-user-id');
el.setAttribute('data-score', '100');

React 中的用法

React 中使用 data-*
function ProductList({ products }: { products: Array<{ id: string; name: string }> }) {
const handleClick = (e: React.MouseEvent<HTMLLIElement>) => {
const productId = e.currentTarget.dataset.productId;
console.log('点击了产品:', productId);
};

return (
<ul>
{products.map((p) => (
<li key={p.id} data-product-id={p.id} onClick={handleClick}>
{p.name}
</li>
))}
</ul>
);
}
最佳实践
  • 需要传递数据给 JS 但不适合显示在页面上时使用 data-*
  • React 中如果只是组件内部的数据传递,优先用 props / state,而非 data-*
  • data-* 常用于测试标记(如 data-testid)、埋点标记、CSS 选择器等场景

Q36: robots meta 标签和 robots.txt 有什么区别?

答案

两者都用于控制搜索引擎爬虫行为,但作用层级不同

对比项<meta name="robots">robots.txt
作用范围单个页面整个站点
位置HTML <head>站点根目录 /robots.txt
控制粒度页面级(可精确控制每个页面)路径级(按目录/文件匹配)
控制内容是否索引、是否跟踪链接是否允许抓取
优先级如果 robots.txt 禁止抓取,爬虫看不到 meta robots先执行,决定是否抓取页面

meta robots 常见值

<!-- 允许索引,允许跟踪链接(默认行为) -->
<meta name="robots" content="index, follow" />

<!-- 禁止索引,但跟踪链接 -->
<meta name="robots" content="noindex, follow" />

<!-- 禁止索引,禁止跟踪链接 -->
<meta name="robots" content="noindex, nofollow" />

<!-- 禁止缓存快照 -->
<meta name="robots" content="noarchive" />

<!-- 禁止显示摘要 -->
<meta name="robots" content="nosnippet" />
注意

关键区别:如果 robots.txt 禁止了某个路径,爬虫根本不会访问该页面,自然也不会读取其中的 meta robots 标签。所以 robots.txt 是"大门",meta robots 是"房间门"。

Q37: label 标签有什么作用?如何正确使用?

答案

<label> 的核心作用是将文字描述与表单控件建立语义关联,它在可访问性和用户体验上至关重要。

三大作用

  1. 扩大点击区域:点击 label 等于点击关联的 input,对小尺寸的 checkbox/radio 尤其有用
  2. 屏幕阅读器支持:视障用户使用屏幕阅读器时,会朗读 label 的文本内容
  3. 语义化:搜索引擎和辅助工具可以理解表单结构

两种关联方式

<!-- 方式一:for + id 关联(推荐) -->
<label for="email">邮箱</label>
<input type="email" id="email" />

<!-- 方式二:嵌套 -->
<label>
邮箱
<input type="email" />
</label>

面试关键点

  • 每个表单控件都应有对应的 label,这是 WCAG 的基本要求
  • 不要用 placeholder 代替 label,placeholder 在用户输入后消失,且屏幕阅读器支持不一致
  • for 属性方式更灵活,label 和 input 不必在 DOM 中相邻

Q38: Canvas 的 save() 和 restore() 有什么作用?

答案

save()restore() 操作的是 Canvas 的绘图状态栈

  • save():将当前绘图状态压入栈中
  • restore():从栈中弹出最近一次保存的状态并恢复

保存的状态包括:

  • 变换矩阵(translate、rotate、scale)
  • 裁剪区域(clip)
  • 样式属性:fillStyle、strokeStyle、globalAlpha、lineWidth、lineCap、lineJoin、font、textAlign、textBaseline、shadowColor、shadowBlur 等
save/restore 示例
const ctx = canvas.getContext('2d')!;

ctx.fillStyle = 'red';
ctx.save(); // 保存状态 A(fillStyle: red)

ctx.fillStyle = 'blue';
ctx.translate(100, 100);
ctx.save(); // 保存状态 B(fillStyle: blue, translate 100,100)

ctx.fillStyle = 'green';
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 50, 50); // 绿色、旋转 45 度

ctx.restore(); // 恢复到状态 B —— blue、translate(100,100)、无旋转
ctx.fillRect(0, 0, 50, 50); // 蓝色、无旋转

ctx.restore(); // 恢复到状态 A —— red、无位移
ctx.fillRect(0, 0, 50, 50); // 红色、原始位置

典型应用场景:绘制复杂图形时,需要对某个子图形做独立的变换(旋转、缩放),操作完后恢复状态,避免影响后续绘制。

Q39: Web Components 和 React/Vue 组件有什么区别?各自的优缺点?

答案

对比维度Web ComponentsReact/Vue 组件
标准W3C 浏览器标准框架私有实现
运行环境任何浏览器,无需框架需要对应框架运行时
样式隔离Shadow DOM 原生隔离需要 CSS Modules / CSS-in-JS
状态管理无内置方案,需手动实现useState / ref / reactive 等
响应式更新手动操作 DOMVirtual DOM / 响应式自动更新
生态工具较少(Lit、Stencil)极其丰富(路由、状态管理、UI 库)
SSR 支持差(需 Declarative Shadow DOM)成熟(Next.js / Nuxt)
学习成本较低(原生 API)中等(需学习框架概念)
类型支持需手动声明React/Vue + TS 支持完善
跨框架复用天然支持不可跨框架

何时选择 Web Components

  • 需要跨多个框架共享组件(公司级 Design System)
  • 构建第三方嵌入式组件(不希望引入框架依赖)
  • 微前端中需要框架无关的组件封装
  • 对 bundle size 极度敏感的场景

何时选择框架组件

  • 构建完整的单页应用
  • 需要丰富的生态工具支持
  • 团队已有框架技术栈
  • 需要 SSR/SSG 等高级特性
最佳实践

实际项目中,可以采用混合方案:用 Web Components 构建底层不含业务逻辑的原子组件(按钮、输入框、弹窗),用框架组件构建业务组件和页面。Shoelace、Spectrum Web Components 就是这种模式。

Q40: 什么是焦点陷阱(Focus Trap)?什么场景需要?

答案

焦点陷阱是指限制 Tab 焦点只在特定区域内循环,不会跳到背后的页面元素。

需要的场景:Modal/Dialog、全屏抽屉、确认弹窗。当这些覆盖层打开时,用户不应该 Tab 到被遮挡的内容上。

实现:拦截 Tab 事件,到最后一个元素时跳回第一个,Shift+Tab 到第一个时跳到最后一个。关闭时恢复之前的焦点。

Q41: HTML5 的 Drag and Drop API 怎么使用?

答案

HTML5 Drag and Drop(拖拽)API 的使用分为 3 步:

第一步:设置可拖拽

<div draggable="true" id="dragItem">拖拽我</div>

第二步:处理拖拽源事件

拖拽源
const dragItem = document.getElementById('dragItem') as HTMLElement;

dragItem.addEventListener('dragstart', (e: DragEvent) => {
// 设置传输的数据
e.dataTransfer!.setData('text/plain', dragItem.id);
// 设置拖拽效果
e.dataTransfer!.effectAllowed = 'move';
// 可选:设置拖拽时的半透明图像
dragItem.style.opacity = '0.4';
});

dragItem.addEventListener('dragend', () => {
dragItem.style.opacity = '1';
});

第三步:处理放置目标事件

放置目标
const dropZone = document.getElementById('dropZone') as HTMLElement;

// 必须阻止 dragover 的默认行为,否则 drop 不会触发
dropZone.addEventListener('dragover', (e: DragEvent) => {
e.preventDefault();
e.dataTransfer!.dropEffect = 'move';
});

dropZone.addEventListener('drop', (e: DragEvent) => {
e.preventDefault();
const id = e.dataTransfer!.getData('text/plain');
const draggedEl = document.getElementById(id);
if (draggedEl) {
dropZone.appendChild(draggedEl);
}
});

完整的拖拽排序示例

列表拖拽排序
const list = document.getElementById('sortableList') as HTMLUListElement;
let draggedItem: HTMLElement | null = null;

list.addEventListener('dragstart', (e: DragEvent) => {
draggedItem = e.target as HTMLElement;
draggedItem.classList.add('dragging');
});

list.addEventListener('dragover', (e: DragEvent) => {
e.preventDefault();
const afterElement = getDragAfterElement(list, e.clientY);
if (draggedItem) {
if (afterElement) {
list.insertBefore(draggedItem, afterElement);
} else {
list.appendChild(draggedItem);
}
}
});

list.addEventListener('dragend', () => {
draggedItem?.classList.remove('dragging');
draggedItem = null;
});

function getDragAfterElement(container: HTMLElement, y: number): HTMLElement | null {
const items = [...container.querySelectorAll<HTMLElement>('li:not(.dragging)')];
return items.reduce<{ offset: number; element: HTMLElement | null }>(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
}
return closest;
},
{ offset: Number.NEGATIVE_INFINITY, element: null }
).element;
}

Q42: 什么是 canonical URL?什么时候需要设置?

答案

canonical URL(规范链接)通过 <link rel="canonical"> 告诉搜索引擎:"当多个 URL 指向相同或相似内容时,请以这个 URL 为准进行索引"。

<link rel="canonical" href="https://example.com/html/meta-seo" />

需要设置 canonical 的场景

  1. 同一内容多个 URL

    • https://example.com/articlehttps://example.com/article?ref=twitter
    • URL 带有排序、筛选等查询参数
  2. HTTP 和 HTTPS 版本并存

    • http://example.com/pagehttps://example.com/page
  3. www 与非 www

    • www.example.com/pageexample.com/page
  4. 分页内容

    • 列表页 ?page=1?page=2
  5. 移动端和桌面端不同 URL

    • m.example.com/pagewww.example.com/page

不设置 canonical 的后果

  • 搜索引擎认为是重复内容,分散页面权重(PageRank)
  • 可能导致搜索引擎索引了错误的 URL 版本
  • 严重时可能被搜索引擎降权

Q43: React 中受控表单、非受控表单和 Form Action 怎么选?

答案

  • 受控表单:输入值来自 React state,通过 valueonChange 更新。适合实时联动、格式化和复杂校验,但高频大表单要控制渲染范围。
  • 非受控表单:值由 DOM 保存,通过原生提交、FormData 或 ref 读取。代码更少,适合简单表单和文件输入,但实时联动能力较弱。
  • Form Action:React 19 可把函数传给 form action 或按钮的 formAction,并配合 useActionStateuseFormStatus 管理提交结果与 pending 状态;它可与服务端框架能力结合,但并不要求所有字段都改成非受控。

选择取决于交互:需要逐键反馈的字段可以受控,普通提交字段可用原生表单语义。同一表单可以混合,但不要在组件生命周期中把同一输入从受控切换成非受控。

无论哪种方式,服务端都要重新验证;提交还要处理重复点击、并发结果、错误聚焦和渐进增强。

Q44: SVG 的 viewBox 属性是什么?有什么用?

答案

viewBox 定义了 SVG 的内部坐标系,格式为 viewBox="min-x min-y width height"

它的作用是在 SVG 的视口(viewport)内部坐标空间之间建立映射关系,从而实现:

  1. 响应式缩放:内部图形会自动按比例缩放到视口大小
  2. 裁剪和平移:调整 min-x、min-y 可以显示图形的不同区域
  3. 等比例布局:无论 SVG 实际显示多大,内部坐标始终一致
viewBox 示例
<!-- 内部坐标 0~100,实际显示 300x300 -->
<!-- 圆心在 (50,50) 半径 40,无论 SVG 实际多大,圆始终居中 -->
<svg viewBox="0 0 100 100" width="300" height="300">
<circle cx="50" cy="50" r="40" fill="#4285f4" />
</svg>

<!-- 同样的 viewBox,显示 600x600,图形自动放大,不失真 -->
<svg viewBox="0 0 100 100" width="600" height="600">
<circle cx="50" cy="50" r="40" fill="#4285f4" />
</svg>

配合 preserveAspectRatio 属性可以控制缩放行为(类似 CSS 的 object-fit):

<!-- 保持比例,居中对齐 -->
<svg viewBox="0 0 100 50" preserveAspectRatio="xMidYMid meet" width="300" height="300">
...
</svg>

<!-- 不保持比例,拉伸填满 -->
<svg viewBox="0 0 100 50" preserveAspectRatio="none" width="300" height="300">
...
</svg>

Q45: 如何在 React 中使用 Web Components?有什么注意事项?

答案

React 18 及之前的注意事项

  1. 属性传递问题:React 将所有 props 作为 HTML attribute(字符串)设置到 DOM 上,而 Web Components 通常需要 property(任意类型)。对象/数组类型的数据会被 toString()[object Object]

  2. 事件绑定问题:React 的事件系统基于合成事件,onXxx 只能绑定标准 DOM 事件。Web Components 的 CustomEvent 需要通过 ref 手动绑定。

React18Usage.tsx
function App() {
const ref = useRef<HTMLElement>(null);

useEffect(() => {
const el = ref.current;
if (!el) return;

// 问题 1:复杂数据需要通过 property 设置
(el as any).config = { theme: 'dark', items: [1, 2, 3] };

// 问题 2:自定义事件需要通过 addEventListener
const handler = (e: Event) => console.log(e);
el.addEventListener('my-event', handler);
return () => el.removeEventListener('my-event', handler);
}, []);

return <my-component ref={ref} name="hello" />;
}

React 19 的改进

React19Usage.tsx
// React 19 自动处理 property vs attribute
function App() {
return (
<my-component
// 自动作为 property 传递(不再 toString)
config={{ theme: 'dark', items: [1, 2, 3] }}
// 自定义事件可以直接绑定
onMyEvent={(e: CustomEvent) => console.log(e.detail)}
name="hello"
/>
);
}

通用最佳实践

  1. 使用 declare global 声明自定义元素的 JSX 类型
  2. 在 React 18 中创建 wrapper 组件封装 ref 逻辑
  3. 确保 Web Components 的 script 在 React 渲染前加载
  4. 使用 composed: true 确保自定义事件能穿透 Shadow DOM

相关链接