HTML 高频面试题

HTML 高频面试题

1. DOCTYPE 的作用是什么?

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

浏览器的三种渲染模式:

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

为什么 HTML5 的 DOCTYPE 这么简洁?

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

html 复制代码
<!-- 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> 进度条 文件上传、任务进度
html 复制代码
<!-- ❌ 不语义化:全是 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 的区别?

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

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

为什么 <script src> 会阻塞?

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

html 复制代码
<!-- 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 的脚本

执行流程对比:

less 复制代码
无属性:   HTML解析 → 暂停 → 下载JS → 执行JS → 继续HTML解析
async:   HTML解析 ──────────────────→ 继续解析...
              └─ 下载JS → 执行JS(下载完就执行,可能打断解析)
defer:   HTML解析 ──────────────────→ 解析完毕 → 执行JS → DOMContentLoaded
              └─ 下载JS ──────────────┘(等DOM解析完才执行)
html 复制代码
<!-- 无属性:阻塞解析,适合关键的内联配置 -->
<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

仅触发重绘(不触发回流)的属性: colorbackground-colorvisibilitybox-shadowoutline

javascript 复制代码
// ❌ 触发多次回流:每次循环都读写交替
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'  // 触发一次回流

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


6. meta viewport 的作用?

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

html 复制代码
<!-- 最常用的设置 -->
<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
html 复制代码
<!-- 移动端常用配置:允许缩放 -->
<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 hr span a em strong i b label code img input button select textarea

注意事项:

  • 行内元素的 padding-top / padding-bottom 虽然视觉上会渲染,但不会影响周围元素的布局(不推开其他元素)
  • 可以通过 CSS 的 display 属性互相转换:display: block / inline / inline-block
  • HTML5 不再严格使用"块级"和"行内"的分类,而是采用更细致的内容模型(如 flow content、phrasing content 等),但面试中仍以传统分类为主
html 复制代码
<!-- 块级元素:独占一行 -->
<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 本地存储方式有哪些?

特性 Cookie localStorage sessionStorage IndexedDB
大小限制 ~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 头,浏览器帮你做了

javascript 复制代码
// ======= 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 控制跨站请求时是否携带 Cookie CSRF 攻击
ini 复制代码
Set-Cookie 完整语法(服务端设置):
Set-Cookie: name=value; Path=/; Domain=.example.com; Max-Age=86400; HttpOnly; Secure; SameSite=Strict
javascript 复制代码
// ✅ 前端设置 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)
javascript 复制代码
// 实际场景示例:
// 1. Strict ------ 从外部链接点进来,Cookie 不会被携带(用户需重新登录)
// 2. Lax ------ 从外部链接点进来 GET 请求会带 Cookie,但 POST 表单不带
// 3. None ------ 第三方场景,如嵌入式支付页面需要读取登录态

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

localStorage 跨标签页通信:

javascript 复制代码
// 标签页 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,跨域场景更麻烦
html 复制代码
<!-- 基本用法 -->
<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' -->
javascript 复制代码
// 父子页面通信(使用 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
url URL 地址 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 允许多个值(用于 emailfile
form 将外部控件关联到指定表单
novalidate(表单属性) 提交时不进行验证

新增表单元素:

元素 说明
<datalist> 输入建议列表(自动补全)
<output> 计算结果输出
<progress> 进度条
<meter> 度量衡(如磁盘用量)
html 复制代码
<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" />
javascript 复制代码
// 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(documentwindow
  • ❌ 无法使用 alert()confirm() 等 UI 相关 API
  • ✅ 可以使用 XMLHttpRequest / fetch 发送网络请求
  • ✅ 可以使用 setTimeout / setInterval
  • ✅ 可以使用 importScripts() 引入其他脚本
  • ✅ 可以使用 IndexedDB
javascript 复制代码
// =========== 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 键导航顺序 自定义可聚焦元素
html 复制代码
<!-- ✅ 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 的区别?

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

特性 Canvas SVG
类型 位图(像素级绘制) 矢量图(基于数学描述)
标准 HTML5 新增 W3C 标准(XML 格式)
缩放 放大后失真(像素化) 无限缩放不失真
DOM 节点 只有一个 <canvas> 元素 每个图形都是独立的 DOM 节点
事件绑定 需要手动计算坐标命中区域 可以直接绑定事件(onclick
CSS 样式 不支持 CSS 操纵图形 支持 CSS 修改样式
动画 需要手动逐帧重绘 支持 CSS 动画和 SMIL
性能 大量元素时性能更好(不增加 DOM) 元素过多时 DOM 操作开销大
文字渲染 较差(位图化) 优秀(保持可选中、可搜索)
无障碍 差(需额外处理) 好(支持 <title> <desc>
适用场景 游戏、图像处理、像素操作、大数据可视化 图标、Logo、图表、地图、交互式图形
javascript 复制代码
// =========== 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()
html 复制代码
<!-- =========== 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)
出发点 基础功能出发,逐步添加增强 完整功能出发,逐步做兼容
开发顺序 先低端 → 后高端 先高端 → 后低端
核心理念 保证所有用户都能用 保证最佳体验优先
适用场景 面向大众的网站(电商、政务) 面向特定用户的应用(管理后台)
html 复制代码
<!-- ✅ 渐进式增强示例 -->

<!-- 示例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 复制代码
/* ✅ 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 复制代码
// ✅ 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 合并小图标
html 复制代码
<!-- 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)可以通过为图片设置 widthheightaspect-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(获取用户位置)
拖放 API Drag and Drop API(原生拖拽)
历史管理 History APIpushState replaceState,SPA 路由基础)
文件操作 File API(读取本地文件)、FileReader
设备访问 deviceorientation(陀螺仪)、devicemotion(加速计)
性能监控 Performance APIrequestAnimationFrame
内容编辑 contenteditable 属性、document.execCommand()(已废弃)

HTML5 移除的元素:

移除的标签 替代方案
<font> CSS font-familycolor
<center> CSS text-align: center
<big> CSS font-size
<strike> / <s> CSS text-decoration: line-through
<frame> / <frameset> <iframe> 或 SPA
<applet> <object> 或 JavaScript
html 复制代码
<!-- 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 用户点击前进/后退按钮时触发
javascript 复制代码
// =========== 基本用法 ===========

// 添加新的历史记录(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'))
  }
})

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


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

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

HTTP vs WebSocket 对比:

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

WebSocket 的连接过程:

  1. 客户端发送 HTTP 请求,Upgrade: websocket 头请求协议升级
  2. 服务端返回 101 Switching Protocols,完成握手
  3. 双方通过同一个 TCP 连接双向通信
  4. 任一方发送关闭帧即可断开
javascript 复制代码
// =========== 客户端 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 超时快得多
javascript 复制代码
// ============ 完整的 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!' })
javascript 复制代码
// ============ 服务端心跳处理(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 Root Shadow Tree 的根节点
Shadow Boundary Shadow DOM 与普通 DOM 的边界(样式隔离边界)
javascript 复制代码
// =========== 创建 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)
html 复制代码
<!-- 使用自定义组件 -->
<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>
javascript 复制代码
// 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 复制代码
<!-- 在 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>
javascript 复制代码
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 复制代码
/* 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-prefetch DNS 预解析 第三方域名
prerender 预渲染整个页面 高概率跳转的页面
modulepreload 预加载 ES Module JS 模块
manifest PWA 应用清单 PWA
canonical 指定规范 URL(SEO 去重) 避免重复内容
alternate 替代版本(多语言/RSS) 国际化、RSS
noopener / noreferrer 安全相关 配合 target="_blank"
html 复制代码
<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>

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


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

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

html 复制代码
<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 目标区域 在目标区域释放时
html 复制代码
<!-- 可拖拽元素需设置 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>
javascript 复制代码
// =========== 看板拖拽示例 ===========

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):

css 复制代码
         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 加速)
html 复制代码
<!-- 优化关键渲染路径的最佳实践 -->
<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 的核心组成部分之一
html 复制代码
<!-- 定义模板 -->
<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>
javascript 复制代码
// =========== 使用模板渲染卡片 ===========

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 中使用)

javascript 复制代码
// =========== 完整的 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)
html 复制代码
<!-- 像使用原生标签一样使用自定义组件 -->
<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 + 轮询 同源 大量数据共享
javascript 复制代码
// =========== 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(跨站脚本)数据注入攻击 。它的核心思想是:白名单策略------只允许加载指定来源的资源

设置方式:

html 复制代码
<!-- 方式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-src JavaScript 来源 script-src 'self' https://cdn.example.com
style-src CSS 来源 style-src 'self' 'unsafe-inline'
img-src 图片来源 img-src 'self' data: https:
font-src 字体来源 font-src 'self' https://fonts.gstatic.com
connect-src Ajax/WebSocket 连接 connect-src 'self' https://api.example.com
frame-src iframe 来源 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> 标签的 URL base-uri 'self'
form-action 表单提交目标 form-action 'self'

常用关键词:

关键词 说明
'self' 当前域名
'none' 禁止所有来源
'unsafe-inline' 允许内联代码(尽量避免)
'unsafe-eval' 允许 eval()(尽量避免)
'nonce-xxx' 允许特定的内联脚本
'strict-dynamic' 信任由可信脚本动态添加的脚本
html 复制代码
<!-- 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-urireport-to 指令发送到指定端点,便于监控和调试。


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

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

alt 的重要作用:

  1. 无障碍 :屏幕阅读器会朗读 alt 文本,帮助视障用户理解图片内容
  2. SEO :搜索引擎通过 alt 理解图片含义,有助于图片搜索排名
  3. 降级体验 :图片加载失败或被禁用时,显示 alt 文本
  4. 语义化:明确描述图片的含义
html 复制代码
<!-- ✅ 正确使用 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 环境下使用。

javascript 复制代码
// =========== 基本用法 ===========

// 检测浏览器是否支持
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-8 Unicode 的可变长编码 1-4 字节 ✅ 几乎覆盖所有语言文字
UTF-16 Unicode 的定长/变长编码 2-4 字节 JavaScript 内部使用

为什么选 UTF-8?

优势 说明
国际化支持 覆盖 Unicode 全部字符(中文、日文、韩文、emoji 等)
向后兼容 ASCII 英文字母只占 1 字节,与 ASCII 完全兼容
节省空间 可变长编码,英文 1 字节,中文 3 字节,比 UTF-16 更紧凑
Web 标准 HTML5 规范默认推荐 UTF-8,W3C 和 WHATWG 强烈建议
避免乱码 统一编码避免不同系统、不同浏览器的编码差异导致乱码
搜索引擎友好 搜索引擎对 UTF-8 页面的抓取和索引更稳定
html 复制代码
<!-- ========== 正确声明 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 -->
javascript 复制代码
// 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>

核心区别

html 复制代码
<!-- ========== 置换元素的特点 ========== -->

<!-- 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>  <!-- 显示为 #前端 -->
javascript 复制代码
// ========== 判断是否为置换元素 ==========
// 置换元素的 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'(由内容撑开)

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


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

对比项 GET POST
参数位置 URL 查询字符串(?key=value&... 请求体(Request Body)
安全性 ❌ 参数暴露在 URL 中,会被浏览器历史记录保存 ✅ 参数在请求体中,相对更安全
数据大小 有限制(通常 2KB ~ 8KB,取决于浏览器和服务器) 理论上无限制(实际受服务器配置限制)
幂等性 ✅ 幂等(多次相同请求结果一致) ❌ 非幂等(可能产生副作用)
缓存 ✅ 会被浏览器缓存 ❌ 不会被缓存
书签 ✅ 可以保存为书签 ❌ 不能保存为书签
回退/刷新 无副作用(再次发送请求) 浏览器会提示"是否重新提交表单"
编码类型 application/x-www-form-urlencoded 支持多种(form-datajson 等)
适用场景 查询、搜索、获取数据 提交表单、上传文件、修改数据
html 复制代码
<!-- ========== 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 复制代码
// ========== 用 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 层面的区别

vbnet 复制代码
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> 会执行多次 同一模块只执行一次(缓存)
顶层 this window undefined
import/export ❌ 不支持 ✅ 支持
CORS 同域脚本不需要 始终使用 CORS(即使同域)
html 复制代码
<!-- ========== 基本使用 ========== -->

<!-- 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>
javascript 复制代码
// ========== 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)
javascript 复制代码
// ========== 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'):

html 复制代码
<!-- 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 的关系:

xml 复制代码
传统 <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 结构

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)展示你的页面:

html 复制代码
<!-- 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 等平台分享时的标题、图片、描述:

html 复制代码
<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 要点

html 复制代码
<!-- ✅ 图片优化 -->
<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 相关的配置文件:

xml 复制代码
// 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 及其依赖 模块化应用的关键模块
html 复制代码
<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 属性(资源优先级微调):

html 复制代码
<!-- 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 的懒加载与解码:

html 复制代码
<!-- 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 状态码):

arduino 复制代码
// 传统流程:
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 可能已经下载完了!)
javascript 复制代码
// 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 })
})

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

html 复制代码
<!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>

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


相关推荐
tech_zjf1 小时前
当 AI 把 Next.js Route 越写越快:我为什么做了 next-route-kit
前端·后端
常宇佳1 小时前
vue3 @代指src路径设置
前端·typescript·vue
LayZhangStrive1 小时前
融360 一面
java·面试·后端开发
砚凝霜1 小时前
软考网络工程师|案例分析:Eth‑Trunk 链路聚合、iStack 堆叠、CSS 集群核心考点总结
前端·css·网络
珐恩AI-人工智能2 小时前
大模型意图召回偏差分析:GEO如何解决“有收录却不触发问答曝光”的难题
大数据·前端·人工智能·html·流量运营·geo优化
城管不管2 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
程序员老赵2 小时前
Docker 部署禅道 ZenTao:轻松搭建研发项目管理平台
前端·后端·github
前端小卡拉2 小时前
用了大半年 AI 编程工具,我才搞懂 Skill 到底是什么
前端·javascript
hunterandroid2 小时前
[鸿蒙从零到一] HarmonyOS 单元测试与 UI 自动化测试实战:从代码质量到用户体验的全链路保障
前端
郭邯2 小时前
用 Intl.DateTimeFormat 手写一个时区转换器,顺便聊聊我和 AI 结对编程的日常
前端·javascript