HTML 高频面试题

10 阅读53分钟

HTML 高频面试题

1. DOCTYPE 的作用是什么?

<!DOCTYPE> 声明位于 HTML 文档的第一行,它不是 HTML 标签,而是一条指令,用于告知浏览器当前文档使用哪个 HTML 版本进行解析,从而决定渲染模式。

浏览器的三种渲染模式:

  • 标准模式(Standards Mode):浏览器按照 W3C 标准规范渲染页面,盒模型、布局等行为可预测且一致
  • 怪异模式(Quirks Mode):浏览器模拟 IE5 等旧版浏览器行为来渲染页面。例如盒模型会采用 IE 盒模型(width 包含 padding 和 border),这会导致不同浏览器下表现不一致
  • 近乎标准模式(Almost Standards Mode):大部分行为与标准模式一致,仅在少数细节(如表格单元格中图片底部间隙的处理)与标准模式有差异

为什么 HTML5 的 DOCTYPE 这么简洁?

HTML5 不基于 SGML(标准通用标记语言),所以不需要引用 DTD(文档类型定义)。而 HTML4.01 基于 SGML,需要引用 DTD 来定义文档规则:

<!-- HTML5:简洁声明 -->
<!DOCTYPE html>

<!-- HTML4.01 Strict:需要引用 DTD -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd">

<!-- HTML4.01 Transitional -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">

面试加分点:如果省略 DOCTYPE,浏览器会进入怪异模式,可能导致 CSS 盒模型计算、表格布局等行为不符合预期,尤其在跨浏览器场景下容易出现兼容性问题。


2. HTML5 新增了哪些语义化标签?

什么是语义化?

语义化是指根据内容的含义选择合适的标签,而不是一味使用 <div> 和 <span>。语义化的好处:

  1. 对开发者友好:代码结构清晰、可读性强,团队协作更高效
  2. 对 SEO 友好:搜索引擎爬虫能更好地理解页面结构,提升搜索排名
  3. 对无障碍友好:屏幕阅读器可以正确识别页面区域,帮助视障用户导航
  4. 对浏览器友好:部分标签有默认行为(如 <details> 可折叠),减少 JS 代码

HTML5 新增的语义化标签:

标签含义使用场景
<header>页面或区块的头部页头、文章头部
<nav>导航链接区域主导航、面包屑
<main>页面主要内容(唯一)每页只能有一个
<article>独立的、完整的内容块博客文章、新闻条目、评论
<section>文档中的主题分组文章章节、选项卡面板
<aside>与主内容间接相关的内容侧边栏、广告、相关链接
<footer>页面或区块的底部版权信息、联系方式
<figure> / <figcaption>独立内容及其说明图片、代码块、图表
<details> / <summary>可折叠的详情内容FAQ、额外说明
<mark>高亮标记文本搜索结果高亮
<time>时间/日期发布日期、事件时间
<progress>进度条文件上传、任务进度
<!-- ❌ 不语义化:全是 div,结构不清晰 -->
<div class="header">
  <div class="nav">...</div>
</div>
<div class="content">
  <div class="article">...</div>
  <div class="sidebar">...</div>
</div>
<div class="footer">...</div>

<!-- ✅ 语义化:结构清晰、含义明确 -->
<header>
  <nav>
    <ul>
      <li><a href="/">首页</a></li>
      <li><a href="/blog">博客</a></li>
      <li><a href="/about">关于</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <header>
      <h1>文章标题</h1>
      <time datetime="2026-04-15">2026年4月15日</time>
    </header>
    <section>
      <h2>第一章</h2>
      <p>文章内容...</p>
      <figure>
        <img src="chart.png" alt="数据统计图" />
        <figcaption>图1:2026年用户增长趋势</figcaption>
      </figure>
    </section>
    <footer>
      <p>作者:张三 | 标签:前端、HTML5</p>
    </footer>
  </article>

  <aside>
    <h3>相关推荐</h3>
    <ul>
      <li><a href="/post/1">CSS3 新特性详解</a></li>
      <li><a href="/post/2">JavaScript 性能优化</a></li>
    </ul>
  </aside>
</main>

<footer>
  <p>&copy; 2026 版权所有</p>
</footer>

面试加分点:<article> 和 <section> 的区别——<article> 强调内容的独立性(脱离上下文仍然完整,如一篇博客文章),<section> 强调内容的主题性分组(文章的章节)。<article> 内可以嵌套 <section>,<section> 内也可以嵌套 <article>。


3. src 和 href 的区别?

两者都用于引入外部资源,但作用机制完全不同:

对比项srchref
全称source(来源)hypertext reference(超文本引用)
作用替换当前元素的内容建立关联,指向外部资源
加载行为下载并嵌入到当前位置建立通道让浏览器识别引用资源
阻塞解析<script src> 会阻塞 HTML 解析<link href> 不阻塞 HTML 解析(但会阻塞渲染)
常用标签<script> <img> <video> <iframe><a> <link>

为什么 <script src> 会阻塞?

因为浏览器需要先下载并执行脚本(脚本可能会修改 DOM),只有执行完毕后才能继续解析后续 HTML。而 <link href> 引用的 CSS 虽然不阻塞 HTML 解析,但会阻塞渲染树的构建(浏览器需要等 CSSOM 构建完毕才能渲染页面)。

<!-- href:建立关联关系 -->
<link rel="stylesheet" href="style.css" />  <!-- 不阻塞 HTML 解析,但阻塞渲染 -->
<a href="https://example.com">跳转链接</a>  <!-- 不下载,仅建立超链接 -->
<link rel="preconnect" href="https://cdn.example.com" />  <!-- 预连接 -->

<!-- src:替换当前元素内容 -->
<script src="app.js"></script>        <!-- ⚠️ 阻塞 HTML 解析和渲染 -->
<img src="logo.png" alt="logo" />     <!-- 不阻塞解析,异步加载 -->
<video src="movie.mp4" controls></video>
<iframe src="page.html"></iframe>

<!-- 常见错误写法 -->
<script href="app.js"></script>  <!-- ❌ script 应该用 src -->
<link src="style.css" />         <!-- ❌ link 应该用 href -->

面试加分点:为了避免 <script> 阻塞页面解析,有三种策略:① 将 <script> 放在 </body> 前面;② 使用 defer 属性;③ 使用 async 属性。这正好可以衔接下一道面试题。


4. script 标签中 defer 和 async 的区别?

两者都是为了解决 <script> 阻塞 HTML 解析的问题,但执行时机和顺序不同:

属性下载时机执行时机执行顺序适用场景
无属性立即下载,阻塞解析下载完立即执行按顺序需要立即执行的关键脚本
async异步下载,不阻塞解析下载完立即执行(可能阻塞解析)不保证顺序独立脚本(统计、广告)
defer异步下载,不阻塞解析DOMContentLoaded 之前,DOM 解析完后执行保证顺序依赖 DOM 的脚本

执行流程对比:

无属性:   HTML解析 → 暂停 → 下载JS → 执行JS → 继续HTML解析
async:   HTML解析 ──────────────────→ 继续解析...
              └─ 下载JS → 执行JS(下载完就执行,可能打断解析)
defer:   HTML解析 ──────────────────→ 解析完毕 → 执行JS → DOMContentLoaded
              └─ 下载JS ──────────────┘(等DOM解析完才执行)
<!-- 无属性:阻塞解析,适合关键的内联配置 -->
<script src="critical-config.js"></script>

<!-- async:异步加载,下载完就执行,不保证顺序 -->
<!-- 适合独立脚本,如第三方统计、广告等 -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script async src="ad-sdk.js"></script>

<!-- defer:异步加载,DOM 解析完后按顺序执行 -->
<!-- 适合需要操作 DOM 的脚本,且有依赖顺序 -->
<script defer src="vue.js"></script>      <!-- 先执行 -->
<script defer src="vue-router.js"></script> <!-- 后执行 -->
<script defer src="app.js"></script>      <!-- 最后执行 -->

面试加分点:DOMContentLoaded 事件会等所有 defer 脚本执行完毕后才触发。而 async 脚本的执行时机不确定,可能在 DOMContentLoaded 之前或之后。实际项目中,第三方无依赖的脚本用 async,自己的业务脚本用 defer。


5. 什么是回流(Reflow)和重绘(Repaint)?

浏览器渲染页面的基本流程:解析 HTML → 构建 DOM 树 → 构建 CSSOM 树 → 合成渲染树(Render Tree)→ 布局(Layout/Reflow)→ 绘制(Paint)→ 合成(Composite)

  • 回流(Reflow/Layout):当元素的几何属性(尺寸、位置、显示状态)发生变化时,浏览器需要重新计算元素在页面中的位置和大小,然后重新布局。回流的性能开销较大,因为可能会影响其他元素的位置。
  • 重绘(Repaint):当元素的外观属性(颜色、背景、阴影、可见性)发生变化,但不影响布局时,浏览器只需要重新绘制该元素。
  • 关系:回流一定触发重绘,但重绘不一定触发回流。

常见触发回流的操作:

操作类型示例
改变元素几何属性width height padding margin border
改变元素位置top left position float
改变显示状态display: none/block
改变窗口大小window.resize
改变文档内容增删 DOM 节点、改变文本内容
读取布局信息offsetWidth offsetHeight scrollTop getComputedStyle()
改变字体大小font-size font-family

仅触发重绘(不触发回流)的属性:color、background-color、visibility、box-shadow、outline

// ❌ 触发多次回流:每次循环都读写交替
for (let i = 0; i < 100; i++) {
  // 读取 offsetLeft 会强制浏览器执行回流以获取最新值
  element.style.left = element.offsetLeft + 1 + 'px'
}

// ✅ 优化1:读写分离,只触发一次回流
const left = element.offsetLeft
for (let i = 0; i < 100; i++) {
  element.style.left = left + i + 'px'
}

// ✅ 优化2:使用 DocumentFragment 批量操作 DOM
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)  // 在内存中操作,不触发回流
}
document.querySelector('ul').appendChild(fragment)  // 一次性插入,只触发一次回流

// ✅ 优化3:使用 CSS class 代替多次内联样式修改
// 不推荐:触发3次回流
element.style.width = '200px'
element.style.height = '100px'
element.style.margin = '10px'
// 推荐:只触发1次回流
element.classList.add('new-layout')

// ✅ 优化4:对复杂动画使用 transform(只触发合成,不触发回流和重绘)
// 不推荐:使用 left/top 做动画,每帧都触发回流
element.style.left = newX + 'px'
// 推荐:使用 transform,在 GPU 合成层完成,不触发回流
element.style.transform = `translateX(${newX}px)`

// ✅ 优化5:使用 will-change 提示浏览器提前优化
// CSS: .animated { will-change: transform, opacity; }

// ✅ 优化6:离线操作 —— 先隐藏元素,修改后再显示
element.style.display = 'none'   // 触发一次回流
// ... 进行多次 DOM 修改操作 ...(不触发回流,因为元素不在渲染树中)
element.style.display = 'block'  // 触发一次回流

面试加分点:现代浏览器有回流队列优化机制,会将多次回流操作合并为一次。但当你读取布局属性(如 offsetWidth、scrollTop、getComputedStyle())时,浏览器为了返回准确值,会强制清空队列,立即执行回流。这就是为什么"读写分离"是最重要的优化策略。此外,CSS 的 contain: layout(或 contain: strict)属性可以告诉浏览器该元素内部的布局变化不会影响外部,从而将回流限制在容器内部,显著减少回流的波及范围。


6. meta viewport 的作用?

<meta name="viewport"> 用于控制移动设备上的视口(Viewport)行为。在没有 viewport 标签的情况下,移动端浏览器默认会将页面宽度设为 980px(或类似值),然后缩小显示整个页面,导致文字极小、需要手动缩放。添加 viewport 标签后,可以让页面宽度适配设备屏幕。

<!-- 最常用的设置 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
属性说明取值
width视口宽度device-width 或具体像素值(如 375)
height视口高度device-height 或具体像素值(较少使用)
initial-scale初始缩放比例0.1 ~ 10,通常设为 1.0
minimum-scale最小缩放比例0.1 ~ 10
maximum-scale最大缩放比例0.1 ~ 10
user-scalable是否允许用户手动缩放yes / no
<!-- 移动端常用配置:允许缩放 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<!-- 禁止缩放(不推荐,影响无障碍体验) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

<!-- iOS Safari 中禁止双击缩放、优化体验 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

面试加分点:viewport-fit=cover 是为了配合 iPhone X 及以上机型的"刘海屏"安全区域适配,配合 CSS 的 env(safe-area-inset-top) 等环境变量使用。另外,不建议设置 user-scalable=no,因为这会影响视障用户的使用体验,违反 WCAG 无障碍指南。


7. 行内元素和块级元素的区别?

HTML 元素根据默认的 display 属性值可以分为三类:

特性块级元素(block)行内元素(inline)行内块元素(inline-block)
独占一行✅ 是❌ 否,与其他行内元素同行❌ 否,与其他行内元素同行
可设置宽高✅ 是❌ 否,由内容决定✅ 是
默认宽度填满父容器内容撑开内容撑开
margin四个方向都生效仅左右生效四个方向都生效
padding四个方向都生效四个方向都生效(但上下不推开其他元素)四个方向都生效
常见标签div p h1~h6 ul ol li form table hrspan a em strong i b label codeimg input button select textarea

注意事项:

  • 行内元素的 padding-top / padding-bottom 虽然视觉上会渲染,但不会影响周围元素的布局(不推开其他元素)
  • 可以通过 CSS 的 display 属性互相转换:display: block / inline / inline-block
  • HTML5 不再严格使用"块级"和"行内"的分类,而是采用更细致的内容模型(如 flow content、phrasing content 等),但面试中仍以传统分类为主
<!-- 块级元素:独占一行 -->
<div style="background: lightblue; margin: 5px;">块级元素1</div>
<p style="background: lightgreen; margin: 5px;">块级元素2(p标签)</p>

<!-- 行内元素:在同一行显示 -->
<span style="background: yellow;">行内1</span>
<a href="#" style="background: pink;">行内2</a>
<strong style="background: lavender;">行内3</strong>

<!-- 行内块元素:同行显示,但可设宽高 -->
<input type="text" placeholder="行内块" style="width: 120px;" />
<img src="logo.png" alt="图片也是行内块" style="width: 50px; height: 50px;" />

<!-- ❌ 常见错误:行内元素内嵌套块级元素 -->
<span><div>不推荐的嵌套方式</div></span>
<!-- ✅ 特例:<a> 标签在 HTML5 中可以包裹块级元素 -->
<a href="/detail">
  <div class="card">整个卡片都可以点击</div>
</a>

面试加分点:面试官可能追问"空白间隙"问题——两个 inline-block 元素之间会出现约 4px 的间隙,这是 HTML 中换行符/空格造成的。解决方法:① 父元素设置 font-size: 0;② 使用 flexbox 布局;③ 删除 HTML 中的换行和空格。


8. HTML5 本地存储方式有哪些?

特性CookielocalStoragesessionStorageIndexedDB
大小限制~4KB~5MB~5MB无硬性限制(通常数百MB)
有效期可设置过期时间永久(除非手动清除)当前会话(标签页关闭即失效)永久(除非手动清除)
服务端通信✅ 每次 HTTP 请求自动携带❌ 不参与❌ 不参与❌ 不参与
跨标签页✅ 同域共享✅ 同域共享❌ 仅当前标签页✅ 同域共享
API 类型字符串操作同步 API同步 API异步 API
数据类型字符串字符串字符串结构化数据(对象、二进制)
使用场景身份认证、会话管理用户偏好、缓存数据表单暂存、一次性数据大量结构化数据、离线应用

cookie 每次http请求会自动携带这是为什么? 这是 HTTP 协议的设计机制。 原理 浏览器在首次访问某个域名时,服务器通过 Set-Cookie 响应头将 cookie 写入浏览器。之后浏览器每次向该域名发起 HTTP 请求时,会自动在请求头中带上 Cookie 字段: GET /api/user HTTP/1.1 Host: example.com Cookie: sessionId=abc123; theme=dark 为什么这样设计 HTTP 是无状态协议 — 每次请求都是独立的,服务器不知道两次请求来自同一个用户 Cookie 弥补了这个缺陷 — 让服务器能"记住"客户端身份和偏好 自动携带 — 开发者不需要手动拼接 Cookie 头,浏览器帮你做了

// ======= Cookie =======
// 设置 Cookie(注意:需要设置安全属性)
document.cookie = 'username=张三; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/; Secure; SameSite=Strict'
document.cookie = 'theme=dark; path=/; Secure; SameSite=Strict'

// 读取所有 Cookie(返回的是一整个字符串,需自行解析)
console.log(document.cookie)  // "username=张三; theme=dark"

// ✅ 解析 Cookie 的几种方式:

// 方式一:手动 split 解析(最常见)
function getCookie(name) {
  const cookies = document.cookie.split('; ')  // 按 "; " 分割成数组
  for (const cookie of cookies) {
    const [key, ...rest] = cookie.split('=')   // 按第一个 "=" 分割
    if (key === name) return rest.join('=')     // value 中可能含 "=",需要拼回
  }
  return null
}
getCookie('username')  // '张三'
getCookie('theme')     // 'dark'

// 方式二:正则匹配(简洁,适合单个取值)
function getCookieByRegex(name) {
  const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
  return match ? decodeURIComponent(match[1]) : null
}

// 方式三:一次性解析为对象(适合需要多次读取的场景)
function parseCookies() {
  return document.cookie.split('; ').reduce((obj, pair) => {
    const [key, ...rest] = pair.split('=')
    obj[key] = decodeURIComponent(rest.join('='))
    return obj
  }, {})
}
const allCookies = parseCookies()  // { username: '张三', theme: 'dark' }

// 删除 Cookie(设置过期时间为过去)
document.cookie = 'username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'

// ======= localStorage =======
// 存储数据(只能存字符串,对象需要 JSON 序列化)
localStorage.setItem('user', JSON.stringify({ name: '张三', age: 25 }))
// 读取数据
const user = JSON.parse(localStorage.getItem('user'))
console.log(user.name)  // '张三'
// 删除单条数据
localStorage.removeItem('user')
// 清空所有数据
localStorage.clear()
// 监听其他标签页的 storage 变化(可用于跨标签页通信)
window.addEventListener('storage', (e) => {
  console.log(`键 ${e.key} 从 ${e.oldValue} 变为 ${e.newValue}`)
})

// ======= sessionStorage =======
// 用法与 localStorage 完全相同,区别在于关闭标签页后数据消失
sessionStorage.setItem('token', 'abc123')
sessionStorage.getItem('token')

// ======= IndexedDB(适合存储大量结构化数据)=======
const request = indexedDB.open('MyDatabase', 1)

request.onupgradeneeded = (e) => {
  const db = e.target.result
  // 创建对象仓库(类似于数据库的表)
  const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true })
  store.createIndex('name', 'name', { unique: false })
}

request.onsuccess = (e) => {
  const db = e.target.result
  // 写入数据
  const tx = db.transaction('users', 'readwrite')
  tx.objectStore('users').add({ name: '张三', age: 25 })
  // 读取数据
  const getTx = db.transaction('users', 'readonly')
  const getReq = getTx.objectStore('users').get(1)
  getReq.onsuccess = () => console.log(getReq.result)
}

面试加分点:

Cookie 安全属性详解:

属性作用防御目标
HttpOnly禁止 JavaScript 通过 document.cookie 读取XSS 攻击窃取 Cookie
Secure仅在 HTTPS 连接下才会发送该 Cookie中间人窃听
SameSite控制跨站请求时是否携带 CookieCSRF 攻击
Set-Cookie 完整语法(服务端设置):
Set-Cookie: name=value; Path=/; Domain=.example.com; Max-Age=86400; HttpOnly; Secure; SameSite=Strict
// ✅ 前端设置 Cookie(注意:HttpOnly 只能由服务端设置,前端无法设置)
document.cookie = 'theme=dark; path=/; Secure; SameSite=Strict; max-age=86400'

// ✅ 服务端设置(以 Node.js/Express 为例)
res.cookie('token', 'abc123', {
  httpOnly: true,     // ✅ 防 XSS:JS 无法读取,只能由浏览器自动携带
  secure: true,       // ✅ 仅 HTTPS 传输
  sameSite: 'Strict', // ✅ 防 CSRF:跨站请求不携带
  maxAge: 24 * 60 * 60 * 1000,  // 有效期 1 天(毫秒)
  path: '/',
  domain: '.example.com'  // 允许子域名共享
})

SameSite 三个取值对比:

值跨站请求携带 Cookie适用场景
Strict❌ 完全不携带银行、支付等高安全场景
Lax(默认)⚠️ 仅 GET 导航携带(链接跳转)普通网站(Chrome 80+ 默认值)
None✅ 始终携带(必须配合 Secure)第三方嵌入(iframe、跨域 API)
// 实际场景示例:
// 1. Strict —— 从外部链接点进来,Cookie 不会被携带(用户需重新登录)
// 2. Lax —— 从外部链接点进来 GET 请求会带 Cookie,但 POST 表单不带
// 3. None —— 第三方场景,如嵌入式支付页面需要读取登录态

// ⚠️ SameSite=None 必须同时设置 Secure,否则浏览器会拒绝
res.cookie('third_party_token', 'xyz', {
  sameSite: 'None',
  secure: true   // 必须!
})

localStorage 跨标签页通信:

// 标签页 A:写入数据
localStorage.setItem('message', JSON.stringify({ type: 'logout', time: Date.now() }))

// 标签页 B:监听变化(注意:只能监听 **其他标签页** 的修改,当前页面修改不会触发)
window.addEventListener('storage', (e) => {
  if (e.key === 'message') {
    const data = JSON.parse(e.newValue)
    if (data.type === 'logout') {
      // 同步登出状态
      window.location.href = '/login'
    }
  }
})

9. iframe 的优缺点?

<iframe> 用于在当前页面中嵌入另一个 HTML 页面,常见于嵌入第三方内容(地图、视频、支付页面等)。

优点:

  1. 内容隔离:iframe 内的 CSS 和 JS 与父页面互不影响,天然的沙箱环境
  2. 嵌入第三方内容:如 Google 地图、YouTube 视频、在线支付等
  3. 并行加载:iframe 中的资源可以与主页面并行下载
  4. 微前端方案:可以用于实现简单的微前端架构,不同团队独立开发和部署

缺点:

  1. 阻塞主页面 onload 事件:iframe 加载完毕前,主页面的 window.onload 不会触发
  2. 不利于 SEO:搜索引擎爬虫无法解析 iframe 中的内容
  3. 安全风险:可能被用于**点击劫持(Clickjacking)**攻击
  4. 导航混乱:iframe 内的导航不会反映在浏览器地址栏
  5. 性能开销:每个 iframe 都会创建独立的文档环境,消耗额外内存
  6. 响应式困难:iframe 内容难以自适应父容器尺寸
  7. 通信复杂:父子页面通信需要 postMessage,跨域场景更麻烦
<!-- 基本用法 -->
<iframe
  src="https://www.example.com"
  width="600"
  height="400"
  title="嵌入内容"
></iframe>

<!-- ✅ 安全最佳实践 -->
<iframe
  src="https://third-party.com/widget"
  title="第三方组件"
  sandbox="allow-scripts allow-same-origin allow-forms"
  loading="lazy"
  referrerpolicy="no-referrer"
  allow="camera; microphone"
></iframe>

<!-- sandbox 属性值说明 -->
<!--
  空值 sandbox=""         : 最严格,禁用所有功能
  allow-scripts          : 允许执行脚本
  allow-same-origin      : 允许同源访问
  allow-forms            : 允许提交表单
  allow-popups           : 允许弹出新窗口
  allow-top-navigation   : 允许控制顶层窗口导航
-->

<!-- 防止被其他网站用 iframe 嵌入(防止点击劫持) -->
<!-- 方法1:HTTP 响应头 -->
<!-- X-Frame-Options: DENY 或 SAMEORIGIN -->
<!-- 方法2:CSP 响应头 -->
<!-- Content-Security-Policy: frame-ancestors 'self' -->
// 父子页面通信(使用 postMessage)
// 父页面发送消息
const iframe = document.getElementById('myIframe')
iframe.contentWindow.postMessage({ type: 'greeting', data: 'Hello' }, 'https://child.example.com')

// 子页面(iframe 内)接收消息
window.addEventListener('message', (e) => {
  // ⚠️ 必须验证来源,防止接收恶意消息
  if (e.origin !== 'https://parent.example.com') return
  console.log('收到父页面消息:', e.data)
  // 回复消息
  e.source.postMessage({ type: 'reply', data: '收到!' }, e.origin)
})

面试加分点:解决 iframe 阻塞 onload 的方法——使用 JavaScript 动态设置 src 属性:iframe.src = 'url',或在 window.onload 之后再插入 iframe。现代项目中,更推荐使用 Web Components 或微前端框架(如 qiankun)替代 iframe。


10. HTML5 新增了哪些表单元素和属性?

HTML5 对表单进行了大量增强,新增了多种输入类型、属性和元素,减少了对 JavaScript 验证的依赖。

新增输入类型(<input type="...">):

类型说明示例值
email邮箱(自动验证格式)user@example.com
urlURL 地址https://example.com
tel电话号码(移动端弹出数字键盘)13800138000
number数字(带增减按钮)42
range范围滑块0~100
date / time / datetime-local日期/时间选择器2026-04-15
month / week月份/周选择器2026-04
color颜色选择器#ff0000
search搜索框(带清除按钮)关键词

新增表单属性:

属性说明
placeholder输入提示文本
required必填验证
autofocus页面加载后自动聚焦
autocomplete自动填充(on / off)
pattern正则表达式验证
min / max / step数值范围和步长
multiple允许多个值(用于 email 和 file)
form将外部控件关联到指定表单
novalidate(表单属性)提交时不进行验证

新增表单元素:

元素说明
<datalist>输入建议列表(自动补全)
<output>计算结果输出
<progress>进度条
<meter>度量衡(如磁盘用量)
<form id="myForm">
  <!-- 邮箱验证(自动验证格式) -->
  <label for="email">邮箱:</label>
  <input type="email" id="email" placeholder="请输入邮箱" required multiple />
  <!-- multiple 允许输入多个邮箱,用逗号分隔 -->

  <!-- 电话(移动端弹出数字键盘) -->
  <label for="phone">手机号:</label>
  <input type="tel" id="phone" pattern="^1[3-9]\d{9}$" title="请输入有效的手机号" required />

  <!-- 数字输入(带范围限制和步长) -->
  <label for="age">年龄:</label>
  <input type="number" id="age" min="0" max="150" step="1" value="25" />

  <!-- 日期选择器 -->
  <label for="birthday">生日:</label>
  <input type="date" id="birthday" min="1900-01-01" max="2026-12-31" />

  <!-- 范围滑块 -->
  <label for="volume">音量:<output id="volumeOutput">50</output></label>
  <input type="range" id="volume" min="0" max="100" value="50"
         oninput="volumeOutput.value = this.value" />

  <!-- 颜色选择器 -->
  <label for="theme">主题色:</label>
  <input type="color" id="theme" value="#007bff" />

  <!-- 搜索框(带清除按钮) -->
  <label for="search">搜索:</label>
  <input type="search" id="search" placeholder="搜索内容..." />

  <!-- datalist 输入建议(可搜索的下拉) -->
  <label for="browser">浏览器:</label>
  <input list="browsers" id="browser" placeholder="选择或输入浏览器" />
  <datalist id="browsers">
    <option value="Chrome" />
    <option value="Firefox" />
    <option value="Safari" />
    <option value="Edge" />
  </datalist>

  <!-- 进度条和度量衡 -->
  <label>上传进度:</label>
  <progress value="70" max="100">70%</progress>

  <label>磁盘使用:</label>
  <meter value="0.7" min="0" max="1" low="0.3" high="0.8" optimum="0.5">70%</meter>

  <button type="submit">提交</button>
</form>

<!-- form 属性:将外部控件关联到表单 -->
<input type="text" name="extra" form="myForm" placeholder="我在表单外,但属于 myForm" />
// HTML5 表单验证 API
const form = document.getElementById('myForm')
const emailInput = document.getElementById('email')

// 自定义验证消息
emailInput.addEventListener('invalid', (e) => {
  e.target.setCustomValidity('请输入正确的邮箱地址,例如:user@example.com')
})
emailInput.addEventListener('input', (e) => {
  e.target.setCustomValidity('')  // 清除自定义消息,恢复默认验证
})

// 手动触发验证
form.addEventListener('submit', (e) => {
  if (!form.checkValidity()) {
    e.preventDefault()
    // form.reportValidity()  // 显示验证错误提示
  }
})

// 验证单个字段
console.log(emailInput.validity.valid)        // 是否通过验证
console.log(emailInput.validity.typeMismatch) // 类型是否不匹配
console.log(emailInput.validity.valueMissing) // required 字段是否为空
console.log(emailInput.validity.patternMismatch) // pattern 是否不匹配

面试加分点:HTML5 原生验证可以减少 JavaScript 代码,但在实际项目中通常还需要配合 JS 做更复杂的验证(如异步校验用户名是否已注册)。novalidate 属性可以关闭浏览器原生验证,完全使用自定义验证逻辑。


11. 什么是 Web Workers?

JavaScript 是单线程的,当执行大量计算时会阻塞主线程,导致页面卡顿。Web Workers 允许在后台线程中运行 JavaScript,执行耗时任务而不阻塞 UI 渲染和用户交互。

Web Workers 的三种类型:

类型说明使用场景
Dedicated Worker只能被创建它的页面使用数据处理、复杂计算
Shared Worker可被多个页面/标签页共享多标签页共享 WebSocket 连接
Service Worker充当代理服务器,可拦截网络请求离线缓存(PWA)、推送通知

Web Workers 的限制:

  • ❌ 无法访问 DOM(document、window)
  • ❌ 无法使用 alert()、confirm() 等 UI 相关 API
  • ✅ 可以使用 XMLHttpRequest / fetch 发送网络请求
  • ✅ 可以使用 setTimeout / setInterval
  • ✅ 可以使用 importScripts() 引入其他脚本
  • ✅ 可以使用 IndexedDB
// =========== Dedicated Worker(专用 Worker)===========

// main.js —— 主线程
const worker = new Worker('worker.js')

// 发送数据给 Worker
worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] })

// 接收 Worker 返回的结果
worker.onmessage = (e) => {
  console.log('计算结果:', e.data.result)  // 55
  console.log('UI 没有卡顿!')
}

// 错误处理
worker.onerror = (err) => {
  console.error('Worker 发生错误:', err.message, '行号:', err.lineno)
}

// 不再需要时终止 Worker(释放资源)
// worker.terminate()

// worker.js —— Worker 线程
self.onmessage = (e) => {
  const { type, data } = e.data

  if (type === 'calculate') {
    // 模拟耗时计算(在后台线程执行,不阻塞主线程)
    let result = 0
    for (let i = 0; i < data.length; i++) {
      result += data[i]
    }
    // 将结果发送回主线程
    self.postMessage({ result })
  }
}

// Worker 内部也可以主动关闭自己
// self.close()


// =========== Service Worker(PWA 离线缓存)===========

// 注册 Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then((reg) => console.log('Service Worker 注册成功,作用域:', reg.scope))
    .catch((err) => console.error('Service Worker 注册失败:', err))
}

// sw.js —— Service Worker 脚本
const CACHE_NAME = 'my-app-v1'
const URLS_TO_CACHE = ['/', '/index.html', '/styles.css', '/app.js']

// 安装阶段:缓存核心资源
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(URLS_TO_CACHE))
  )
})

// 拦截网络请求:优先使用缓存
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request)
      .then((cached) => cached || fetch(e.request))
  )
})

面试加分点:Service Worker 是 PWA(渐进式 Web 应用)的核心技术,它可以拦截网络请求、管理缓存,实现离线访问和后台推送通知。Service Worker 有独立的生命周期(install → activate → fetch),且只能在 HTTPS 环境下使用(localhost 除外)。


12. HTML 中的无障碍(Accessibility)如何实现?

无障碍(a11y)是指让残障人士(视障、听障、肢体障碍等)也能正常使用网页。这不仅是道德要求,很多国家和地区也有法律法规强制要求(如美国 ADA 法案、欧盟 EAA)。

核心实现方式:

  1. 使用语义化标签:屏幕阅读器可以自动识别 <nav>、<main>、<button> 等标签
  2. ARIA 属性:当语义化标签不够用时,使用 ARIA(Accessible Rich Internet Applications)属性增强可访问性
  3. 键盘可访问:确保所有交互都可以通过键盘完成(Tab 键导航、Enter/Space 激活)
  4. 足够的颜色对比度:文字与背景的对比度至少 4.5:1(WCAG AA 标准)
  5. 为媒体提供替代文本:图片加 alt、视频加字幕

常用 ARIA 属性:

属性说明示例
role定义元素的角色role="button" role="dialog"
aria-label为元素提供可访问名称图标按钮的文字说明
aria-labelledby用其他元素的 ID 作为标签对话框标题
aria-describedby引用描述性文本输入框的提示信息
aria-hidden对屏幕阅读器隐藏元素装饰性图标
aria-expanded折叠/展开状态下拉菜单、手风琴
aria-live动态内容变化通知消息提示、实时更新
aria-required标记必填字段表单验证
aria-disabled标记禁用状态按钮不可点击
tabindex控制 Tab 键导航顺序自定义可聚焦元素
<!-- ✅ 1. 语义化按钮(首选:直接使用 <button>) -->
<button onclick="handleClick()">提交</button>

<!-- ❌ 不推荐:用 div 模拟按钮(缺乏语义和键盘支持) -->
<div onclick="handleClick()">提交</div>

<!-- ⚠️ 必须用 div 时,添加 ARIA 和键盘支持 -->
<div
  role="button"
  tabindex="0"
  aria-label="提交表单"
  onclick="handleClick()"
  onkeydown="if(event.key === 'Enter' || event.key === ' ') handleClick()"
>
  提交
</div>

<!-- ✅ 2. 表单无障碍 -->
<form>
  <label for="username">用户名 <span aria-hidden="true">*</span></label>
  <input
    id="username"
    type="text"
    aria-required="true"
    aria-describedby="username-hint username-error"
    aria-invalid="false"
  />
  <span id="username-hint">请输入 3-20 个字符</span>
  <span id="username-error" role="alert" hidden>用户名不能为空</span>
</form>

<!-- ✅ 3. 图片无障碍 -->
<img src="logo.png" alt="腾讯公司 Logo" />                      <!-- 有含义的图片 -->
<img src="decoration.png" alt="" role="presentation" />          <!-- 装饰性图片 -->
<img src="chart.png" alt="2026年Q1营收同比增长15%的柱状图" />      <!-- 信息性图片 -->

<!-- ✅ 4. 跳过导航链接(视障用户可快速到达主内容区) -->
<a href="#main-content" class="skip-link">跳到主要内容</a>
<nav aria-label="主导航">
  <ul>
    <li><a href="/">首页</a></li>
    <li><a href="/about">关于</a></li>
  </ul>
</nav>
<main id="main-content">...</main>

<!-- ✅ 5. 实时区域(动态内容变化时通知屏幕阅读器) -->
<div aria-live="polite" aria-atomic="true">
  <!-- polite: 等用户空闲时再朗读 -->
  <!-- assertive: 立即打断当前朗读 -->
  <p>购物车已更新,当前有 3 件商品</p>
</div>

<!-- ✅ 6. 对话框/模态框 -->
<div
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
  aria-modal="true"
>
  <h2 id="dialog-title">确认删除</h2>
  <p id="dialog-desc">删除后不可恢复,确认继续吗?</p>
  <button>确认</button>
  <button>取消</button>
</div>

<style>
  /* 跳过导航链接样式:平时隐藏,聚焦时显示 */
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    z-index: 1000;
    padding: 8px 16px;
    background: #000;
    color: #fff;
  }
  .skip-link:focus {
    top: 0;
  }
</style>

面试加分点:ARIA 的第一原则是"如果能用原生 HTML 元素实现,就不要用 ARIA"。比如 <button> 天然支持键盘操作和屏幕阅读器,无需额外的 role="button"。只有在原生元素无法满足需求时(如自定义组件)才使用 ARIA。


13. Canvas 和 SVG 的区别?

两者都用于在网页中绘制图形,但底层原理完全不同:

特性CanvasSVG
类型位图(像素级绘制)矢量图(基于数学描述)
标准HTML5 新增W3C 标准(XML 格式)
缩放放大后失真(像素化)无限缩放不失真
DOM 节点只有一个 <canvas> 元素每个图形都是独立的 DOM 节点
事件绑定需要手动计算坐标命中区域可以直接绑定事件(onclick)
CSS 样式不支持 CSS 操纵图形支持 CSS 修改样式
动画需要手动逐帧重绘支持 CSS 动画和 SMIL
性能大量元素时性能更好(不增加 DOM)元素过多时 DOM 操作开销大
文字渲染较差(位图化)优秀(保持可选中、可搜索)
无障碍差(需额外处理)好(支持 <title> <desc>)
适用场景游戏、图像处理、像素操作、大数据可视化图标、Logo、图表、地图、交互式图形
// =========== Canvas 示例 ===========
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')

// 绘制填充矩形
ctx.fillStyle = '#007bff'
ctx.fillRect(10, 10, 120, 60)

// 绘制描边矩形
ctx.strokeStyle = '#28a745'
ctx.lineWidth = 3
ctx.strokeRect(150, 10, 120, 60)

// 绘制圆形
ctx.beginPath()
ctx.arc(80, 130, 40, 0, Math.PI * 2)
ctx.fillStyle = '#ffc107'
ctx.fill()

// 绘制文字
ctx.font = '18px Arial'
ctx.fillStyle = '#333'
ctx.fillText('Hello Canvas', 140, 135)

// 绘制渐变
const gradient = ctx.createLinearGradient(0, 180, 280, 180)
gradient.addColorStop(0, '#667eea')
gradient.addColorStop(1, '#764ba2')
ctx.fillStyle = gradient
ctx.fillRect(10, 180, 270, 40)

// Canvas 逐帧动画
let x = 0
function animate() {
  ctx.clearRect(0, 230, canvas.width, 50)
  ctx.fillStyle = '#e74c3c'
  ctx.beginPath()
  ctx.arc(x, 255, 15, 0, Math.PI * 2)
  ctx.fill()
  x = (x + 2) % canvas.width
  requestAnimationFrame(animate)
}
animate()
<!-- =========== SVG 示例 =========== -->
<svg width="300" height="250" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="SVG 图形示例">
  <title>SVG 图形示例</title>
  <desc>包含矩形、圆形和文字的 SVG 示例</desc>

  <!-- 矩形 -->
  <rect x="10" y="10" width="120" height="60" fill="#007bff" rx="8" ry="8" />

  <!-- 圆形(支持 CSS hover 效果) -->
  <circle cx="230" cy="40" r="35" fill="#28a745" class="interactive-circle" />

  <!-- 文字(可选中、可搜索) -->
  <text x="80" y="120" font-size="18" fill="#333" text-anchor="middle">Hello SVG</text>

  <!-- 路径绘制(贝塞尔曲线) -->
  <path d="M10,150 Q150,100 290,150" stroke="#e74c3c" stroke-width="3" fill="none" />

  <!-- 渐变 -->
  <defs>
    <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" style="stop-color:#667eea" />
      <stop offset="100%" style="stop-color:#764ba2" />
    </linearGradient>
  </defs>
  <rect x="10" y="170" width="270" height="40" fill="url(#grad)" rx="6" />

  <!-- SVG 动画(SMIL) -->
  <circle cx="20" cy="230" r="12" fill="#e74c3c">
    <animate attributeName="cx" from="20" to="280" dur="3s" repeatCount="indefinite" />
  </circle>
</svg>

<style>
  /* SVG 支持 CSS 样式和动画 */
  .interactive-circle {
    cursor: pointer;
    transition: fill 0.3s, r 0.3s;
  }
  .interactive-circle:hover {
    fill: #20c997;
    r: 40;
  }
</style>

面试加分点:在实际项目中,图标推荐使用 SVG(体积小、不失真、可变色);游戏和图像处理推荐 Canvas;大数据量图表(如 10 万+数据点的散点图)推荐 Canvas,少量交互图表推荐 SVG。主流图表库如 ECharts 支持 Canvas 和 SVG 双引擎切换。


14. 什么是渐进式增强和优雅降级?

两者都是处理浏览器兼容性的策略,但出发点不同:

对比项渐进式增强(Progressive Enhancement)优雅降级(Graceful Degradation)
出发点从基础功能出发,逐步添加增强从完整功能出发,逐步做兼容
开发顺序先低端 → 后高端先高端 → 后低端
核心理念保证所有用户都能用保证最佳体验优先
适用场景面向大众的网站(电商、政务)面向特定用户的应用(管理后台)
<!-- ✅ 渐进式增强示例 -->

<!-- 示例1:视频播放 -->
<!-- 基础层:不支持 video 的浏览器显示下载链接 -->
<video controls width="640" height="360">
  <source src="video.mp4" type="video/mp4" />
  <source src="video.webm" type="video/webm" />
  <!-- 最终降级方案 -->
  <p>您的浏览器不支持视频播放,<a href="video.mp4">点此下载视频</a></p>
</video>

<!-- 示例2:图片格式渐进式增强 -->
<picture>
  <source srcset="image.avif" type="image/avif" />  <!-- 最新格式,最小体积 -->
  <source srcset="image.webp" type="image/webp" />  <!-- 次新格式 -->
  <img src="image.jpg" alt="渐进式图片" />            <!-- 兜底方案 -->
</picture>
/* ✅ CSS 渐进式增强 */

/* 基础样式:所有浏览器都支持 */
.card {
  background: #f0f0f0;
  padding: 20px;
  border-radius: 8px;
}

/* 增强层1:支持渐变的浏览器 */
.card {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

/* 增强层2:@supports 特性检测 */
@supports (display: grid) {
  .layout {
    display: grid;
    grid-template-columns: 1fr 2fr 1fr;
    gap: 20px;
  }
}

@supports (backdrop-filter: blur(10px)) {
  .glass-card {
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
  }
}

/* ✅ 优雅降级示例 */

/* 先写现代浏览器的样式 */
.button {
  background: linear-gradient(135deg, #667eea, #764ba2);
  border: none;
  border-radius: 8px;
  color: white;
  cursor: pointer;
}

/* 再为旧浏览器做降级(不支持渐变时使用纯色) */
.button {
  background: #667eea;  /* 旧浏览器使用的降级样式 */
  background: linear-gradient(135deg, #667eea, #764ba2);  /* 覆盖,支持的浏览器用这个 */
}
// ✅ JavaScript 特性检测(渐进式增强的核心方法)

// 检测 API 是否存在后再使用
if ('geolocation' in navigator) {
  navigator.geolocation.getCurrentPosition((pos) => {
    console.log(`经度: ${pos.coords.longitude}, 纬度: ${pos.coords.latitude}`)
  })
} else {
  console.log('您的浏览器不支持地理定位,请手动输入位置')
}

// 检测 IntersectionObserver
if ('IntersectionObserver' in window) {
  // 使用 IntersectionObserver 实现懒加载
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target
        img.src = img.dataset.src
        observer.unobserve(img)
      }
    })
  })
  document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img))
} else {
  // 降级方案:直接加载所有图片
  document.querySelectorAll('img[data-src]').forEach((img) => {
    img.src = img.dataset.src
  })
}

面试加分点:现代前端开发更推荐渐进式增强策略,因为它确保了核心功能对所有用户可用。配合工具链(如 Babel 转译 JS、PostCSS 添加 CSS 前缀、Polyfill.io 按需加载 Polyfill)可以高效地实现。


15. HTML 中如何优化图片加载?

图片通常是网页中体积最大的资源,优化图片加载对页面性能至关重要。

核心优化策略:

策略技术方案效果
减少体积WebP/AVIF 格式、压缩减少 30%~80% 体积
延迟加载loading="lazy"只加载视口内的图片
按需加载响应式图片 srcset根据设备分辨率加载合适尺寸
提前加载<link rel="preload">关键图片优先加载
渐进显示渐进式 JPEG、LQIP先显示模糊图,再加载高清
减少请求CSS Sprites、内联 Base64合并小图标
<!-- 1. 懒加载(原生支持,最简单有效) -->
<!-- 视口外的图片不会立即加载,滚动到可见时才加载 -->
<img src="image.jpg" loading="lazy" alt="懒加载图片" />
<!-- 注意:首屏关键图片不要加 loading="lazy",否则会延迟加载 -->
<img src="hero.jpg" alt="首屏图片" fetchpriority="high" />

<!-- 2. 响应式图片:根据屏幕宽度加载不同尺寸 -->
<img
  src="image-800.jpg"
  srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
  alt="自适应分辨率图片"
/>
<!--
  srcset:告诉浏览器可选的图片及其宽度
  sizes:告诉浏览器在不同视口宽度下显示的图片尺寸
  浏览器会根据设备 DPR(设备像素比)和视口宽度自动选择最合适的图片
-->

<!-- 3. <picture> 元素:艺术指导 + 格式降级 -->
<picture>
  <!-- 现代格式优先(AVIF > WebP > JPEG) -->
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <!-- 针对不同屏幕尺寸裁切不同的图片(艺术指导) -->
  <source media="(max-width: 768px)" srcset="image-mobile.jpg" />
  <source media="(max-width: 1200px)" srcset="image-tablet.jpg" />
  <img src="image-desktop.jpg" alt="响应式图片" />
</picture>

<!-- 4. 预加载关键图片(LCP 优化) -->
<link rel="preload" as="image" href="hero-banner.jpg" fetchpriority="high" />
<!-- 预加载响应式图片 -->
<link rel="preload" as="image" href="hero.webp" type="image/webp"
      imagesrcset="hero-400.webp 400w, hero-800.webp 800w"
      imagesizes="100vw" />

<!-- 5. 指定图片尺寸(防止布局偏移 CLS) -->
<!-- 始终设置 width 和 height,浏览器可以提前预留空间 -->
<img src="photo.jpg" width="800" height="600" alt="固定宽高比" style="max-width: 100%; height: auto;" />
<!-- 或使用 CSS aspect-ratio -->
<img src="photo.jpg" alt="固定宽高比" style="aspect-ratio: 4/3; width: 100%;" />

<!-- 6. 解码优化 -->
<!-- decoding="async" 允许浏览器异步解码图片,不阻塞主线程 -->
<img src="large-photo.jpg" decoding="async" alt="异步解码" />

<!-- 7. 小图标用 SVG 或 CSS Sprites -->
<!-- SVG 图标(体积小、不失真、可变色) -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
  <path d="M12 2L3 7v10l9 5 9-5V7l-9-5z" />
</svg>

面试加分点:fetchpriority 属性是较新的优化手段,可以告诉浏览器资源加载的优先级(high / low / auto)。首屏大图设置 fetchpriority="high" 可以提升 LCP(Largest Contentful Paint)指标。CLS(Cumulative Layout Shift)可以通过为图片设置 width 和 height 或 aspect-ratio 来优化。


16. HTML5 有哪些新特性?(综合概述)

HTML5 是 HTML 标准的第五次重大修订,于 2014 年正式发布。相比 HTML4,它不仅新增了大量 API 和标签,更重要的是推动了 Web 从"文档平台"向"应用平台"的转变。

HTML5 新特性一览:

分类新增内容
语义化标签<header> <footer> <nav> <main> <article> <section> <aside> <figure> <details> 等
多媒体<audio> <video> 原生音视频支持,无需 Flash
图形绘制<canvas> 2D/WebGL 绘图、<svg> 内联矢量图
表单增强新输入类型(email date range color 等)、原生表单验证
本地存储localStorage sessionStorage IndexedDB
离线与缓存Service Worker、Cache API(替代已废弃的 Application Cache)
网络通信WebSocket(全双工通信)、Server-Sent Events(服务器推送)
多线程Web Workers(后台线程计算)
地理定位Geolocation API(获取用户位置)
拖放 APIDrag and Drop API(原生拖拽)
历史管理History API(pushState replaceState,SPA 路由基础)
文件操作File API(读取本地文件)、FileReader
设备访问deviceorientation(陀螺仪)、devicemotion(加速计)
性能监控Performance API、requestAnimationFrame
内容编辑contenteditable 属性、document.execCommand()(已废弃)

HTML5 移除的元素:

移除的标签替代方案
<font>CSS font-family、color
<center>CSS text-align: center
<big>CSS font-size
<strike> / <s>CSS text-decoration: line-through
<frame> / <frameset><iframe> 或 SPA
<applet><object> 或 JavaScript
<!-- HTML5 多媒体示例 -->
<audio controls preload="metadata">
  <source src="music.mp3" type="audio/mpeg" />
  <source src="music.ogg" type="audio/ogg" />
  <p>您的浏览器不支持音频播放</p>
</audio>

<video controls width="640" height="360" poster="cover.jpg" preload="metadata">
  <source src="video.mp4" type="video/mp4" />
  <source src="video.webm" type="video/webm" />
  <track kind="subtitles" src="subs_cn.vtt" srclang="zh" label="中文字幕" default />
  <track kind="subtitles" src="subs_en.vtt" srclang="en" label="English" />
  <p>您的浏览器不支持视频播放</p>
</video>

面试加分点:HTML5 不仅是标签层面的更新,更是整个 Web 生态的升级。很多面试官问"HTML5 新特性"其实是在考察你对现代 Web 技术栈的全面了解。回答时可以按"标签/API/存储/通信/多媒体"等维度分类阐述,显示出知识的系统性。


17. 什么是 HTML5 的 History API?

History API 允许 JavaScript 操作浏览器的历史记录和 URL,无需刷新页面。它是现代 SPA(单页应用)路由的核心基础(如 Vue Router、React Router 的 history 模式)。

核心方法:

方法/属性说明
history.pushState(state, title, url)添加新的历史记录条目,URL 变化但不刷新页面
history.replaceState(state, title, url)替换当前历史记录条目,URL 变化但不刷新页面
history.back()后退一页(等同于点击浏览器后退按钮)
history.forward()前进一页
history.go(n)前进或后退 n 页(go(-2) 后退两页)
history.length历史记录的数量
history.state当前历史条目的状态对象
window.onpopstate用户点击前进/后退按钮时触发
// =========== 基本用法 ===========

// 添加新的历史记录(URL 变为 /page2,但页面不刷新)
history.pushState(
  { page: 2, title: '第二页' },  // state:可以存任意可序列化数据
  '',                              // title:大多数浏览器忽略此参数
  '/page2'                         // url:新的 URL(必须同源)
)

// 替换当前历史记录(不增加新条目)
history.replaceState({ page: 2, updated: true }, '', '/page2?v=2')

// 监听前进/后退事件
window.addEventListener('popstate', (e) => {
  console.log('用户点击了前进/后退')
  console.log('目标状态:', e.state)  // pushState 时传入的 state 对象
  // 根据 state 渲染对应的页面内容
  if (e.state) {
    renderPage(e.state.page)
  }
})


// =========== 简单的 SPA 路由实现 ===========

class SimpleRouter {
  constructor() {
    this.routes = {}
    window.addEventListener('popstate', (e) => this.handleRoute())
  }

  // 注册路由
  register(path, handler) {
    this.routes[path] = handler
  }

  // 导航到指定路径
  navigate(path) {
    history.pushState({ path }, '', path)
    this.handleRoute()
  }

  // 处理路由变化
  handleRoute() {
    const path = window.location.pathname
    const handler = this.routes[path]
    if (handler) {
      handler()
    } else {
      this.routes['/404']?.()
    }
  }
}

// 使用示例
const router = new SimpleRouter()
router.register('/', () => { document.getElementById('app').innerHTML = '<h1>首页</h1>' })
router.register('/about', () => { document.getElementById('app').innerHTML = '<h1>关于</h1>' })
router.register('/404', () => { document.getElementById('app').innerHTML = '<h1>404 页面未找到</h1>' })

// 拦截链接点击
document.addEventListener('click', (e) => {
  if (e.target.tagName === 'A' && e.target.getAttribute('href')?.startsWith('/')) {
    e.preventDefault()
    router.navigate(e.target.getAttribute('href'))
  }
})

面试加分点:pushState 和 replaceState 的区别——pushState 会在历史记录中新增一条,用户可以点击后退回到之前的 URL;replaceState 只修改当前条目,不增加历史记录。注意:popstate 事件只在用户点击前进/后退按钮时触发,调用 pushState / replaceState 不会触发。


18. 什么是 WebSocket?与 HTTP 有什么区别?

WebSocket 是 HTML5 提供的一种全双工通信协议,建立在单个 TCP 连接上,允许服务端主动向客户端推送数据。

HTTP vs WebSocket 对比:

对比项HTTPWebSocket
通信方式请求-响应(半双工)全双工(双向实时通信)
连接方式短连接(每次请求新建连接)或 Keep-Alive长连接(一次握手,持续通信)
服务端推送❌ 不支持(需轮询或 SSE)✅ 原生支持
协议头开销较大(每次请求都带完整 Header)极小(握手后数据帧只有 2~10 字节头)
协议标识http:// / https://ws:// / wss://(加密)
适用场景普通页面请求、RESTful API实时聊天、在线游戏、股票行情、协同编辑

WebSocket 的连接过程:

  1. 客户端发送 HTTP 请求,Upgrade: websocket 头请求协议升级
  2. 服务端返回 101 Switching Protocols,完成握手
  3. 双方通过同一个 TCP 连接双向通信
  4. 任一方发送关闭帧即可断开
// =========== 客户端 WebSocket ===========

const ws = new WebSocket('wss://example.com/chat')

// 连接建立
ws.addEventListener('open', () => {
  console.log('WebSocket 连接已建立')
  ws.send(JSON.stringify({ type: 'join', room: 'general', user: '张三' }))
})

// 接收服务端消息
ws.addEventListener('message', (e) => {
  const data = JSON.parse(e.data)
  console.log('收到消息:', data)

  switch (data.type) {
    case 'chat':
      displayMessage(data.user, data.content)
      break
    case 'online':
      updateOnlineUsers(data.users)
      break
  }
})

// 连接关闭
ws.addEventListener('close', (e) => {
  console.log(`连接已关闭,Code: ${e.code}, Reason: ${e.reason}`)
  // 自动重连逻辑
  if (e.code !== 1000) {
    setTimeout(() => reconnect(), 3000)
  }
})

// 错误处理
ws.addEventListener('error', (err) => {
  console.error('WebSocket 错误:', err)
})

// 发送消息
function sendMessage(content) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'chat', content }))
  }
}

// 关闭连接
function disconnect() {
  ws.close(1000, '用户主动断开')  // 1000 表示正常关闭
}

// readyState 状态值
// 0: CONNECTING(正在连接)
// 1: OPEN(已连接)
// 2: CLOSING(正在关闭)
// 3: CLOSED(已关闭)

实时通信方案对比:

方案原理实时性开销适用场景
短轮询定时发送 HTTP 请求低高简单场景
长轮询请求挂起,有数据再返回中中兼容性要求高
SSE(Server-Sent Events)服务端单向推送高低通知、新闻推送
WebSocket双向实时通信最高最低聊天、游戏、协同

面试加分点:WebSocket 需要心跳机制保持连接活跃,还需要实现断线自动重连逻辑。生产环境推荐使用 Socket.IO 等库,它在 WebSocket 不可用时自动降级为长轮询,并内置了重连、房间、命名空间等功能。

心跳保活机制(Heartbeat / Ping-Pong):

为什么需要心跳?

  • NAT 超时:中间路由器/防火墙会清除长时间无数据传输的 TCP 连接映射(通常 60s~5min)
  • 代理/负载均衡超时:Nginx 默认 60s 无活动就断开 WebSocket(proxy_read_timeout)
  • 检测"假死"连接:客户端断网/崩溃时,服务端无法立刻感知(TCP 没有发送 FIN)
  • 及时触发重连:通过心跳超时判断连接失效,比等待 TCP 超时快得多
// ============ 完整的 WebSocket 心跳保活 + 断线重连方案 ============

class WebSocketClient {
  constructor(url, options = {}) {
    this.url = url
    this.options = {
      heartbeatInterval: 30000,  // 心跳发送间隔(30s)
      heartbeatTimeout: 10000,   // 等待 pong 响应的超时时间(10s)
      reconnectInterval: 3000,   // 重连间隔基础时间
      reconnectMaxInterval: 30000, // 最大重连间隔
      reconnectMaxAttempts: 10,  // 最大重连次数
      ...options
    }

    this.ws = null
    this.heartbeatTimer = null   // 心跳发送定时器
    this.heartbeatTimeout = null // 心跳超时定时器
    this.reconnectAttempts = 0   // 当前重连次数
    this.reconnectTimer = null   // 重连定时器
    this.isManualClose = false   // 是否手动关闭(区分主动断开和异常断开)
    this.messageHandlers = new Map()

    this.connect()
  }

  // ===== 建立连接 =====
  connect() {
    this.ws = new WebSocket(this.url)

    this.ws.addEventListener('open', () => {
      console.log('[WS] 连接建立')
      this.reconnectAttempts = 0    // 重置重连计数
      this.startHeartbeat()          // 开始心跳
    })

    this.ws.addEventListener('message', (e) => {
      const data = JSON.parse(e.data)

      // ✅ 收到 pong 响应 → 连接正常,重置超时计时器
      if (data.type === 'pong') {
        this.resetHeartbeatTimeout()
        return
      }

      // 处理业务消息
      this.handleMessage(data)
    })

    this.ws.addEventListener('close', (e) => {
      console.log(`[WS] 连接关闭 Code:${e.code} Reason:${e.reason}`)
      this.stopHeartbeat()

      // 非手动关闭 → 触发重连
      if (!this.isManualClose) {
        this.reconnect()
      }
    })

    this.ws.addEventListener('error', (err) => {
      console.error('[WS] 连接错误:', err)
      // error 事件后通常会紧跟 close 事件,重连逻辑在 close 中处理
    })
  }

  // ===== 心跳机制 =====
  startHeartbeat() {
    this.stopHeartbeat()  // 先清除旧定时器

    // 每隔 N 秒发送一次 ping
    this.heartbeatTimer = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        // 发送 ping 帧(应用层心跳)
        this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }))

        // 设置超时:如果 N 秒内没收到 pong,认为连接已死
        this.heartbeatTimeout = setTimeout(() => {
          console.warn('[WS] 心跳超时,主动断开以触发重连')
          this.ws.close(4000, 'Heartbeat timeout')  // 自定义关闭码
        }, this.options.heartbeatTimeout)
      }
    }, this.options.heartbeatInterval)
  }

  // 收到 pong → 清除超时计时器(说明连接正常)
  resetHeartbeatTimeout() {
    if (this.heartbeatTimeout) {
      clearTimeout(this.heartbeatTimeout)
      this.heartbeatTimeout = null
    }
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
    this.resetHeartbeatTimeout()
  }

  // ===== 断线重连(指数退避) =====
  reconnect() {
    if (this.reconnectAttempts >= this.options.reconnectMaxAttempts) {
      console.error('[WS] 达到最大重连次数,停止重连')
      return
    }

    this.reconnectAttempts++

    // 指数退避:3s → 6s → 12s → 24s → 30s(上限)
    const delay = Math.min(
      this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
      this.options.reconnectMaxInterval
    )

    console.log(`[WS] 第 ${this.reconnectAttempts} 次重连,${delay / 1000}s 后执行...`)

    this.reconnectTimer = setTimeout(() => {
      this.connect()
    }, delay)
  }

  // ===== 发送消息 =====
  send(data) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
    } else {
      console.warn('[WS] 连接未就绪,消息发送失败')
    }
  }

  // ===== 主动关闭 =====
  close() {
    this.isManualClose = true
    this.stopHeartbeat()
    clearTimeout(this.reconnectTimer)
    this.ws?.close(1000, '用户主动断开')
  }

  // ===== 业务消息处理 =====
  handleMessage(data) {
    const handler = this.messageHandlers.get(data.type)
    if (handler) handler(data)
  }

  on(type, handler) {
    this.messageHandlers.set(type, handler)
  }
}

// ============ 使用示例 ============
const ws = new WebSocketClient('wss://example.com/chat', {
  heartbeatInterval: 25000,  // 25s 发一次心跳(小于 Nginx 默认 60s 超时)
  heartbeatTimeout: 8000,
  reconnectMaxAttempts: 5
})

ws.on('chat', (data) => {
  console.log(`${data.user}: ${data.content}`)
})

ws.on('online', (data) => {
  console.log('在线用户:', data.users)
})

ws.send({ type: 'chat', content: 'Hello!' })
// ============ 服务端心跳处理(Node.js + ws 库示例) ============
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', (ws) => {
  ws.isAlive = true

  ws.on('message', (message) => {
    const data = JSON.parse(message)

    // 收到客户端 ping → 回复 pong
    if (data.type === 'ping') {
      ws.isAlive = true
      ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }))
      return
    }

    // 处理其他业务消息...
  })

  ws.on('close', () => {
    console.log('客户端断开')
  })
})

// 服务端也可以主动检测:每 30s 检查一次连接是否存活
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) {
      console.log('检测到死连接,主动终止')
      return ws.terminate()  // 强制关闭(不发关闭帧)
    }
    ws.isAlive = false  // 标记为待验证,等待下次 ping 恢复
  })
}, 30000)

wss.on('close', () => clearInterval(interval))

心跳方案选择:

方案原理优点缺点
WebSocket 协议层 Ping/Pong浏览器自动处理 opcode 0x9/0xA无需应用层代码前端无法控制时机,部分代理不转发
应用层心跳(推荐)自定义 {type:'ping'} 消息完全可控,可携带业务数据需要自己实现
TCP Keep-Alive操作系统层面的探活无需代码间隔太长(默认 2h),无法快速检测

💡 生产环境最佳实践:

  1. 心跳间隔建议 20~30s(小于大多数代理/防火墙超时时间 60s)
  2. 使用指数退避重连,避免服务器重启时大量客户端同时重连("惊群效应")
  3. 重连时加入随机抖动(jitter):delay * (0.5 + Math.random() * 0.5)
  4. 页面不可见时(visibilitychange)可以降低心跳频率以省电

19. 什么是 Shadow DOM?

Shadow DOM 是 Web Components 标准的核心技术之一,它允许将一个封装的、隐藏的 DOM 子树附加到元素上。Shadow DOM 内部的样式和结构与外部文档完全隔离,互不影响。

为什么需要 Shadow DOM?

在大型项目中,CSS 的全局性会导致样式污染和冲突。Shadow DOM 提供了原生的样式隔离机制,类似于 Vue 的 scoped 样式或 CSS Modules,但是在浏览器层面实现的。

核心概念:

概念说明
Shadow Host持有 Shadow DOM 的普通 DOM 元素
Shadow Tree隐藏的 DOM 子树
Shadow RootShadow Tree 的根节点
Shadow BoundaryShadow DOM 与普通 DOM 的边界(样式隔离边界)
// =========== 创建 Shadow DOM ===========

class MyCard extends HTMLElement {
  constructor() {
    super()
    // 创建 Shadow Root(mode: 'open' 允许外部通过 JS 访问)
    const shadow = this.attachShadow({ mode: 'open' })

    // Shadow DOM 内部的结构和样式
    shadow.innerHTML = `
      <style>
        /* 这些样式只在 Shadow DOM 内部生效,不会污染外部 */
        :host {
          display: block;
          border: 1px solid #e0e0e0;
          border-radius: 8px;
          overflow: hidden;
          font-family: sans-serif;
        }
        :host(:hover) {
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        }
        .header {
          background: #007bff;
          color: white;
          padding: 12px 16px;
          font-size: 16px;
        }
        .body {
          padding: 16px;
          color: #333;
        }
        /* 外部的 .header 样式不会影响这里 */
        /* 这里的 .header 样式也不会泄漏到外部 */
      </style>
      <div class="header">
        <slot name="title">默认标题</slot>
      </div>
      <div class="body">
        <slot>默认内容</slot>
      </div>
    `
  }
}

// 注册自定义元素
customElements.define('my-card', MyCard)
<!-- 使用自定义组件 -->
<my-card>
  <span slot="title">这是卡片标题</span>
  <p>这是卡片内容,Shadow DOM 让样式完全隔离!</p>
</my-card>

<!-- 外部样式不会影响 Shadow DOM 内部 -->
<style>
  .header { background: red; }  /* 不会影响 my-card 内部的 .header */
  p { color: green; }           /* 不会影响 my-card 内部的 p 标签 */
</style>
// mode: 'open' vs 'closed'
const openShadow = element.attachShadow({ mode: 'open' })
// 外部可以通过 element.shadowRoot 访问 Shadow DOM
console.log(element.shadowRoot)  // ShadowRoot 对象

const closedShadow = element.attachShadow({ mode: 'closed' })
// 外部无法访问
console.log(element.shadowRoot)  // null

面试加分点:浏览器原生的 <video>、<input type="range">、<details> 等元素内部就是使用 Shadow DOM 实现的。Chrome DevTools 中勾选"Show user agent shadow DOM"可以查看这些元素的内部结构。Web Components = Custom Elements + Shadow DOM + HTML Templates。


20. 什么是 data-* 自定义数据属性?

HTML5 允许在标签上使用 data- 前缀定义自定义属性,用于存储页面或应用的私有数据。这些数据不会被浏览器渲染或影响布局,但可以通过 JavaScript 和 CSS 读取和使用。

命名规则:

  • 属性名必须以 data- 开头
  • 不能包含大写字母(会自动转为小写)
  • JavaScript 中通过 dataset 访问时,连字符会转为驼峰命名
<!-- 在 HTML 中定义 data 属性 -->
<div
  id="user"
  data-id="12345"
  data-user-name="张三"
  data-role="admin"
  data-is-active="true"
  data-config='{"theme":"dark","lang":"zh"}'
>
  用户信息
</div>

<ul class="product-list">
  <li data-product-id="p001" data-price="99.9" data-category="电子">商品A</li>
  <li data-product-id="p002" data-price="199" data-category="服装">商品B</li>
  <li data-product-id="p003" data-price="59.9" data-category="食品">商品C</li>
</ul>
const userEl = document.getElementById('user')

// =========== 读取 data 属性 ===========

// 方式1:dataset API(推荐,自动驼峰转换)
console.log(userEl.dataset.id)          // '12345'
console.log(userEl.dataset.userName)    // '张三'   (data-user-name → userName)
console.log(userEl.dataset.role)        // 'admin'
console.log(userEl.dataset.isActive)    // 'true'  (注意:始终是字符串)

// 解析 JSON
const config = JSON.parse(userEl.dataset.config)
console.log(config.theme)  // 'dark'

// 方式2:getAttribute(通用方法)
console.log(userEl.getAttribute('data-user-name'))  // '张三'

// =========== 修改 data 属性 ===========

userEl.dataset.role = 'editor'  // 修改
userEl.dataset.lastLogin = '2026-04-15'  // 新增 data-last-login 属性
delete userEl.dataset.isActive  // 删除

// =========== 实际应用:事件委托 ===========
document.querySelector('.product-list').addEventListener('click', (e) => {
  const li = e.target.closest('li')
  if (li) {
    const { productId, price, category } = li.dataset
    console.log(`点击了商品 ${productId},价格 ${price},分类 ${category}`)
    addToCart(productId)
  }
})
/* CSS 中也可以读取 data 属性 */

/* 1. 属性选择器 */
[data-role="admin"] {
  border-left: 3px solid red;
}

[data-category="电子"] {
  background: #e3f2fd;
}

/* 2. 用 attr() 函数在 content 中显示 */
[data-price]::after {
  content: '  ¥' attr(data-price);
  color: #e74c3c;
  font-weight: bold;
}

/* 3. 根据 data 属性状态切换样式 */
[data-is-active="true"] {
  opacity: 1;
}
[data-is-active="false"] {
  opacity: 0.5;
}

面试加分点:data-* 属性的值始终是字符串类型,使用时需注意类型转换。在 React/Vue 等框架中,虽然有更好的状态管理方案,但 data-* 属性在 E2E 测试(如 data-testid)和无框架的场景中仍然非常实用。性能方面,大量的 data-* 属性比使用全局 Map 存储数据会略慢。


21. <link> 标签的 rel 属性有哪些常见值?

<link> 标签用于定义当前文档与外部资源的关系,rel 属性(relationship)指定了这种关系的类型。

rel 值说明使用场景
stylesheet引入外部样式表CSS 文件
icon / shortcut icon网站图标favicon
preload预加载关键资源字体、首屏图片、关键 JS/CSS
prefetch预获取下一页可能用到的资源下一页的资源
preconnect预连接(DNS + TCP + TLS)第三方域名
dns-prefetchDNS 预解析第三方域名
prerender预渲染整个页面高概率跳转的页面
modulepreload预加载 ES ModuleJS 模块
manifestPWA 应用清单PWA
canonical指定规范 URL(SEO 去重)避免重复内容
alternate替代版本(多语言/RSS)国际化、RSS
noopener / noreferrer安全相关配合 target="_blank"
<head>
  <!-- 基本样式 -->
  <link rel="stylesheet" href="styles.css" />

  <!-- 网站图标 -->
  <link rel="icon" href="/favicon.ico" sizes="32x32" />
  <link rel="icon" href="/icon.svg" type="image/svg+xml" />
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />

  <!-- ========== 资源优化(按优先级从高到低)========== -->

  <!-- 1. preconnect:提前建立连接(DNS + TCP + TLS),节省连接时间 -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://cdn.example.com" crossorigin />

  <!-- 2. dns-prefetch:仅做 DNS 解析(比 preconnect 开销更小) -->
  <link rel="dns-prefetch" href="https://analytics.example.com" />

  <!-- 3. preload:预加载当前页面必需的关键资源 -->
  <link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin />
  <link rel="preload" href="/hero.jpg" as="image" />
  <link rel="preload" href="/critical.js" as="script" />

  <!-- 4. prefetch:空闲时预获取下一页可能用到的资源(低优先级) -->
  <link rel="prefetch" href="/next-page.js" />
  <link rel="prefetch" href="/next-page-data.json" />

  <!-- 5. modulepreload:预加载 ES Module(类似 preload 但针对模块) -->
  <link rel="modulepreload" href="/src/app.js" />

  <!-- ========== SEO 相关 ========== -->

  <!-- 规范 URL(告诉搜索引擎哪个是原始页面) -->
  <link rel="canonical" href="https://example.com/article/123" />

  <!-- 多语言版本 -->
  <link rel="alternate" hreflang="en" href="https://example.com/en/article" />
  <link rel="alternate" hreflang="zh" href="https://example.com/zh/article" />

  <!-- RSS 订阅 -->
  <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed.xml" />

  <!-- PWA 清单 -->
  <link rel="manifest" href="/manifest.json" />
</head>

面试加分点:preload 和 prefetch 的区别——preload 是告诉浏览器"当前页面马上需要这个资源",优先级高;prefetch 是"下一个页面可能需要",优先级低,浏览器空闲时才获取。错误使用 preload 反而会浪费带宽(Chrome 会在控制台警告未在 3 秒内使用的 preload 资源)。


22. <meta> 标签有哪些常见用法?

<meta> 标签位于 <head> 中,用于提供文档的元数据(metadata),这些数据不会显示在页面上,但对浏览器、搜索引擎和社交媒体平台非常重要。

<head>
  <!-- ========== 基础配置 ========== -->

  <!-- 字符编码(必须放在 head 最前面,且在 1024 字节内) -->
  <meta charset="UTF-8" />

  <!-- 视口配置(移动端必备) -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- IE 兼容模式:使用最新的渲染引擎 -->
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />

  <!-- ========== SEO 相关 ========== -->

  <!-- 页面描述(搜索引擎摘要,建议 150 字以内) -->
  <meta name="description" content="这是一个关于 HTML5 面试题的详细总结,涵盖语义化、存储、API 等核心知识点" />

  <!-- 关键词(现代搜索引擎已基本忽略此标签) -->
  <meta name="keywords" content="HTML5, 面试题, 前端开发" />

  <!-- 作者 -->
  <meta name="author" content="前端开发者" />

  <!-- 搜索引擎爬虫指令 -->
  <meta name="robots" content="index, follow" />          <!-- 允许索引和跟踪链接 -->
  <meta name="robots" content="noindex, nofollow" />      <!-- 禁止索引和跟踪 -->
  <meta name="robots" content="noarchive" />               <!-- 禁止缓存快照 -->

  <!-- ========== 社交媒体分享(Open Graph) ========== -->

  <!-- 分享到微信、微博、Facebook 等平台时显示的信息 -->
  <meta property="og:title" content="HTML5 面试题大全" />
  <meta property="og:description" content="最全面的 HTML5 面试知识点总结" />
  <meta property="og:image" content="https://example.com/share-cover.jpg" />
  <meta property="og:url" content="https://example.com/html5-interview" />
  <meta property="og:type" content="article" />

  <!-- Twitter 卡片 -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="HTML5 面试题大全" />

  <!-- ========== 安全相关 ========== -->

  <!-- 内容安全策略(CSP):防止 XSS 攻击 -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.example.com" />

  <!-- 禁止浏览器 MIME 类型嗅探 -->
  <meta http-equiv="X-Content-Type-Options" content="nosniff" />

  <!-- ========== 移动端与 PWA ========== -->

  <!-- 主题颜色(浏览器地址栏颜色) -->
  <meta name="theme-color" content="#007bff" />

  <!-- iOS Safari 全屏模式 -->
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
  <meta name="apple-mobile-web-app-title" content="我的应用" />

  <!-- 禁止将数字识别为电话号码 -->
  <meta name="format-detection" content="telephone=no" />

  <!-- ========== HTTP 等价属性 ========== -->

  <!-- 自动刷新/跳转 -->
  <meta http-equiv="refresh" content="5" />                <!-- 5秒后刷新 -->
  <meta http-equiv="refresh" content="3;url=/new-page" />  <!-- 3秒后跳转 -->

  <!-- 设置缓存(不推荐,建议用 HTTP 响应头) -->
  <meta http-equiv="Cache-Control" content="no-cache" />
</head>

面试加分点:<meta charset="UTF-8"> 必须放在 <head> 的最前面(在 <title> 之前),因为浏览器需要在解析 1024 字节内确定编码方式。http-equiv 属性模拟 HTTP 响应头的效果,但实际项目中更推荐直接在服务端设置 HTTP 响应头(如 CSP、Cache-Control),因为 meta 标签是 HTML 解析后才生效的。


23. 什么是 Drag and Drop(拖放 API)?

HTML5 原生的 Drag and Drop API 允许用户通过鼠标拖拽来移动元素或传递数据,无需依赖第三方库。

拖放流程和事件:

事件触发对象触发时机
dragstart被拖拽元素开始拖拽时
drag被拖拽元素拖拽过程中持续触发
dragend被拖拽元素拖拽结束时(无论是否放置成功)
dragenter目标区域拖拽元素进入目标区域时
dragover目标区域拖拽元素在目标区域上方移动时(必须阻止默认行为才能 drop)
dragleave目标区域拖拽元素离开目标区域时
drop目标区域在目标区域释放时
<!-- 可拖拽元素需设置 draggable="true" -->
<div class="task-board">
  <div class="column" id="todo">
    <h3>待办</h3>
    <div class="task" draggable="true" data-task-id="1">任务 1:设计页面</div>
    <div class="task" draggable="true" data-task-id="2">任务 2:编写接口</div>
    <div class="task" draggable="true" data-task-id="3">任务 3:联调测试</div>
  </div>
  <div class="column" id="doing">
    <h3>进行中</h3>
  </div>
  <div class="column" id="done">
    <h3>已完成</h3>
  </div>
</div>

<!-- 文件拖拽上传区域 -->
<div id="drop-zone">
  <p>将文件拖拽到此处上传</p>
</div>
// =========== 看板拖拽示例 ===========

let draggedElement = null

// 拖拽开始
document.querySelectorAll('.task').forEach((task) => {
  task.addEventListener('dragstart', (e) => {
    draggedElement = e.target
    e.target.style.opacity = '0.5'
    // DataTransfer 用于在拖拽事件之间传递数据
    e.dataTransfer.setData('text/plain', e.target.dataset.taskId)
    e.dataTransfer.effectAllowed = 'move'
  })

  task.addEventListener('dragend', (e) => {
    e.target.style.opacity = '1'
    draggedElement = null
  })
})

// 目标列监听
document.querySelectorAll('.column').forEach((column) => {
  column.addEventListener('dragover', (e) => {
    e.preventDefault()  // ⚠️ 必须阻止默认行为,否则 drop 事件不会触发
    e.dataTransfer.dropEffect = 'move'
    column.classList.add('drag-over')
  })

  column.addEventListener('dragleave', (e) => {
    column.classList.remove('drag-over')
  })

  column.addEventListener('drop', (e) => {
    e.preventDefault()
    column.classList.remove('drag-over')
    const taskId = e.dataTransfer.getData('text/plain')
    if (draggedElement) {
      column.appendChild(draggedElement)
      console.log(`任务 ${taskId} 移动到 ${column.id}`)
    }
  })
})


// =========== 文件拖拽上传 ===========

const dropZone = document.getElementById('drop-zone')

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault()
  dropZone.classList.add('active')
})

dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('active')
})

dropZone.addEventListener('drop', (e) => {
  e.preventDefault()
  dropZone.classList.remove('active')
  const files = e.dataTransfer.files  // 获取拖入的文件
  Array.from(files).forEach((file) => {
    console.log(`文件名: ${file.name}, 大小: ${file.size} 字节, 类型: ${file.type}`)
    // 上传文件...
  })
})

// 阻止浏览器默认的文件打开行为
document.addEventListener('dragover', (e) => e.preventDefault())
document.addEventListener('drop', (e) => e.preventDefault())

面试加分点:dragover 事件中必须调用 e.preventDefault(),否则浏览器不允许放置(drop 事件不会触发)。DataTransfer 对象还支持 setDragImage() 自定义拖拽时的预览图。实际项目中,复杂的拖拽排序推荐使用 SortableJS 或 dnd-kit 等成熟库。


24. 浏览器是如何解析 HTML 的?(关键渲染路径)

这是一道高频考题,考察你对浏览器渲染原理的理解。

关键渲染路径(Critical Rendering Path):

         HTML 文档
             │
             ▼
    HTML 解析器(Tokenizer → Tree Builder)
             │
             ▼
         DOM 树(Document Object Model)
             │                          CSS 文件
             │                             │
             │                             ▼
             │                    CSS 解析器(CSSOM)
             │                             │
             ▼                             ▼
         ┌────────────────────────────────────┐
         │        合并生成 渲染树(Render Tree)  │
         │    (只包含可见元素,display:none 排除) │
         └────────────────────────────────────┘
                         │
                         ▼
                   布局(Layout/Reflow)
                  计算每个元素的几何信息
                         │
                         ▼
                   绘制(Paint)
                  将元素绘制到各个图层
                         │
                         ▼
                   合成(Composite)
                  将图层合成为最终页面

详细步骤:

  1. 解析 HTML,构建 DOM 树

    • 浏览器逐字节读取 HTML,经过字节 → 字符 → Token → 节点 → DOM 树的过程
    • 解析过程中遇到 <script>(无 defer/async)会暂停 DOM 构建
  2. 解析 CSS,构建 CSSOM 树

    • 解析所有 CSS(外部样式表、<style> 标签、内联样式)
    • CSS 解析不阻塞 DOM 构建,但阻塞渲染树的生成
    • CSS 也阻塞 JS 执行(因为 JS 可能需要读取样式信息)
  3. 合并 DOM 和 CSSOM,生成渲染树

    • 渲染树只包含可见元素(display: none 的元素不在渲染树中)
    • visibility: hidden 的元素在渲染树中(占据空间)
  4. 布局(Layout)

    • 计算每个元素的确切位置和大小(盒模型)
  5. 绘制(Paint)

    • 将元素转化为屏幕上的实际像素
  6. 合成(Composite)

    • 将各个绘制图层组合在一起(GPU 加速)
<!-- 优化关键渲染路径的最佳实践 -->
<head>
  <!-- 1. CSS 放在 head 中,尽早加载(CSS 阻塞渲染) -->
  <link rel="stylesheet" href="critical.css" />

  <!-- 2. 非关键 CSS 异步加载 -->
  <link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="non-critical.css" /></noscript>

  <!-- 3. 内联关键 CSS(首屏渲染所需的最小 CSS) -->
  <style>
    /* 首屏关键样式,减少渲染阻塞 */
    body { margin: 0; font-family: sans-serif; }
    .hero { min-height: 100vh; }
  </style>
</head>
<body>
  <!-- 页面内容 -->

  <!-- 4. JS 放在 body 底部,或使用 defer -->
  <script defer src="app.js"></script>
</body>

面试加分点:CSS 虽然不阻塞 DOM 解析,但会阻塞 JS 执行(因为 JS 可能需要获取计算后的样式),而 JS 又会阻塞 DOM 解析,所以 CSS 间接地阻塞了 DOM 解析。这就是为什么CSS 文件要尽早放在 <head> 中加载。另外,Chrome 的 Performance 面板可以清晰地看到关键渲染路径的各个阶段耗时。

预加载扫描器(Preload Scanner):当主解析器被 <script> 阻塞时,浏览器不会"干等着"——它会启动一个轻量级的预加载扫描器,继续向后扫描 HTML 中的 <link>、<script>、<img> 等标签,提前发起网络请求。这意味着即使 JS 阻塞了 DOM 构建,后续的 CSS 和图片仍然在并行下载。不要用 JS 动态插入关键资源(如 document.write('<script>')),因为这些资源无法被预加载扫描器发现,会严重拖慢加载速度。


25. 什么是 <template> 标签?

<template> 标签用于定义一段 HTML 模板内容,该内容在页面加载时不会被渲染到页面上,也不会被执行(内部的 <script> 和 <img> 等不会加载),但可以通过 JavaScript 在运行时克隆和插入到 DOM 中。

特点:

  • 模板内容不可见,不占据布局空间
  • 模板内的资源不会被下载(图片、脚本等)
  • 可以放在 <head>、<body>、<table> 等任何位置
  • 是 Web Components 的核心组成部分之一
<!-- 定义模板 -->
<template id="card-template">
  <div class="card">
    <img src="" alt="" class="card-img" />
    <div class="card-body">
      <h3 class="card-title"></h3>
      <p class="card-desc"></p>
      <button class="card-btn">查看详情</button>
    </div>
  </div>
</template>

<!-- 列表渲染模板 -->
<template id="list-item-template">
  <tr>
    <td class="name"></td>
    <td class="email"></td>
    <td class="role"></td>
    <td><button class="delete-btn">删除</button></td>
  </tr>
</template>

<div id="card-container"></div>
<table>
  <thead>
    <tr><th>姓名</th><th>邮箱</th><th>角色</th><th>操作</th></tr>
  </thead>
  <tbody id="user-list"></tbody>
</table>
// =========== 使用模板渲染卡片 ===========

const template = document.getElementById('card-template')
const container = document.getElementById('card-container')

const products = [
  { title: 'iPhone 17', desc: '全新设计,性能飙升', img: 'iphone.jpg' },
  { title: 'MacBook Pro', desc: '专业级生产力工具', img: 'macbook.jpg' },
  { title: 'AirPods 4', desc: '沉浸式音频体验', img: 'airpods.jpg' },
]

products.forEach((product) => {
  // 克隆模板内容(deep clone)
  const clone = template.content.cloneNode(true)

  // 填充数据
  clone.querySelector('.card-img').src = product.img
  clone.querySelector('.card-img').alt = product.title
  clone.querySelector('.card-title').textContent = product.title
  clone.querySelector('.card-desc').textContent = product.desc

  // 绑定事件
  clone.querySelector('.card-btn').addEventListener('click', () => {
    alert(`查看 ${product.title} 的详情`)
  })

  // 插入到页面
  container.appendChild(clone)
})


// =========== 使用模板渲染表格 ===========

const listTemplate = document.getElementById('list-item-template')
const userList = document.getElementById('user-list')

const users = [
  { name: '张三', email: 'zhangsan@example.com', role: '管理员' },
  { name: '李四', email: 'lisi@example.com', role: '编辑' },
]

users.forEach((user) => {
  const clone = listTemplate.content.cloneNode(true)
  clone.querySelector('.name').textContent = user.name
  clone.querySelector('.email').textContent = user.email
  clone.querySelector('.role').textContent = user.role
  clone.querySelector('.delete-btn').addEventListener('click', (e) => {
    e.target.closest('tr').remove()
  })
  userList.appendChild(clone)
})

面试加分点:<template> 与直接在 JS 中写 innerHTML 字符串相比,优势在于:① HTML 结构可以在标签中直观编写和维护;② 模板内容在 DocumentFragment 中,克隆和插入性能更好;③ 是 Web Components 中定义组件模板的标准方式。Vue 的单文件组件 <template> 标签灵感正来源于此。


26. 什么是 Web Components?

Web Components 是浏览器原生支持的组件化方案,允许创建可复用、封装良好的自定义 HTML 元素。它由三项核心技术组成:

技术说明作用
Custom Elements自定义 HTML 元素定义新标签及其行为
Shadow DOM影子 DOM样式和结构隔离
HTML Templates<template> / <slot>定义可复用的 HTML 模板

优势: 原生支持、无需框架、跨框架通用(可在 React/Vue/Angular 中使用)

// =========== 完整的 Web Component 示例 ===========

class UserProfile extends HTMLElement {
  // 声明需要监听变化的属性
  static get observedAttributes() {
    return ['name', 'avatar', 'role']
  }

  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  // 元素插入 DOM 时调用(类似 mounted)
  connectedCallback() {
    this.render()
  }

  // 监听的属性变化时调用(类似 watch)
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render()
    }
  }

  // 元素从 DOM 移除时调用(类似 unmounted)
  disconnectedCallback() {
    console.log('组件已移除,清理资源...')
  }

  render() {
    const name = this.getAttribute('name') || '未知用户'
    const avatar = this.getAttribute('avatar') || 'default-avatar.png'
    const role = this.getAttribute('role') || '成员'

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-flex;
          align-items: center;
          gap: 12px;
          padding: 12px 16px;
          border-radius: 12px;
          background: #f8f9fa;
          font-family: system-ui, sans-serif;
        }
        :host([role="admin"]) {
          border: 2px solid #ffc107;
          background: #fffde7;
        }
        .avatar {
          width: 48px;
          height: 48px;
          border-radius: 50%;
          object-fit: cover;
        }
        .info { display: flex; flex-direction: column; }
        .name { font-weight: 600; font-size: 16px; color: #333; }
        .role { font-size: 12px; color: #999; margin-top: 2px; }
        .actions { margin-left: auto; }
        button {
          padding: 4px 12px;
          border: 1px solid #ddd;
          border-radius: 6px;
          background: white;
          cursor: pointer;
        }
        button:hover { background: #007bff; color: white; border-color: #007bff; }
      </style>
      <img class="avatar" src="${avatar}" alt="${name}" />
      <div class="info">
        <span class="name">${name}</span>
        <span class="role">${role}</span>
      </div>
      <div class="actions">
        <slot name="actions">
          <button>关注</button>
        </slot>
      </div>
    `

    // 绑定事件
    this.shadowRoot.querySelector('button')?.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('follow', {
        detail: { name },
        bubbles: true,    // 事件可以冒泡到 Shadow DOM 外部
        composed: true    // 事件可以穿越 Shadow Boundary
      }))
    })
  }
}

// 注册自定义元素(名称必须包含连字符)
customElements.define('user-profile', UserProfile)
<!-- 像使用原生标签一样使用自定义组件 -->
<user-profile name="张三" avatar="avatar1.jpg" role="admin"></user-profile>

<user-profile name="李四" avatar="avatar2.jpg" role="成员">
  <button slot="actions">发消息</button>
</user-profile>

<script>
  // 监听自定义事件
  document.addEventListener('follow', (e) => {
    console.log(`关注了 ${e.detail.name}`)
  })
</script>

面试加分点:Web Components 的最大优势是跨框架通用——同一个组件可以在 React、Vue、Angular 或纯 HTML 中使用。许多 UI 库(如 Shoelace、Lit)基于 Web Components 构建。缺点是需要手动管理状态和更新(没有 Vue/React 的响应式系统),SSR 支持也较弱。


27. 如何实现页面间的通信(跨标签页通信)?

在同一浏览器中,不同标签页(同源页面)之间有多种通信方式:

方案同源要求实时性适用场景
localStorage + storage 事件同源高简单数据同步
BroadcastChannel同源高多标签页广播通信
SharedWorker同源高复杂的多标签页协作
postMessage可跨域高iframe 通信、跨域通信
Service Worker同源高PWA 场景下的消息推送
IndexedDB + 轮询同源低大量数据共享
// =========== 1. localStorage + storage 事件(最简单)===========
// 页面 A:发送消息
localStorage.setItem('message', JSON.stringify({
  type: 'notification',
  content: '有新订单!',
  timestamp: Date.now()
}))

// 页面 B:监听消息(注意:只有其他页面修改时才触发,当前页面修改不触发)
window.addEventListener('storage', (e) => {
  if (e.key === 'message' && e.newValue) {
    const data = JSON.parse(e.newValue)
    console.log('收到消息:', data.content)
  }
})


// =========== 2. BroadcastChannel(推荐,专为此场景设计)===========
// 页面 A
const channelA = new BroadcastChannel('app-channel')
channelA.postMessage({ type: 'user-login', user: '张三' })

// 页面 B
const channelB = new BroadcastChannel('app-channel')  // 同名频道
channelB.addEventListener('message', (e) => {
  console.log('收到广播:', e.data)  // { type: 'user-login', user: '张三' }
})

// 不用时关闭
// channelA.close()


// =========== 3. SharedWorker(可处理复杂逻辑)===========

// shared-worker.js
const connections = []
self.onconnect = (e) => {
  const port = e.ports[0]
  connections.push(port)

  port.onmessage = (event) => {
    // 将消息转发给所有连接的标签页
    connections.forEach((p) => {
      if (p !== port) {  // 不发给自己
        p.postMessage(event.data)
      }
    })
  }
}

// 页面中使用
const worker = new SharedWorker('shared-worker.js')
worker.port.start()
worker.port.postMessage({ type: 'chat', content: 'Hello' })
worker.port.onmessage = (e) => {
  console.log('来自其他标签页:', e.data)
}


// =========== 4. postMessage(可跨域,常用于 iframe 通信)===========
// 父页面向 iframe 发消息
const iframe = document.getElementById('myIframe')
iframe.contentWindow.postMessage({ action: 'update', data: '新数据' }, 'https://other-domain.com')

// iframe 内接收
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://parent-domain.com') return  // ⚠️ 必须验证来源
  console.log('收到父页面消息:', e.data)
})

面试加分点:推荐使用 BroadcastChannel,它是专为多标签页通信设计的 API,语义清晰且无需借助存储。localStorage 方案虽然兼容性好,但有 5MB 大小限制且依赖存储写入。postMessage 是唯一支持跨域通信的方案,但必须严格验证 e.origin 防止安全风险。


28. 什么是 HTML 的内容安全策略(CSP)?

CSP(Content Security Policy,内容安全策略) 是一种安全机制,通过指定可信的资源来源,防止 XSS(跨站脚本) 和数据注入攻击。它的核心思想是:白名单策略——只允许加载指定来源的资源。

设置方式:

<!-- 方式1:通过 meta 标签设置(前端可控) -->
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'" />

<!-- 方式2:通过 HTTP 响应头设置(推荐,更安全) -->
<!-- Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com -->

<!-- 方式3:仅报告不阻止(用于测试) -->
<!-- Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report -->

常用指令:

指令说明示例
default-src默认策略(其他未指定的指令的回退)default-src 'self'
script-srcJavaScript 来源script-src 'self' https://cdn.example.com
style-srcCSS 来源style-src 'self' 'unsafe-inline'
img-src图片来源img-src 'self' data: https:
font-src字体来源font-src 'self' https://fonts.gstatic.com
connect-srcAjax/WebSocket 连接connect-src 'self' https://api.example.com
frame-srciframe 来源frame-src https://www.youtube.com
frame-ancestors谁可以嵌入当前页面(防止点击劫持)frame-ancestors 'self'
media-src音视频来源media-src 'self'
object-src插件来源(Flash 等)object-src 'none'
base-uri<base> 标签的 URLbase-uri 'self'
form-action表单提交目标form-action 'self'

常用关键词:

关键词说明
'self'当前域名
'none'禁止所有来源
'unsafe-inline'允许内联代码(尽量避免)
'unsafe-eval'允许 eval()(尽量避免)
'nonce-xxx'允许特定的内联脚本
'strict-dynamic'信任由可信脚本动态添加的脚本
<!-- CSP 实际应用示例 -->

<!-- 严格的 CSP 策略 -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'none';
  script-src 'self' 'nonce-abc123';
  style-src 'self' https://fonts.googleapis.com;
  img-src 'self' data: https://img.cdn.com;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
" />

<!-- 使用 nonce 允许特定的内联脚本 -->
<script nonce="abc123">
  // 这个内联脚本被允许执行(nonce 值匹配)
  console.log('合法的内联脚本')
</script>

<script>
  // ❌ 这个内联脚本会被 CSP 阻止(没有匹配的 nonce)
  console.log('被阻止的脚本')
</script>

面试加分点:CSP 是防御 XSS 的最后一道防线(不是替代输入验证和输出编码)。nonce 方式比 'unsafe-inline' 更安全,每次请求生成随机 nonce 值,只有匹配的内联脚本才能执行。现代框架(如 Next.js)会自动生成和管理 CSP nonce。CSP 违规报告可以通过 report-uri 或 report-to 指令发送到指定端点,便于监控和调试。


29. <img> 标签的 alt 和 title 属性有什么区别?

属性作用显示时机是否必需
alt替代文本(图片无法显示时的替代描述)图片加载失败时显示;屏幕阅读器朗读✅ 推荐始终添加
title提示文本(鼠标悬停时的工具提示)鼠标悬停时显示为 tooltip❌ 可选

alt 的重要作用:

  1. 无障碍:屏幕阅读器会朗读 alt 文本,帮助视障用户理解图片内容
  2. SEO:搜索引擎通过 alt 理解图片含义,有助于图片搜索排名
  3. 降级体验:图片加载失败或被禁用时,显示 alt 文本
  4. 语义化:明确描述图片的含义
<!-- ✅ 正确使用 alt -->
<img src="team-photo.jpg" alt="前端开发团队 2026 年合影,共 12 人在办公室合影" />

<!-- ✅ 功能性图片:描述图片的功能而非外观 -->
<a href="/home">
  <img src="logo.png" alt="回到首页" />  <!-- 描述点击后的效果 -->
</a>

<!-- ✅ 图表/信息图:描述数据含义 -->
<img src="sales-chart.png" alt="2026年Q1销售额 1200 万,同比增长 15%" />

<!-- ✅ 装饰性图片:使用空 alt -->
<img src="divider.png" alt="" />           <!-- 分割线装饰 -->
<img src="bg-pattern.png" alt="" role="presentation" />  <!-- 背景装饰 -->

<!-- ❌ 错误示范 -->
<img src="photo.jpg" />                     <!-- 缺少 alt 属性 -->
<img src="photo.jpg" alt="图片" />           <!-- 无意义的 alt -->
<img src="photo.jpg" alt="photo.jpg" />      <!-- 文件名不是有效描述 -->
<img src="banner.jpg" alt="点击查看详情点击查看详情点击查看详情" />  <!-- 冗余重复 -->

<!-- title 的使用 -->
<img src="avatar.jpg" alt="张三的头像" title="点击查看张三的个人主页" />
<a href="/profile" title="查看个人资料">个人中心</a>

<!-- 注意:title 不应该替代 alt -->
<!-- title 不是必需的,屏幕阅读器对 title 的支持不如 alt -->

面试加分点:alt 是 <img> 标签在 HTML 规范中的必需属性(虽然省略不会报错)。编写 alt 的原则是"闭上眼睛,别人读 alt 文本给你听时,你能准确理解图片传达的信息"。空 alt="" 不等于没有 alt——空 alt 告诉屏幕阅读器"这是装饰性图片,可以跳过",而没有 alt 属性时屏幕阅读器可能会朗读文件名。


30. HTML5 中如何实现地理定位?

HTML5 的 Geolocation API 允许网页获取用户的地理位置信息。出于隐私保护,使用前必须获得用户的明确授权,且只能在 HTTPS 环境下使用。

// =========== 基本用法 ===========

// 检测浏览器是否支持
if ('geolocation' in navigator) {
  // 获取一次性位置
  navigator.geolocation.getCurrentPosition(
    // 成功回调
    (position) => {
      console.log('纬度:', position.coords.latitude)      // 如 39.9042
      console.log('经度:', position.coords.longitude)     // 如 116.4074
      console.log('精度(米):', position.coords.accuracy)   // 位置精度半径
      console.log('海拔:', position.coords.altitude)       // 可能为 null
      console.log('速度:', position.coords.speed)          // 可能为 null
      console.log('方向:', position.coords.heading)        // 可能为 null
      console.log('时间戳:', position.timestamp)
    },
    // 失败回调
    (error) => {
      switch (error.code) {
        case error.PERMISSION_DENIED:
          console.error('用户拒绝了定位请求')
          break
        case error.POSITION_UNAVAILABLE:
          console.error('位置信息不可用')
          break
        case error.TIMEOUT:
          console.error('请求超时')
          break
      }
    },
    // 配置选项
    {
      enableHighAccuracy: true,  // 启用高精度(GPS,更耗电)
      timeout: 10000,            // 超时时间(毫秒)
      maximumAge: 60000          // 缓存有效期(毫秒),0 表示不缓存
    }
  )

  // 持续监听位置变化(导航场景)
  const watchId = navigator.geolocation.watchPosition(
    (position) => {
      updateMapMarker(position.coords.latitude, position.coords.longitude)
    },
    (error) => console.error('监听位置失败:', error),
    { enableHighAccuracy: true }
  )

  // 停止监听
  // navigator.geolocation.clearWatch(watchId)

} else {
  console.log('您的浏览器不支持地理定位')
}


// =========== 实际应用:计算两点间的距离 ===========

function getDistanceKm(lat1, lon1, lat2, lon2) {
  const R = 6371  // 地球半径(千米)
  const dLat = (lat2 - lat1) * Math.PI / 180
  const dLon = (lon2 - lon1) * Math.PI / 180
  const a = Math.sin(dLat / 2) ** 2 +
            Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
            Math.sin(dLon / 2) ** 2
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}

// 计算当前位置到某地点的距离
navigator.geolocation.getCurrentPosition((pos) => {
  const distance = getDistanceKm(
    pos.coords.latitude, pos.coords.longitude,
    39.9042, 116.4074  // 北京天安门坐标
  )
  console.log(`距离北京天安门 ${distance.toFixed(2)} 公里`)
})

面试加分点:Geolocation API 有三种定位方式:① GPS(精度最高,但耗电且仅移动设备支持);② Wi-Fi 定位(通过周围 Wi-Fi 热点推算位置);③ IP 定位(精度最低,只能到城市级别)。浏览器会根据 enableHighAccuracy 设置和设备能力自动选择最佳方式。实际项目中,高精度定位推荐使用百度/腾讯/高德等地图 SDK。


31. HTML 文档为什么推荐使用 UTF-8 编码?

<meta charset="UTF-8"> 是每个 HTML 文件都应声明的编码方式。UTF-8 是目前 Web 领域最通用的字符编码标准。

常见编码对比

编码说明字节数适用范围
ASCII最早的编码标准1 字节只支持英文 + 基本符号(128 个字符)
GBK/GB2312中国国标编码1-2 字节中文 + 英文
UTF-8Unicode 的可变长编码1-4 字节✅ 几乎覆盖所有语言文字
UTF-16Unicode 的定长/变长编码2-4 字节JavaScript 内部使用

为什么选 UTF-8?

优势说明
国际化支持覆盖 Unicode 全部字符(中文、日文、韩文、emoji 等)
向后兼容 ASCII英文字母只占 1 字节,与 ASCII 完全兼容
节省空间可变长编码,英文 1 字节,中文 3 字节,比 UTF-16 更紧凑
Web 标准HTML5 规范默认推荐 UTF-8,W3C 和 WHATWG 强烈建议
避免乱码统一编码避免不同系统、不同浏览器的编码差异导致乱码
搜索引擎友好搜索引擎对 UTF-8 页面的抓取和索引更稳定
<!-- ========== 正确声明 UTF-8 编码 ========== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <!-- ⚠️ charset 声明必须在 <head> 的前 1024 字节内 -->
  <meta charset="UTF-8" />
  <title>页面标题</title>
</head>
<body>
  <p>中文内容不会乱码 ✅</p>
  <p>English works too ✅</p>
  <p>日本語も大丈夫 ✅</p>
  <p>Emoji 也支持:😀🎉🚀 ✅</p>
</body>
</html>

<!-- ========== 乱码的常见原因 ========== -->
<!--
  1. HTML 文件声明 UTF-8,但文件实际保存为 GBK 编码
  2. 服务器 HTTP 响应头的 Content-Type 编码与文件不一致
  3. 数据库编码与页面编码不统一
-->

<!-- 服务器端也要配置一致的编码 -->
<!-- Nginx: charset utf-8; -->
<!-- Apache: AddDefaultCharset UTF-8 -->
<!-- HTTP 响应头: Content-Type: text/html; charset=UTF-8 -->
// UTF-8 编码规则(面试可能追问)
// 1 字节: 0xxxxxxx                          → ASCII (0-127)
// 2 字节: 110xxxxx 10xxxxxx                  → 拉丁文、希腊文等
// 3 字节: 1110xxxx 10xxxxxx 10xxxxxx         → 中文、日文、韩文
// 4 字节: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx → Emoji、生僻字

// 例:'中' 的 Unicode 是 U+4E2D
// UTF-8 编码:11100100 10111000 10101101 → 0xE4 0xB8 0xAD(3 字节)

console.log(new TextEncoder().encode('A'))   // Uint8Array [65](1 字节)
console.log(new TextEncoder().encode('中'))  // Uint8Array [228, 184, 173](3 字节)
console.log(new TextEncoder().encode('😀'))  // Uint8Array [240, 159, 152, 128](4 字节)

💡 面试加分点: UTF-8 之所以成为 Web 标准,核心原因是向后兼容 ASCII + 可变长度——英文网站几乎没有额外开销,中文网站比 UTF-16 节省空间。面试时如果被问到"编码和解码不一致会怎样"——会出现乱码,因为不同编码对同一字节序列的解读方式不同。前端常见的乱码解决方案:① 确保 <meta charset> 与文件实际编码一致;② 确保服务器 Content-Type 头部声明正确;③ 数据库使用 utf8mb4(支持 4 字节的 emoji)。


32. 什么是置换元素和非置换元素?

这道题考察你对 HTML 元素渲染行为的底层理解。

定义

类型定义特点
置换元素(Replaced Element)内容由外部资源决定,不由 CSS 渲染有固有尺寸(宽高由内容决定)
非置换元素(Non-replaced Element)内容由DOM 内容和 CSS渲染尺寸由内容或 CSS 决定

常见分类

置换元素非置换元素
<img><div>
<input><span>
<textarea><p>
<select><h1> ~ <h6>
<video><a>
<audio><table>
<canvas><ul> / <li>
<iframe><section>
<object><article>

核心区别

<!-- ========== 置换元素的特点 ========== -->

<!-- 1. 有固有尺寸(不设置宽高也能显示) -->
<img src="photo.jpg" />
<!-- 图片有自身的像素宽高,浏览器按原始尺寸显示 -->

<!-- 2. 可以设置 width/height(即使是行内元素) -->
<img src="photo.jpg" style="width: 200px; height: 150px;" />
<!-- ✅ 生效!虽然 img 是 inline 元素,但它是置换元素 -->

<span style="width: 200px; height: 150px;">文字</span>
<!-- ❌ 不生效!span 是非置换行内元素,width/height 无效 -->

<!-- 3. 内容不受 CSS 控制 -->
<img src="photo.jpg" />
<!-- CSS 可以控制 img 的尺寸、边距、边框,但不能控制图片本身的内容 -->

<!-- 4. 不支持伪元素 ::before / ::after(大部分置换元素) -->
<style>
  /* ❌ img 的 ::before 和 ::after 在大多数浏览器中不生效 */
  img::before { content: '图片:'; }

  /* ✅ 非置换元素可以使用伪元素 */
  .title::before { content: '📌 '; }
</style>


<!-- ========== 非置换元素的特点 ========== -->

<!-- 1. 内容由 DOM 内容决定 -->
<p>这段文字就是 p 元素的内容</p>

<!-- 2. 行内非置换元素不能设置宽高 -->
<span style="width: 200px;">宽度无效</span>
<!-- 需要设置 display: inline-block 或 display: block 才能设置宽高 -->

<!-- 3. 可以使用 ::before / ::after 伪元素 -->
<style>
  .tag::before { content: '#'; color: blue; }
</style>
<span class="tag">前端</span>  <!-- 显示为 #前端 -->
// ========== 判断是否为置换元素 ==========
// 置换元素的 getComputedStyle 会反映其固有尺寸
const img = document.querySelector('img')
const span = document.querySelector('span')

// img 即使不设置宽高,也有固有尺寸
console.log(img.naturalWidth, img.naturalHeight)  // 如 800, 600

// span 没有固有尺寸概念
console.log(getComputedStyle(span).width)  // 'auto'(由内容撑开)

💡 面试加分点: 置换元素最重要的特征是——行内置换元素可以设置 width 和 height(如 <img>、<input>),而行内非置换元素(如 <span>)不可以。这也是为什么 <img> 虽然是行内元素却能设置宽高的原因。另外,<input> 等表单元素的样式在不同浏览器/操作系统下表现不同,正是因为它们是置换元素,由浏览器内核(而非 CSS)控制渲染。


33. 表单提交中 GET 和 POST 有什么区别?

对比项GETPOST
参数位置URL 查询字符串(?key=value&...)请求体(Request Body)
安全性❌ 参数暴露在 URL 中,会被浏览器历史记录保存✅ 参数在请求体中,相对更安全
数据大小有限制(通常 2KB ~ 8KB,取决于浏览器和服务器)理论上无限制(实际受服务器配置限制)
幂等性✅ 幂等(多次相同请求结果一致)❌ 非幂等(可能产生副作用)
缓存✅ 会被浏览器缓存❌ 不会被缓存
书签✅ 可以保存为书签❌ 不能保存为书签
回退/刷新无副作用(再次发送请求)浏览器会提示"是否重新提交表单"
编码类型application/x-www-form-urlencoded支持多种(form-data、json 等)
适用场景查询、搜索、获取数据提交表单、上传文件、修改数据
<!-- ========== GET 提交 ========== -->
<form action="/search" method="GET">
  <input type="text" name="keyword" placeholder="搜索关键词" />
  <input type="number" name="page" value="1" />
  <button type="submit">搜索</button>
</form>
<!-- 提交后 URL: /search?keyword=前端&page=1 -->
<!-- 适合:搜索、筛选、分页等查询操作 -->


<!-- ========== POST 提交 ========== -->
<form action="/register" method="POST">
  <input type="text" name="username" placeholder="用户名" />
  <input type="password" name="password" placeholder="密码" />
  <input type="file" name="avatar" />
  <button type="submit">注册</button>
</form>
<!-- 提交后 URL: /register(参数不在 URL 中) -->
<!-- 适合:注册、登录、上传文件等修改操作 -->


<!-- ========== 上传文件必须用 POST + multipart/form-data ========== -->
<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="file" multiple />
  <button type="submit">上传</button>
</form>
// ========== 用 JavaScript 模拟 GET 和 POST ========== 

// GET 请求:参数拼接在 URL 中
fetch('/api/search?keyword=前端&page=1')
  .then(res => res.json())
  .then(data => console.log(data))

// POST 请求:参数在请求体中
fetch('/api/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'zhangsan',
    password: '123456'
  })
})
  .then(res => res.json())
  .then(data => console.log(data))


// ========== 常见误区 ==========
// ❌ 误区1:"POST 比 GET 安全"
// 真相:POST 只是参数不在 URL 中,但抓包工具可以轻松看到请求体
// 真正的安全要靠 HTTPS 加密传输

// ❌ 误区2:"GET 只能传字符串"
// 真相:URL 参数确实是字符串,但可以通过序列化传递复杂数据
// 例如:/api/list?filters={"status":"active","sort":"date"}

// ❌ 误区3:"POST 请求不能被缓存"
// 真相:HTTP 规范允许缓存 POST 响应(通过 Cache-Control),但浏览器默认不缓存

深层原理:TCP 层面的区别

GET 请求:
  浏览器会把 HTTP Header 和 Data 一起发出去
  服务器响应 200(一次 TCP 数据包)

POST 请求:
  浏览器先发 Header(带 Expect: 100-continue)
  服务器响应 100 Continue
  浏览器再发 Data
  服务器响应 200(两次 TCP 数据包)

⚠️ 注意:这个"两次数据包"的说法有争议,实际取决于浏览器实现
  Firefox 的 POST 就只发一次包
  所以面试时说"大部分浏览器"更严谨

💡 面试加分点: 从 RESTful API 设计角度来说,HTTP 方法不只 GET/POST——GET(查询)、POST(创建)、PUT(全量更新)、PATCH(部分更新)、DELETE(删除)各有语义。面试时提到"幂等性"是加分点——GET/PUT/DELETE 是幂等的(多次操作结果一致),POST 是非幂等的。另外,GET 请求的 URL 长度限制不是 HTTP 协议本身的限制,而是浏览器和 Web 服务器的实现限制(IE 约 2KB,Chrome 约 8KB,Nginx 默认 8KB)。


34. <script type="module"> 与原生 ES Module

什么是浏览器原生 ES Module?

ES2015 引入了模块系统(import/export),现代浏览器已原生支持在 HTML 中直接使用 <script type="module"> 加载 ES Module,无需打包工具。这也是 Vite 在开发模式下能实现极速启动的底层原理。

<script type="module"> 的特性:

特性<script>(传统)<script type="module">
作用域全局(变量挂在 window 上)模块作用域(不污染全局)
加载方式同步阻塞(默认)默认 defer(异步,DOM 解析完再执行)
严格模式需要手动 'use strict'自动启用严格模式
重复加载重复 <script> 会执行多次同一模块只执行一次(缓存)
顶层 thiswindowundefined
import/export❌ 不支持✅ 支持
CORS同域脚本不需要始终使用 CORS(即使同域)
<!-- ========== 基本使用 ========== -->

<!-- 1. 传统脚本:变量挂在全局 -->
<script>
  var name = '张三'  // window.name = '张三'
</script>

<!-- 2. 模块脚本:独立作用域 -->
<script type="module">
  const name = '李四'  // 不会污染全局
  console.log(window.name)  // 不是 '李四'
</script>

<!-- 3. 引入外部模块文件 -->
<script type="module" src="./app.js"></script>

<!-- 4. nomodule 作为降级方案(现代浏览器忽略,旧浏览器执行) -->
<script nomodule src="./legacy-bundle.js"></script>
// ========== app.js(入口模块) ==========
import { formatDate } from './utils/date.js'     // 注意:必须带 .js 后缀!
import { createApp } from './framework/index.js'

// 支持动态导入(按需加载)
const module = await import('./heavy-module.js')

// 支持 top-level await(模块顶层可直接使用 await)
const config = await fetch('/api/config').then(r => r.json())

createApp(config)
// ========== utils/date.js ==========
// 命名导出
export function formatDate(date) {
  return new Intl.DateTimeFormat('zh-CN').format(date)
}

export function timeAgo(date) {
  const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
  if (seconds < 60) return `${seconds}秒前`
  if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟前`
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时前`
  return formatDate(date)
}

// 默认导出
export default { formatDate, timeAgo }

Import Maps(导入映射):

解决模块路径别名问题,无需打包工具即可使用"裸模块标识符"(如 import { ref } from 'vue'):

<!-- Import Maps 必须在任何 module 脚本之前声明 -->
<script type="importmap">
{
  "imports": {
    "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
    "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
    "@/utils/": "./src/utils/",
    "#components/": "./src/components/"
  }
}
</script>

<script type="module">
// ✅ 现在可以像在 Node.js/打包环境中一样使用裸模块名
import { createApp, ref } from 'vue'
import { debounce } from 'lodash-es'
import { formatDate } from '@/utils/date.js'

const app = createApp({
  setup() {
    const count = ref(0)
    return { count }
  }
})
app.mount('#app')
</script>

与 defer / async 的关系:

传统 <script>       : 解析 HTML → 遇到脚本 → 停止解析 → 下载 → 执行 → 继续解析
<script defer>      : 解析 HTML → 并行下载 → HTML 解析完 → 按序执行
<script async>      : 解析 HTML → 并行下载 → 下载完立即执行(可能打断解析)
<script type=module>: 解析 HTML → 并行下载(所有依赖) → HTML 解析完 → 按序执行(等同 defer)
<script type=module async>: 解析 HTML → 并行下载 → 下载完立即执行(类似 async)

💡 面试加分点:Vite 在开发模式下直接利用浏览器原生 ES Module,每个文件是一个独立模块请求,所以启动极快(不需要打包)。但生产环境仍然需要 Rollup 打包,因为:①HTTP/2 虽然支持多路复用,但大量小文件仍有性能问题;②需要 Tree Shaking 消除无用代码;③需要代码分割(Code Splitting)优化加载。Import Maps 目前主流浏览器都已支持(Chrome 89+、Firefox 108+、Safari 16.4+),是未来无构建开发的基础设施。


35. HTML 如何做 SEO 优化?(结构化数据与社交分享)

SEO 优化的 HTML 层面手段:

1. 基础 HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <!-- ✅ title 是最重要的 SEO 因素,每个页面唯一,包含关键词 -->
  <title>前端面试题大全 - 2025最新 HTML/CSS/JS 高频考点 | 你的网站名</title>

  <!-- ✅ description 会显示在搜索结果摘要中 -->
  <meta name="description" content="涵盖 HTML5 语义化、CSS 布局、JavaScript 异步、Vue/React 框架等 200+ 前端面试题,附带详细解答和代码示例。" />

  <!-- ✅ canonical 防止重复内容(多个 URL 指向相同内容时指定主 URL) -->
  <link rel="canonical" href="https://example.com/interview/html" />

  <!-- ✅ robots 控制搜索引擎行为 -->
  <meta name="robots" content="index, follow" />
  <!-- noindex:不收录此页 | nofollow:不追踪链接 | noarchive:不缓存快照 -->
</head>
<body>
  <!-- ✅ 使用语义化标签,搜索引擎能更好理解页面结构 -->
  <header>
    <nav aria-label="面包屑导航">
      <ol>
        <li><a href="/">首页</a></li>
        <li><a href="/interview">面试题</a></li>
        <li aria-current="page">HTML 篇</li>
      </ol>
    </nav>
  </header>

  <main>
    <article>
      <!-- ✅ 每个页面只有一个 h1 -->
      <h1>HTML 高频面试题</h1>
      <h2>1. DOCTYPE 的作用</h2>
      <h3>浏览器渲染模式</h3>
      <!-- 标题层级不能跳跃(h1 → h3 ❌) -->
    </article>
  </main>
</body>
</html>

2. 结构化数据(JSON-LD / Schema.org)

结构化数据让搜索引擎以"富媒体片段"(Rich Snippets)展示你的页面:

<!-- FAQ 结构化数据 — 搜索结果中直接展示问答 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "什么是 DOCTYPE?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "DOCTYPE 声明告知浏览器当前文档使用哪个 HTML 版本解析,决定渲染模式。"
      }
    },
    {
      "@type": "Question",
      "name": "HTML5 有哪些语义化标签?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "header、nav、main、article、section、aside、footer 等。"
      }
    }
  ]
}
</script>

<!-- 面包屑导航结构化数据 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "首页", "item": "https://example.com/" },
    { "@type": "ListItem", "position": 2, "name": "面试题", "item": "https://example.com/interview" },
    { "@type": "ListItem", "position": 3, "name": "HTML 篇", "item": "https://example.com/interview/html" }
  ]
}
</script>

<!-- 文章结构化数据 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "HTML 高频面试题",
  "author": { "@type": "Person", "name": "作者名" },
  "datePublished": "2025-01-01",
  "dateModified": "2025-05-07",
  "image": "https://example.com/cover.jpg"
}
</script>

3. Open Graph 协议(社交分享优化)

控制页面在微信、Twitter、Facebook 等平台分享时的标题、图片、描述:

<head>
  <!-- ===== Open Graph(Facebook / 微信 / QQ) ===== -->
  <meta property="og:type" content="article" />
  <meta property="og:title" content="HTML 高频面试题 - 前端面试必备" />
  <meta property="og:description" content="涵盖 33+ 道 HTML 面试题,附详细解答" />
  <meta property="og:image" content="https://example.com/og-cover.png" />
  <meta property="og:url" content="https://example.com/interview/html" />
  <meta property="og:site_name" content="前端面试宝典" />
  <meta property="og:locale" content="zh_CN" />

  <!-- ===== Twitter Card ===== -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="HTML 高频面试题" />
  <meta name="twitter:description" content="涵盖 33+ 道 HTML 面试题" />
  <meta name="twitter:image" content="https://example.com/twitter-cover.png" />
</head>

4. 其他 SEO 要点

<!-- ✅ 图片优化 -->
<img
  src="diagram.webp"
  alt="浏览器关键渲染路径流程图"  <!-- 描述性 alt,包含关键词 -->
  width="800"
  height="400"
  loading="lazy"
/>

<!-- ✅ 链接语义 -->
<a href="/interview/css">CSS 面试题</a>  <!-- 有意义的锚文本 -->
<a href="https://external.com" rel="noopener noreferrer">外部链接</a>  <!-- 外链加 rel -->
<a href="/login" rel="nofollow">登录</a>  <!-- 不需要被收录的页面 -->

<!-- ✅ 多语言 -->
<link rel="alternate" hreflang="en" href="https://example.com/en/interview" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/interview" />
<link rel="alternate" hreflang="x-default" href="https://example.com/interview" />

SEO 相关的配置文件:

// robots.txt(告诉搜索引擎爬虫哪些页面可以/不可以爬取)
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Sitemap: https://example.com/sitemap.xml

// sitemap.xml(站点地图,列出所有需要收录的页面)
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/interview/html</loc>
    <lastmod>2025-05-07</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

💡 面试加分点:Core Web Vitals(LCP、FID/INP、CLS)已成为 Google 搜索排名因素之一。语义化 HTML + 结构化数据 + 性能优化三者结合才是完整的 SEO 方案。用 Google Search Console 的"富媒体搜索结果测试"工具可以验证结构化数据是否正确。另外,SPA 应用需要 SSR/SSG(如 Next.js、Nuxt.js)才能让搜索引擎正确抓取内容。


36. HTML 中有哪些性能优化相关的标签和属性?(资源优先级全景)

浏览器需要加载大量资源(CSS、JS、图片、字体等),资源优先级控制决定了哪些资源先加载、哪些延后,直接影响页面加载速度。

资源提示标签(Resource Hints)全景:

标签/属性作用使用时机
<link rel="preload">提前加载当前页面必需的关键资源首屏关键 CSS/字体/大图
<link rel="prefetch">空闲时预加载下一页面可能需要的资源下一页的 JS/CSS
<link rel="preconnect">提前建立连接(DNS + TCP + TLS)第三方域名(CDN/API)
<link rel="dns-prefetch">只做 DNS 解析第三方域名(更轻量)
<link rel="prerender">在后台预渲染整个页面高概率要访问的下一页(慎用,耗资源)
<link rel="modulepreload">预加载 ES Module 及其依赖模块化应用的关键模块
<head>
  <!-- ===== 资源提示 ===== -->

  <!-- 1. preconnect:提前建立连接(节省 DNS + TCP + TLS 时间,约 100~300ms) -->
  <link rel="preconnect" href="https://cdn.example.com" />
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />

  <!-- 2. dns-prefetch:只做 DNS 解析(兼容性更好,开销更小) -->
  <link rel="dns-prefetch" href="https://analytics.example.com" />

  <!-- 3. preload:强制优先加载关键资源(不会推迟其他资源) -->
  <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
  <link rel="preload" href="/css/critical.css" as="style" />
  <link rel="preload" href="/images/hero.webp" as="image" />
  <link rel="preload" href="/js/app.js" as="script" />

  <!-- 4. modulepreload:预加载 ES Module(包括其静态依赖) -->
  <link rel="modulepreload" href="/js/app.js" />
  <link rel="modulepreload" href="/js/utils.js" />

  <!-- 5. prefetch:空闲时预获取(下一个页面可能需要的资源) -->
  <link rel="prefetch" href="/js/detail-page.js" />
  <link rel="prefetch" href="/data/product-list.json" />

  <!-- ===== 实际加载 ===== -->
  <link rel="stylesheet" href="/css/critical.css" />
</head>

fetchpriority 属性(资源优先级微调):

<!-- fetchpriority 可以覆盖浏览器默认的优先级判断 -->

<!-- ✅ 提高优先级:首屏大图(LCP 元素) -->
<img src="hero-banner.webp" fetchpriority="high" alt="首页横幅" />

<!-- ✅ 降低优先级:非首屏图片 -->
<img src="footer-ad.webp" fetchpriority="low" loading="lazy" alt="广告" />

<!-- ✅ 关键脚本提高优先级 -->
<script src="critical-interaction.js" fetchpriority="high"></script>

<!-- ✅ 配合 preload 使用 -->
<link rel="preload" href="/api/initial-data" as="fetch" fetchpriority="high" crossorigin />

图片/iframe 的懒加载与解码:

<!-- loading 属性:控制何时加载 -->
<img src="photo.webp" loading="lazy" alt="照片" />   <!-- 进入视口前才加载 -->
<img src="hero.webp" loading="eager" alt="首图" />   <!-- 立即加载(默认) -->
<iframe src="/widget" loading="lazy"></iframe>         <!-- iframe 也支持 -->

<!-- decoding 属性:控制图片解码方式 -->
<img src="photo.webp" decoding="async" alt="照片" />  <!-- 异步解码,不阻塞主线程渲染 -->
<img src="hero.webp" decoding="sync" alt="首图" />    <!-- 同步解码,确保立即可见 -->
<img src="other.webp" decoding="auto" alt="其他" />   <!-- 浏览器自行决定(默认) -->

<!--
  loading vs decoding 的区别:
  - loading="lazy":控制何时 **下载** 图片(延迟网络请求)
  - decoding="async":控制下载后如何 **解码** 图片(不阻塞渲染)
  - 两者可以组合使用:loading="lazy" decoding="async"
-->

Early Hints(103 状态码):

// 传统流程:
Client → Server: GET /page
Server: 处理请求(可能需要 200ms~1s 查数据库等)
Server → Client: 200 OK + HTML(此时才开始下载 CSS/JS)

// Early Hints 优化流程:
Client → Server: GET /page
Server → Client: 103 Early Hints(立即返回,告知关键资源)
  Link: </css/main.css>; rel=preload; as=style
  Link: </fonts/inter.woff2>; rel=preload; as=font; crossorigin
(浏览器立即开始下载这些资源)
Server: 继续处理请求...
Server → Client: 200 OK + HTML(此时 CSS 可能已经下载完了!)
// Node.js / Express 实现 Early Hints
app.get('/page', (req, res) => {
  // 先发送 103 Early Hints
  res.writeEarlyHints({
    link: [
      '</css/main.css>; rel=preload; as=style',
      '</js/app.js>; rel=preload; as=script',
    ]
  })

  // 再处理耗时逻辑
  const data = await fetchFromDatabase()

  // 最终返回完整响应
  res.render('page', { data })
})

完整的资源优先级策略示例:

<!DOCTYPE html>
<html>
<head>
  <!-- 第一优先级:建立连接 -->
  <link rel="preconnect" href="https://cdn.example.com" />

  <!-- 第二优先级:预加载关键资源 -->
  <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
  <link rel="preload" href="/css/above-fold.css" as="style" />

  <!-- 第三优先级:关键 CSS -->
  <link rel="stylesheet" href="/css/above-fold.css" />

  <!-- 第四优先级:非关键 CSS 异步加载 -->
  <link rel="stylesheet" href="/css/below-fold.css" media="print" onload="this.media='all'" />

  <!-- 第五优先级:下一页资源预获取 -->
  <link rel="prefetch" href="/js/next-page.js" />
</head>
<body>
  <!-- LCP 元素:高优先级 -->
  <img src="hero.webp" fetchpriority="high" alt="首图" />

  <!-- 非首屏图片:低优先级 + 懒加载 -->
  <img src="gallery-1.webp" loading="lazy" decoding="async" fetchpriority="low" alt="相册" />
</body>
</html>

💡 面试加分点:preload 和 prefetch 的本质区别:preload 是命令式的("我现在就需要"),prefetch 是建议式的("我等会儿可能需要")。滥用 preload 反而会降低性能(抢占带宽)。fetchpriority 是 Chrome 101+ 支持的新属性,可以精细控制 Fetch API、<img>、<script>、<link> 的请求优先级,对优化 LCP 特别有效。Early Hints(103)需要服务器和 CDN 支持(Cloudflare 已支持),可以节省服务端处理时间期间的网络空闲等待。