HTML 高频面试题
1. DOCTYPE 的作用是什么?
<!DOCTYPE> 声明位于 HTML 文档的第一行,它不是 HTML 标签,而是一条指令,用于告知浏览器当前文档使用哪个 HTML 版本进行解析,从而决定渲染模式。
浏览器的三种渲染模式:
- 标准模式(Standards Mode):浏览器按照 W3C 标准规范渲染页面,盒模型、布局等行为可预测且一致
- 怪异模式(Quirks Mode) :浏览器模拟 IE5 等旧版浏览器行为来渲染页面。例如盒模型会采用 IE 盒模型(
width包含padding和border),这会导致不同浏览器下表现不一致 - 近乎标准模式(Almost Standards Mode):大部分行为与标准模式一致,仅在少数细节(如表格单元格中图片底部间隙的处理)与标准模式有差异
为什么 HTML5 的 DOCTYPE 这么简洁?
HTML5 不基于 SGML(标准通用标记语言),所以不需要引用 DTD(文档类型定义)。而 HTML4.01 基于 SGML,需要引用 DTD 来定义文档规则:
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>。语义化的好处:
- 对开发者友好:代码结构清晰、可读性强,团队协作更高效
- 对 SEO 友好:搜索引擎爬虫能更好地理解页面结构,提升搜索排名
- 对无障碍友好:屏幕阅读器可以正确识别页面区域,帮助视障用户导航
- 对浏览器友好 :部分标签有默认行为(如
<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>© 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 |
仅触发重绘(不触发回流)的属性: color、background-color、visibility、box-shadow、outline
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' // 触发一次回流
面试加分点 :现代浏览器有回流队列 优化机制,会将多次回流操作合并为一次。但当你读取布局属性(如
offsetWidth、scrollTop、getComputedStyle())时,浏览器为了返回准确值,会强制清空队列 ,立即执行回流。这就是为什么"读写分离"是最重要的优化策略。此外,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 页面,常见于嵌入第三方内容(地图、视频、支付页面等)。
优点:
- 内容隔离:iframe 内的 CSS 和 JS 与父页面互不影响,天然的沙箱环境
- 嵌入第三方内容:如 Google 地图、YouTube 视频、在线支付等
- 并行加载:iframe 中的资源可以与主页面并行下载
- 微前端方案:可以用于实现简单的微前端架构,不同团队独立开发和部署
缺点:
- 阻塞主页面 onload 事件 :iframe 加载完毕前,主页面的
window.onload不会触发 - 不利于 SEO:搜索引擎爬虫无法解析 iframe 中的内容
- 安全风险:可能被用于**点击劫持(Clickjacking)**攻击
- 导航混乱:iframe 内的导航不会反映在浏览器地址栏
- 性能开销:每个 iframe 都会创建独立的文档环境,消耗额外内存
- 响应式困难:iframe 内容难以自适应父容器尺寸
- 通信复杂 :父子页面通信需要
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 |
允许多个值(用于 email 和 file) |
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(
document、window) - ❌ 无法使用
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)。
核心实现方式:
- 使用语义化标签 :屏幕阅读器可以自动识别
<nav>、<main>、<button>等标签 - ARIA 属性:当语义化标签不够用时,使用 ARIA(Accessible Rich Internet Applications)属性增强可访问性
- 键盘可访问:确保所有交互都可以通过键盘完成(Tab 键导航、Enter/Space 激活)
- 足够的颜色对比度:文字与背景的对比度至少 4.5:1(WCAG AA 标准)
- 为媒体提供替代文本 :图片加
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)可以通过为图片设置width和height或aspect-ratio来优化。
16. HTML5 有哪些新特性?(综合概述)
HTML5 是 HTML 标准的第五次重大修订,于 2014 年正式发布。相比 HTML4,它不仅新增了大量 API 和标签,更重要的是推动了 Web 从"文档平台"向"应用平台"的转变。
HTML5 新特性一览:
| 分类 | 新增内容 |
|---|---|
| 语义化标签 | <header> <footer> <nav> <main> <article> <section> <aside> <figure> <details> 等 |
| 多媒体 | <audio> <video> 原生音视频支持,无需 Flash |
| 图形绘制 | <canvas> 2D/WebGL 绘图、<svg> 内联矢量图 |
| 表单增强 | 新输入类型(email date range color 等)、原生表单验证 |
| 本地存储 | localStorage sessionStorage IndexedDB |
| 离线与缓存 | Service Worker、Cache API(替代已废弃的 Application Cache) |
| 网络通信 | WebSocket(全双工通信)、Server-Sent Events(服务器推送) |
| 多线程 | Web Workers(后台线程计算) |
| 地理定位 | Geolocation API(获取用户位置) |
| 拖放 API | Drag and Drop API(原生拖拽) |
| 历史管理 | History API(pushState replaceState,SPA 路由基础) |
| 文件操作 | File API(读取本地文件)、FileReader |
| 设备访问 | deviceorientation(陀螺仪)、devicemotion(加速计) |
| 性能监控 | Performance API、requestAnimationFrame |
| 内容编辑 | contenteditable 属性、document.execCommand()(已废弃) |
HTML5 移除的元素:
| 移除的标签 | 替代方案 |
|---|---|
<font> |
CSS font-family、color |
<center> |
CSS text-align: center |
<big> |
CSS font-size |
<strike> / <s> |
CSS text-decoration: line-through |
<frame> / <frameset> |
<iframe> 或 SPA |
<applet> |
<object> 或 JavaScript |
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'))
}
})
面试加分点 :
pushState和replaceState的区别------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 的连接过程:
- 客户端发送 HTTP 请求,
Upgrade: websocket头请求协议升级 - 服务端返回
101 Switching Protocols,完成握手 - 双方通过同一个 TCP 连接双向通信
- 任一方发送关闭帧即可断开
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),无法快速检测 |
💡 生产环境最佳实践:
- 心跳间隔建议 20~30s(小于大多数代理/防火墙超时时间 60s)
- 使用指数退避重连,避免服务器重启时大量客户端同时重连("惊群效应")
- 重连时加入随机抖动(jitter) :
delay * (0.5 + Math.random() * 0.5)- 页面不可见时(
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>
面试加分点 :
preload和prefetch的区别------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)
将图层合成为最终页面
详细步骤:
-
解析 HTML,构建 DOM 树
- 浏览器逐字节读取 HTML,经过字节 → 字符 → Token → 节点 → DOM 树的过程
- 解析过程中遇到
<script>(无 defer/async)会暂停 DOM 构建
-
解析 CSS,构建 CSSOM 树
- 解析所有 CSS(外部样式表、
<style>标签、内联样式) - CSS 解析不阻塞 DOM 构建,但阻塞渲染树的生成
- CSS 也阻塞 JS 执行(因为 JS 可能需要读取样式信息)
- 解析所有 CSS(外部样式表、
-
合并 DOM 和 CSSOM,生成渲染树
- 渲染树只包含可见元素 (
display: none的元素不在渲染树中) visibility: hidden的元素在渲染树中(占据空间)
- 渲染树只包含可见元素 (
-
布局(Layout)
- 计算每个元素的确切位置和大小(盒模型)
-
绘制(Paint)
- 将元素转化为屏幕上的实际像素
-
合成(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-uri或report-to指令发送到指定端点,便于监控和调试。
29. <img> 标签的 alt 和 title 属性有什么区别?
| 属性 | 作用 | 显示时机 | 是否必需 |
|---|---|---|---|
alt |
替代文本(图片无法显示时的替代描述) | 图片加载失败时显示;屏幕阅读器朗读 | ✅ 推荐始终添加 |
title |
提示文本(鼠标悬停时的工具提示) | 鼠标悬停时显示为 tooltip | ❌ 可选 |
alt 的重要作用:
- 无障碍 :屏幕阅读器会朗读
alt文本,帮助视障用户理解图片内容 - SEO :搜索引擎通过
alt理解图片含义,有助于图片搜索排名 - 降级体验 :图片加载失败或被禁用时,显示
alt文本 - 语义化:明确描述图片的含义
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'(由内容撑开)
💡 面试加分点: 置换元素最重要的特征是------行内置换元素可以设置
width和height(如<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-data、json 等) |
| 适用场景 | 查询、搜索、获取数据 | 提交表单、上传文件、修改数据 |
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>
💡 面试加分点 :
preload和prefetch的本质区别:preload 是命令式 的("我现在就需要"),prefetch 是建议式 的("我等会儿可能需要")。滥用 preload 反而会降低性能(抢占带宽)。fetchpriority是 Chrome 101+ 支持的新属性,可以精细控制 Fetch API、<img>、<script>、<link>的请求优先级,对优化 LCP 特别有效。Early Hints(103)需要服务器和 CDN 支持(Cloudflare 已支持),可以节省服务端处理时间期间的网络空闲等待。