HTML 热门面试题
使用说明
这组题共 45 道,覆盖 HTML 分类最常问的语义化、可访问性、资源加载、表单、SEO 与 Web Components。口头回答时先说结论,再补使用场景和取舍,不需要背规范原文。
Q1: 什么是 HTML 语义化?为什么重要?
答案:
语义化就是根据内容职责选择合适的标签,例如导航用 nav、正文用 main、独立内容用 article,而不是全部使用 div。
- 它让浏览器、搜索引擎和屏幕阅读器更容易理解页面结构。
- 代码对开发者也更可读,但语义标签不会自动带来正确样式和无障碍体验。
- 选择标签应看内容含义,不要为了“看起来高级”滥用
section或article。
Q2: div、section 和 article 应该怎么选?
答案:
<div>— 无语义,纯粹的容器,用于布局和 CSS 分组<section>— 有主题的区块,通常有标题,表示页面中的一个逻辑部分<article>— 独立完整的内容,可以脱离上下文独立存在(如一篇博客、一条评论)
判断标准:内容能独立分享/订阅吗?能 → <article>;是页面的一个主题区块?→ <section>;纯布局需要?→ <div>。
Q3: DOCTYPE 有什么作用?
答案:
<!DOCTYPE html> 是一条文档类型声明,告诉浏览器使用哪种标准来解析文档。
作用:触发浏览器的标准模式(Standards Mode) 进行渲染。
不写 DOCTYPE 的后果:浏览器会进入怪异模式(Quirks Mode),表现如下:
| 特性 | 标准模式 | 怪异模式 |
|---|---|---|
| 盒模型 | content-box(width 不含 padding/border) | border-box(width 包含 padding/border) |
| 图片底部间隙 | inline 元素有基线对齐间隙 | 无间隙 |
| 百分比高度 | 需父元素有明确高度 | 可以不需要 |
| CSS 解析 | 严格按规范 | 兼容旧浏览器的非标准行为 |
console.log(document.compatMode);
// "CSS1Compat" -> 标准模式
// "BackCompat" -> 怪异模式
<!DOCTYPE html>不是 HTML 标签,而是一条给浏览器的指令- HTML5 的 DOCTYPE 不需要引用 DTD(HTML4 需要)
- 必须写在文档的第一行,前面不能有任何字符(包括空行和注释)
Q4: async 和 defer 有什么区别?
答案:
二者都能避免普通外部脚本阻塞 HTML 下载,但执行时机和顺序不同。
async下载完立即执行,不保证多个脚本的执行顺序,适合独立统计脚本。defer在文档解析完成后、DOMContentLoaded前按文档顺序执行,适合有依赖的应用脚本。type="module"默认具有类似defer的行为,还支持模块依赖和严格模式。
Q5: DOMContentLoaded 和 load 有什么区别?
答案:
DOMContentLoaded 表示 HTML 已解析并构建 DOM,不等待图片等普通资源;load 要等页面及其依赖资源基本加载完成。
初始化 DOM 交互通常监听前者;确实依赖图片尺寸或全部资源时才使用后者。现代框架一般通过挂载时机管理,不需要到处手写全局监听。
Q6: 图片的 alt 应该怎么写?
答案:
alt 要表达图片在当前上下文中的信息或功能,而不是机械描述文件名。
- 信息图片写等价的简短文本。
- 作为按钮的图片描述动作,例如“搜索”,不要只写“放大镜”。
- 纯装饰图片使用空字符串
alt="",让屏幕阅读器跳过;不能省略后让它朗读文件名。
Q7: 原生 HTML 如何提升可访问性?
答案:
优先使用有内建语义和键盘行为的原生元素,再补充标签关联、焦点顺序和状态提示。
- 点击操作用
button,导航用a,不要用带onClick的div冒充。 - 表单用
label关联控件,错误信息要能被辅助技术感知。 - 只有原生语义不足时才使用 ARIA;ARIA 能补语义,但不会自动实现键盘交互。
Q8: html 的 lang 属性为什么重要?
答案:
lang 声明页面主要语言,帮助屏幕阅读器选择正确发音规则,也会影响浏览器翻译、拼写检查、搜索理解和 CSS :lang() 匹配。
应使用有效的 BCP 47 语言标签,例如简体中文常写 zh-CN,具体内容切换语言时可在局部元素上覆盖。它不能替代字符编码声明,也不会自动翻译页面。
服务端渲染多语言页面时要保证 lang 与实际内容一致;仅写一个固定值却输出另一种语言,会比缺失更误导辅助技术。
Q9: 表单原生校验和业务校验怎么分工?
答案:
原生属性如 required、type、min、pattern 适合提供基础约束和移动端输入体验,复杂业务规则由应用层处理。
前端校验用于即时反馈,服务端仍必须重新校验,因为请求可以绕过浏览器。错误提示要定位到字段,并保留用户输入。
Q10: Canvas 和 SVG 怎么选择?
答案:
两者的核心差异是渲染模型:
- SVG:每个图形都是 DOM 节点,天然支持事件、可访问性和 CSS,缩放不失真,适合元素数量可控、需要精细交互的图标、流程图和图表。
- Canvas:把绘制结果写进位图,没有逐图元 DOM,适合大量对象、高频重绘、像素处理、游戏和自定义渲染器。
选择时看真实数据规模、更新频率、命中检测、文本可访问性、导出和团队工具链,不要背“超过几百或几万个元素”的固定分界。复杂应用也可混合:Canvas 绘制主体,DOM/SVG 承载标签、焦点和交互层。
Canvas 还要自行处理 DPR、重绘和命中检测;SVG 节点过多时则会增加 DOM、样式和布局成本。
Q11: 什么是响应式图片?
答案:
响应式图片让浏览器根据视口、像素密度和格式支持选择合适资源,常用 srcset、sizes 和 picture。
srcset提供候选图片,sizes描述图片实际展示宽度。picture可做艺术裁剪或 AVIF/WebP 格式回退。- 还要设置宽高或
aspect-ratio,减少图片加载造成的布局偏移。
Q12: preload、prefetch 和懒加载有什么区别?
答案:
preload 提前加载当前页面确定很快会用到的高优先级资源;prefetch 为未来导航准备低优先级资源;懒加载则延后非关键资源。
预加载不是越多越好,错误使用会与首屏关键资源抢带宽。图片和 iframe 可结合原生 loading="lazy",首屏主图通常不应懒加载。
Q13: iframe 有哪些安全注意点?
答案:
iframe 会引入新的页面上下文和第三方代码风险,应通过来源、能力和通信三层限制。
- 使用
sandbox只开放必要能力,谨慎组合脚本与同源权限。 - 用 CSP、
frame-ancestors或相关响应头控制谁能嵌入页面。 postMessage通信必须校验origin、消息结构和业务权限,不能使用任意来源通配。
Q14: Web Components 包含哪些核心能力?
答案:
Web Components 由三大核心技术组成:
-
Custom Elements:允许开发者自定义 HTML 标签,继承
HTMLElement,通过customElements.define()注册。提供connectedCallback、disconnectedCallback、attributeChangedCallback、adoptedCallback四个生命周期回调。 -
Shadow DOM:为自定义元素提供独立的 DOM 树和样式作用域。通过
attachShadow({ mode: 'open' | 'closed' })创建。内部样式不会泄漏到外部,外部样式也不会影响内部(CSS 自定义属性除外),实现浏览器原生级别的样式隔离。 -
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> | 机器可读的时间/日期 |
作用:
- SEO 优化:搜索引擎能更准确地理解页面结构和内容权重
- 可访问性:屏幕阅读器可通过语义标签提供更好的导航体验(如按区域跳转)
- 代码可读性:开发者看到标签名就能理解内容含义,无需依赖 class 名
- 维护性:结构清晰,减少沟通成本
详细内容请参考 语义化与可访问性。
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 的后果:
- 浏览器使用默认布局视口(通常 980px),页面在手机上显示很小
- 用户需要双指缩放才能阅读文字
- 媒体查询中的
max-width断点不会生效 - 响应式布局完全失效
不要设置 user-scalable=no 或 maximum-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(文件选择,支持 accept 和 multiple)、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。
使用时仍要补齐业务体验:
- 提供可理解的标题、关闭按钮和取消路径。
- 打开后把初始焦点放到合适控件,关闭后恢复到触发元素。
- 处理
cancel事件,例如用户按 Escape。 - 不要在内部嵌套另一个模态交互;复杂堆叠需要统一管理。
- 用
::backdrop设置遮罩,但不要仅靠视觉遮罩表达模态状态。
dialog 能减少手写焦点隔离的工作,但不能自动替你完成文案、校验和异步提交状态管理。
Q22: hidden、aria-hidden 和 inert 分别解决什么问题?
答案:
三者作用层次不同,不能互相替代:
| 能力 | hidden | aria-hidden="true" | inert |
|---|---|---|---|
| 是否显示 | 不显示 | 仍可显示 | 仍可显示 |
| 是否进入可访问性树 | 否 | 否 | 否 |
| 是否可聚焦、可交互 | 否 | 可能仍可以 | 否 |
| 典型用途 | 完全隐藏内容 | 隐藏重复或装饰性语义 | 暂时禁用一整棵子树 |
不要把 aria-hidden="true" 放在仍包含焦点的区域,否则视觉用户可以操作、屏幕阅读器却无法感知。弹窗打开时,如果不用原生 dialog,可以用 inert 禁用背景区域。
Q23: HTML 的 video 和 audio 有哪些常用属性?自定义播放器要处理什么?
答案:
常用属性包括 src、controls、preload、autoplay、muted、loop;视频还常用 poster、playsinline、宽高和 track 字幕。preload 只是提示,浏览器可根据网络与策略调整。
自定义播放器不是只隐藏 controls 再做几个按钮,还要处理:
play()返回 Promise,自动播放可能因用户激活、静音状态和浏览器策略被拒绝。- 播放、暂停、等待、进度、音量、倍速、全屏、画中画和错误状态。
- 键盘操作、焦点、按钮名称、字幕与屏幕阅读器反馈。
- seek 范围、移动端内联播放、后台切换和媒体会话。
- 流媒体场景还需 HLS/DASH/MSE、清晰度、缓冲和监控。
浏览器常允许静音自动播放,但不能把“autoplay 必须与 muted 绑定”当成所有环境的绝对语法规则。
Q24: Open Graph 协议是什么?有什么作用?
答案:
Open Graph 是一组页面元数据约定,许多社交和即时通信平台会读取 og:title、og:description、og:image、og:url、og:type 等字段生成链接预览卡片。
实践中要注意:
- 标题、描述和图片应与页面真实内容一致,并使用抓取服务可访问的绝对 URL。
- 图片尺寸、格式和体积按主要平台要求测试,不要只准备一张超大图。
og:url通常与 canonical 策略协调,避免分享统计分散。- 不同平台有缓存和私有扩展,更新后可能需要平台调试工具重新抓取。
- Open Graph 影响分享展示,不直接保证 SEO 排名或固定点击率提升。
Twitter Card 等协议可作为补充,但应避免多套字段内容互相矛盾。
Q25: 表单控件必须写在 form 元素内部吗?form 属性有什么用?
答案:
不必须。支持表单关联的控件可以通过 form="表单 id" 归属到文档中其他位置的 form,提交时仍会参与该表单的数据收集和原生校验。这适合按钮或筛选控件因布局需要放在表单外部的场景。
button、部分 input 还可以通过 formaction、formmethod、formenctype 等属性覆盖所属表单的提交行为,例如同一表单提供“保存草稿”和“正式提交”。
需要注意:
- 表单要有唯一且稳定的
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);
关键点:
canvas.width/height设置的是画布的像素分辨率canvas.style.width/height设置的是CSS 显示大小- 分辨率放大 dpr 倍 + 显示大小不变 = 用更多像素渲染同样面积 = 高清
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
| 对比项 | open | closed |
|---|---|---|
| 外部访问 | 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 告诉浏览器字段的真实含义,例如 name、email、current-password、new-password、one-time-code 和 street-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() 会追加。
常见场景:
- 文件上传:
FormData天然支持File对象,配合fetch时浏览器自动设置multipart/form-data编码 - 收集表单数据:
new FormData(form)一次性获取所有带name的控件值 - 转换为 JSON:
Object.fromEntries(formData.entries())转为普通对象后JSON.stringify
面试关键点:使用 fetch 发送 FormData 时不要手动设置 Content-Type,否则 boundary 会丢失导致服务端解析失败。
Q32: 如何用 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 有四个生命周期回调:
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声明的 attributeconnectedCallback可能被多次调用(元素被移动时)disconnectedCallback适合做资源清理,类似 React 的useEffect返回的清理函数
Q34: 如何让一个自定义组件(如 Dropdown)支持键盘操作?
答案:
- 触发按钮用
<button>(原生键盘支持),设置aria-expanded和aria-haspopup - 选项列表用
role="listbox",每个选项role="option"+aria-selected - 监听
onKeyDown:ArrowDown/ArrowUp移动焦点,Enter选中,Escape关闭 - 用
tabIndex管理哪个选项可聚焦(roving tabindex 模式) - 打开时焦点进入列表,关闭时焦点回到触发按钮
Q35: data-* 自定义属性怎么用?在 JS/React 中如何获取?
答案:
data-* 属性允许在 HTML 元素上存储自定义数据,不影响页面展示,也不会被浏览器特殊处理。
命名规则:
- HTML 中使用连字符命名:
data-user-id - JS 中通过
dataset对象以驼峰形式读取:dataset.userId
原生 JS 获取:
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 中的用法:
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> 的核心作用是将文字描述与表单控件建立语义关联,它在可访问性和用户体验上至关重要。
三大作用:
- 扩大点击区域:点击 label 等于点击关联的 input,对小尺寸的 checkbox/radio 尤其有用
- 屏幕阅读器支持:视障用户使用屏幕阅读器时,会朗读 label 的文本内容
- 语义化:搜索引擎和辅助工具可以理解表单结构
两种关联方式:
<!-- 方式一: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 等
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 Components | React/Vue 组件 |
|---|---|---|
| 标准 | W3C 浏览器标准 | 框架私有实现 |
| 运行环境 | 任何浏览器,无需框架 | 需要对应框架运行时 |
| 样式隔离 | Shadow DOM 原生隔离 | 需要 CSS Modules / CSS-in-JS |
| 状态管理 | 无内置方案,需手动实现 | useState / ref / reactive 等 |
| 响应式更新 | 手动操作 DOM | Virtual 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 的场景:
-
同一内容多个 URL:
https://example.com/article和https://example.com/article?ref=twitter- URL 带有排序、筛选等查询参数
-
HTTP 和 HTTPS 版本并存:
http://example.com/page和https://example.com/page
-
www 与非 www:
www.example.com/page和example.com/page
-
分页内容:
- 列表页
?page=1、?page=2等
- 列表页
-
移动端和桌面端不同 URL:
m.example.com/page和www.example.com/page
不设置 canonical 的后果:
- 搜索引擎认为是重复内容,分散页面权重(PageRank)
- 可能导致搜索引擎索引了错误的 URL 版本
- 严重时可能被搜索引擎降权
Q43: React 中受控表单、非受控表单和 Form Action 怎么选?
答案:
- 受控表单:输入值来自 React state,通过
value和onChange更新。适合实时联动、格式化和复杂校验,但高频大表单要控制渲染范围。 - 非受控表单:值由 DOM 保存,通过原生提交、
FormData或 ref 读取。代码更少,适合简单表单和文件输入,但实时联动能力较弱。 - Form Action:React 19 可把函数传给
form action或按钮的formAction,并配合useActionState、useFormStatus管理提交结果与 pending 状态;它可与服务端框架能力结合,但并不要求所有字段都改成非受控。
选择取决于交互:需要逐键反馈的字段可以受控,普通提交字段可用原生表单语义。同一表单可以混合,但不要在组件生命周期中把同一输入从受控切换成非受控。
无论哪种方式,服务端都要重新验证;提交还要处理重复点击、并发结果、错误聚焦和渐进增强。
Q44: SVG 的 viewBox 属性是什么?有什么用?
答案:
viewBox 定义了 SVG 的内部坐标系,格式为 viewBox="min-x min-y width height"。
它的作用是在 SVG 的视口(viewport)和内部坐标空间之间建立映射关系,从而实现:
- 响应式缩放:内部图形会自动按比例缩放到视口大小
- 裁剪和平移:调整 min-x、min-y 可以显示图形的不同区域
- 等比例布局:无论 SVG 实际显示多大,内部坐标始终一致
<!-- 内部坐标 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 及之前的注意事项:
-
属性传递问题:React 将所有 props 作为 HTML attribute(字符串)设置到 DOM 上,而 Web Components 通常需要 property(任意类型)。对象/数组类型的数据会被
toString()成[object Object]。 -
事件绑定问题:React 的事件系统基于合成事件,
onXxx只能绑定标准 DOM 事件。Web Components 的CustomEvent需要通过ref手动绑定。
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 的改进:
// 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"
/>
);
}
通用最佳实践:
- 使用
declare global声明自定义元素的 JSX 类型 - 在 React 18 中创建 wrapper 组件封装 ref 逻辑
- 确保 Web Components 的 script 在 React 渲染前加载
- 使用
composed: true确保自定义事件能穿透 Shadow DOM