HTML5 新特性
问题
HTML5 相比 HTML4 有哪些新特性?
答案
HTML5 是 HTML 标准的第五个主要版本,于 2014 年由 W3C 正式推荐。相比 HTML4,HTML5 在语义化、多媒体、表单、图形绘制、客户端存储、离线能力等方面进行了大幅增强,同时大幅简化了文档声明。
HTML5 不仅是标签的升级,更带来了一整套 Web 平台 API(Canvas、Geolocation、Drag & Drop、Web Storage、Web Workers、History API 等),使浏览器从"文档阅读器"进化为"应用运行平台"。
1. 语义化标签
HTML5 新增了一批语义化标签,用于替代大量无意义的 <div> 嵌套,让文档结构更清晰,有利于 SEO 和可访问性。
| 标签 | 语义 |
|---|---|
<header> | 页头 / 区块头部 |
<nav> | 导航区域 |
<main> | 页面主内容(唯一) |
<article> | 独立完整内容 |
<section> | 主题性区块 |
<aside> | 侧边栏 / 附属内容 |
<footer> | 页脚 / 区块底部 |
<figure> / <figcaption> | 图片 / 图表及说明 |
<details> / <summary> | 可折叠的详情区域 |
<mark> | 高亮文本 |
<time> | 时间 / 日期 |
<header>
<nav>...</nav>
</header>
<main>
<article>
<section>...</section>
</article>
<aside>...</aside>
</main>
<footer>...</footer>
语义标签的详细用法、ARIA 属性以及可访问性最佳实践,请参考 语义化与可访问性。
2. 多媒体标签
HTML5 之前嵌入音视频需要依赖 Flash 或其他插件。HTML5 引入了原生的 <video> 和 <audio> 标签,浏览器内置解码器直接播放。
常用属性
| 属性 | 说明 |
|---|---|
src | 媒体文件地址 |
controls | 显示原生控制栏 |
autoplay | 自动播放(通常需配合 muted) |
loop | 循环播放 |
muted | 默认静音 |
poster | 视频封面图(仅 <video>) |
preload | 预加载策略:none / metadata / auto |
多源 + 字幕
<video controls width="640" poster="/cover.jpg">
<!-- 按优先级排列,浏览器选择第一个支持的格式 -->
<source src="/video.mp4" type="video/mp4" />
<source src="/video.webm" type="video/webm" />
<track kind="subtitles" src="/subs-zh.vtt" srclang="zh" label="中文" default />
<track kind="subtitles" src="/subs-en.vtt" srclang="en" label="English" />
<p>您的浏览器不支持 video 标签。</p>
</video>
通过 JS 操控播放器
const video = document.querySelector('video') as HTMLVideoElement;
// 播放 / 暂停
function togglePlay(): void {
if (video.paused) {
video.play();
} else {
video.pause();
}
}
// 监听事件
video.addEventListener('timeupdate', () => {
const progress = (video.currentTime / video.duration) * 100;
console.log(`播放进度: ${progress.toFixed(1)}%`);
});
video.addEventListener('ended', () => {
console.log('播放结束');
});
3. 表单增强
HTML5 为 <input> 新增了多种类型和属性,浏览器可提供原生校验和专用 UI(如日期选择器、颜色面板),减少对第三方库的依赖。
新增 input 类型
| 类型 | 说明 | 示例 UI |
|---|---|---|
email | 邮箱格式验证 | 带 @ 校验 |
url | URL 格式验证 | 带协议校验 |
number | 数字输入 | 上下箭头 |
range | 滑块 | 拖动条 |
date / datetime-local | 日期 / 日期时间 | 日历弹窗 |
time | 时间选择 | 时间弹窗 |
month / week | 月份 / 周选择 | 日历弹窗 |
color | 颜色选择 | 调色板 |
search | 搜索框 | 带清除按钮 |
tel | 电话号码 | 移动端弹出数字键盘 |
新增表单属性
| 属性 | 说明 |
|---|---|
placeholder | 占位提示文字 |
required | 必填校验 |
autofocus | 页面加载后自动聚焦 |
pattern | 正则验证(如 pattern="[0-9]{6}") |
autocomplete | 自动填充控制 |
min / max / step | 数值范围和步长 |
multiple | 允许多个值(如多文件上传) |
form | 关联表单(跨 DOM 提交) |
datalist 联想
<input list="frameworks" placeholder="选择框架" />
<datalist id="frameworks">
<option value="React" />
<option value="Vue" />
<option value="Angular" />
<option value="Svelte" />
</datalist>
const form = document.querySelector('form') as HTMLFormElement;
form.addEventListener('submit', (e: Event) => {
e.preventDefault();
const formData = new FormData(form);
const email = formData.get('email') as string;
const age = Number(formData.get('age'));
console.log({ email, age });
});
4. Canvas 绘图
<canvas> 提供了一块"画布",通过 JavaScript 进行 2D(或 WebGL 3D)绘图,适合游戏、数据可视化、图片编辑等场景。
<canvas id="myCanvas" width="400" height="300"></canvas>
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
// 矩形
ctx.fillStyle = '#4CAF50';
ctx.fillRect(10, 10, 100, 80);
// 圆形
ctx.beginPath();
ctx.arc(200, 50, 40, 0, Math.PI * 2);
ctx.fillStyle = '#2196F3';
ctx.fill();
// 文字
ctx.font = '20px Arial';
ctx.fillStyle = '#333';
ctx.fillText('Hello Canvas', 10, 140);
// 绘制图片
const img = new Image();
img.src = '/logo.png';
img.onload = () => {
ctx.drawImage(img, 10, 160, 100, 100);
};
Canvas 是位图绘制,缩放会失真;如需矢量图形和 DOM 事件支持,请使用 SVG。
5. SVG 内联
HTML5 允许在 HTML 文档中直接内联 SVG 标记,无需通过 <img> 或 <object> 引入。内联 SVG 可以用 CSS 控制样式、用 JavaScript 操作 DOM。
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" fill="#FF5722" />
<text x="50" y="55" text-anchor="middle" fill="white" font-size="14">SVG</text>
</svg>
| 特性 | Canvas | SVG |
|---|---|---|
| 渲染方式 | 位图(像素) | 矢量(DOM 节点) |
| 缩放 | 失真 | 不失真 |
| 事件绑定 | 需手动计算坐标 | 可直接绑定 DOM 事件 |
| 适用场景 | 游戏、图片处理、大量粒子 | 图标、图表、地图 |
| 性能 | 元素多时更优 | DOM 节点多时会卡顿 |
6. Web Storage
HTML5 提供了 localStorage 和 sessionStorage,替代了过去只能用 Cookie 进行客户端数据存储的局面。
// localStorage - 持久存储,关闭浏览器不丢失
localStorage.setItem('theme', 'dark');
const theme: string | null = localStorage.getItem('theme');
localStorage.removeItem('theme');
// sessionStorage - 会话存储,关闭标签页即清除
sessionStorage.setItem('token', 'abc123');
| 特性 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 容量 | ~4KB | ~5MB | ~5MB |
| 生命周期 | 可设过期时间 | 永久 | 标签页关闭清除 |
| 是否随请求发送 | 是 | 否 | 否 |
| 作用域 | 同源 + 路径 | 同源 | 同源 + 同标签页 |
Cookie、IndexedDB、Cache API 等完整的前端存储方案,请参考 前端存储技术。
7. Geolocation API
Geolocation API 允许网页获取用户的地理位置(需用户授权)。
interface Position {
coords: {
latitude: number;
longitude: number;
accuracy: number;
};
timestamp: number;
}
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position: GeolocationPosition) => {
console.log(`纬度: ${position.coords.latitude}`);
console.log(`经度: ${position.coords.longitude}`);
console.log(`精度: ${position.coords.accuracy}m`);
},
(error: GeolocationPositionError) => {
console.error(`获取位置失败: ${error.message}`);
},
{
enableHighAccuracy: true, // 高精度
timeout: 5000, // 超时时间
maximumAge: 0, // 不使用缓存
}
);
}
Geolocation API 要求页面必须在 HTTPS 环境下才能使用(localhost 除外),且需要用户明确授权。
8. Drag and Drop API
HTML5 原生支持拖拽操作,通过 draggable 属性和一系列拖拽事件实现。
核心事件
| 事件 | 触发时机 | 绑定对象 |
|---|---|---|
dragstart | 开始拖拽 | 被拖元素 |
drag | 拖拽过程中持续触发 | 被拖元素 |
dragend | 拖拽结束 | 被拖元素 |
dragenter | 拖入目标区域 | 放置区域 |
dragover | 在目标区域上方移动 | 放置区域 |
dragleave | 离开目标区域 | 放置区域 |
drop | 在目标区域释放 | 放置区域 |
<div id="item" draggable="true" style="width:100px;height:100px;background:#4CAF50;">
拖我
</div>
<div id="dropzone" style="width:300px;height:200px;border:2px dashed #ccc;margin-top:20px;">
放置区域
</div>
const item = document.getElementById('item') as HTMLDivElement;
const dropzone = document.getElementById('dropzone') as HTMLDivElement;
item.addEventListener('dragstart', (e: DragEvent) => {
e.dataTransfer?.setData('text/plain', item.id);
item.style.opacity = '0.5';
});
item.addEventListener('dragend', () => {
item.style.opacity = '1';
});
// 必须阻止 dragover 默认行为才能触发 drop
dropzone.addEventListener('dragover', (e: DragEvent) => {
e.preventDefault();
dropzone.style.borderColor = '#4CAF50';
});
dropzone.addEventListener('dragleave', () => {
dropzone.style.borderColor = '#ccc';
});
dropzone.addEventListener('drop', (e: DragEvent) => {
e.preventDefault();
const id = e.dataTransfer?.getData('text/plain');
if (id) {
const el = document.getElementById(id);
if (el) dropzone.appendChild(el);
}
dropzone.style.borderColor = '#ccc';
});
9. Web Workers
HTML5 引入了 Web Workers,允许在后台线程中运行 JavaScript,不阻塞主线程 UI 渲染。
// 主线程
const worker = new Worker('/heavy-task.js');
worker.postMessage({ data: [1, 2, 3, 4, 5] });
worker.onmessage = (e: MessageEvent) => {
console.log('Worker 结果:', e.data);
};
Worker、SharedWorker、Service Worker 的详细对比与缓存策略,请参考 Web Workers。
10. History API
HTML5 的 History API 允许在不刷新页面的情况下操作浏览器历史记录,是 SPA 前端路由的基础。
// 添加新历史记录(不刷新页面)
history.pushState({ page: 'about' }, '', '/about');
// 替换当前历史记录
history.replaceState({ page: 'home' }, '', '/home');
// 监听前进/后退
window.addEventListener('popstate', (e: PopStateEvent) => {
console.log('导航到:', e.state);
});
Hash 模式与 History 模式的对比、前端路由实现原理,请参考 History API 与前端路由。
11. data-* 自定义属性
HTML5 允许在元素上添加以 data- 为前缀的自定义属性,通过 JavaScript 的 dataset API 读取。
<div
id="user"
data-user-id="42"
data-role="admin"
data-full-name="张三"
>
用户信息
</div>
const el = document.getElementById('user') as HTMLElement;
// data-user-id -> dataset.userId(驼峰转换)
console.log(el.dataset.userId); // "42"
console.log(el.dataset.role); // "admin"
console.log(el.dataset.fullName); // "张三"
// 设置新值
el.dataset.score = '100';
// 等价于 el.setAttribute('data-score', '100')
function UserCard({ userId, role }: { userId: string; role: string }) {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const { userId, role } = e.currentTarget.dataset;
console.log(userId, role);
};
return (
<div data-user-id={userId} data-role={role} onClick={handleClick}>
点击查看
</div>
);
}
12. 新的全局属性
HTML5 新增了多个实用的全局属性:
| 属性 | 说明 | 示例 |
|---|---|---|
contenteditable | 使元素可编辑 | <div contenteditable="true"> |
hidden | 隐藏元素(语义化的 display: none) | <p hidden>隐藏内容</p> |
spellcheck | 启用/禁用拼写检查 | <textarea spellcheck="true"> |
translate | 标记是否应翻译 | <code translate="no"> |
tabindex | 控制 Tab 键导航顺序 | <div tabindex="0"> |
draggable | 使元素可拖拽 | <div draggable="true"> |
<div contenteditable="true" style="border:1px solid #ccc;padding:10px;min-height:100px;">
这里的内容可以直接编辑...
</div>
const editor = document.querySelector('[contenteditable]') as HTMLDivElement;
editor.addEventListener('input', () => {
console.log('HTML 内容:', editor.innerHTML);
console.log('纯文本:', editor.textContent);
});
13. DOCTYPE 简化
HTML5 将文档类型声明从冗长的 DTD 引用简化为一行:
<!DOCTYPE html>
而 HTML4 的 DOCTYPE 需要引用外部 DTD:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!DOCTYPE html> 的作用是告诉浏览器以标准模式(Standards Mode) 渲染页面。如果省略或写错 DOCTYPE,浏览器会进入怪异模式(Quirks Mode),导致盒模型、CSS 解析等行为与标准不一致。
HTML5 新特性全景总结
常见面试问题
Q1: HTML5 新增了哪些语义标签?有什么作用?
答案:
HTML5 新增的语义标签主要包括:
| 标签 | 作用 |
|---|---|
<header> | 页面或区块的头部,通常包含 logo、标题、导航 |
<nav> | 导航链接区域 |
<main> | 页面主内容区(整个页面只能有一个) |
<article> | 独立的、可复用的内容块(如一篇博客文章) |
<section> | 按主题分组的内容区域 |
<aside> | 与主内容相关但非核心的附属内容(侧边栏) |
<footer> | 页面或区块的底部,通常包含版权、联系信息 |
<figure> / <figcaption> | 独立的媒体内容及其说明 |
<details> / <summary> | 原生的折叠/展开交互 |
<mark> | 标记/高亮文本 |
<time> | 机器可读的时间/日期 |
作用:
- SEO 优化:搜索引擎能更准确地理解页面结构和内容权重
- 可访问性:屏幕阅读器可通过语义标签提供更好的导航体验(如按区域跳转)
- 代码可读性:开发者看到标签名就能理解内容含义,无需依赖 class 名
- 维护性:结构清晰,减少沟通成本
详细内容请参考 语义化与可访问性。
Q2: HTML5 的 video 和 audio 标签有哪些常用属性?如何实现自定义播放器?
答案:
常用属性一览:
| 属性 | video | audio | 说明 |
|---|---|---|---|
src | o | o | 媒体源 |
controls | o | o | 显示原生控制栏 |
autoplay | o | o | 自动播放(需配合 muted) |
loop | o | o | 循环播放 |
muted | o | o | 默认静音 |
preload | o | o | 预加载策略 |
poster | o | x | 视频封面 |
width / height | o | x | 尺寸 |
自定义播放器思路:
- 隐藏原生
controls,自行设计 UI - 通过 JS API 控制播放行为
const video = document.querySelector('video') as HTMLVideoElement;
const playBtn = document.getElementById('playBtn') as HTMLButtonElement;
const progressBar = document.getElementById('progress') as HTMLInputElement;
// 播放/暂停
playBtn.addEventListener('click', () => {
video.paused ? video.play() : video.pause();
});
// 进度条同步
video.addEventListener('timeupdate', () => {
progressBar.value = String((video.currentTime / video.duration) * 100);
});
// 拖动进度条
progressBar.addEventListener('input', () => {
video.currentTime = (Number(progressBar.value) / 100) * video.duration;
});
// 音量控制
function setVolume(volume: number): void {
video.volume = Math.max(0, Math.min(1, volume));
}
// 全屏
function toggleFullscreen(): void {
if (!document.fullscreenElement) {
video.requestFullscreen();
} else {
document.exitFullscreen();
}
}
Chrome 等浏览器限制了非静音的自动播放,autoplay 必须配合 muted 使用,否则会被浏览器阻止。
Q3: HTML5 新增了哪些表单 input 类型?有什么好处?
答案:
新增类型:
| 类型 | 用途 | 移动端优化 |
|---|---|---|
email | 邮箱输入 | 弹出带 @ 的键盘 |
url | 网址输入 | 弹出带 .com 的键盘 |
tel | 电话输入 | 弹出数字键盘 |
number | 数字输入 | 弹出数字键盘 + 上下箭头 |
range | 范围滑块 | 滑动条 UI |
date | 日期选择 | 原生日期选择器 |
datetime-local | 日期时间 | 原生日期时间选择器 |
time | 时间选择 | 原生时间选择器 |
month | 月份选择 | 原生月份选择器 |
week | 周选择 | 原生周选择器 |
color | 颜色选择 | 原生取色器 |
search | 搜索框 | 带清除按钮 |
好处:
- 原生验证:浏览器自动校验格式(如 email 是否包含 @),不需要额外写正则
- 移动端体验:自动弹出与输入类型匹配的键盘,减少输入成本
- 无障碍支持:屏幕阅读器可识别输入类型,提供更好的辅助
- 减少依赖:很多场景不再需要引入第三方日期选择器、滑块组件
<form>
<!-- 浏览器自动校验邮箱格式 -->
<input type="email" required placeholder="请输入邮箱" />
<!-- 正则验证6位数字验证码 -->
<input type="text" pattern="[0-9]{6}" placeholder="6位验证码" />
<!-- 数字范围限制 -->
<input type="number" min="0" max="100" step="1" />
<button type="submit">提交</button>
</form>
Q4: data-* 自定义属性怎么用?在 JS/React 中如何获取?
答案:
data-* 属性允许在 HTML 元素上存储自定义数据,不影响页面展示,也不会被浏览器特殊处理。
命名规则:
- HTML 中使用连字符命名:
data-user-id - JS 中通过
dataset对象以驼峰形式读取:dataset.userId
原生 JS 获取:
const el = document.querySelector('[data-user-id]') as HTMLElement;
// 读取(自动驼峰转换)
const userId: string | undefined = el.dataset.userId;
const role: string | undefined = el.dataset.role;
// 写入
el.dataset.active = 'true';
// DOM 上会生成 data-active="true"
// 删除
delete el.dataset.active;
// 也可以用 getAttribute/setAttribute
el.getAttribute('data-user-id');
el.setAttribute('data-score', '100');
React 中的用法:
function ProductList({ products }: { products: Array<{ id: string; name: string }> }) {
const handleClick = (e: React.MouseEvent<HTMLLIElement>) => {
const productId = e.currentTarget.dataset.productId;
console.log('点击了产品:', productId);
};
return (
<ul>
{products.map((p) => (
<li key={p.id} data-product-id={p.id} onClick={handleClick}>
{p.name}
</li>
))}
</ul>
);
}
- 需要传递数据给 JS 但不适合显示在页面上时使用
data-* - React 中如果只是组件内部的数据传递,优先用 props / state,而非
data-* data-*常用于测试标记(如data-testid)、埋点标记、CSS 选择器等场景
Q5: HTML5 的 Drag and Drop API 怎么使用?
答案:
HTML5 Drag and Drop(拖拽)API 的使用分为 3 步:
第一步:设置可拖拽
<div draggable="true" id="dragItem">拖拽我</div>
第二步:处理拖拽源事件
const dragItem = document.getElementById('dragItem') as HTMLElement;
dragItem.addEventListener('dragstart', (e: DragEvent) => {
// 设置传输的数据
e.dataTransfer!.setData('text/plain', dragItem.id);
// 设置拖拽效果
e.dataTransfer!.effectAllowed = 'move';
// 可选:设置拖拽时的半透明图像
dragItem.style.opacity = '0.4';
});
dragItem.addEventListener('dragend', () => {
dragItem.style.opacity = '1';
});
第三步:处理放置目标事件
const dropZone = document.getElementById('dropZone') as HTMLElement;
// 必须阻止 dragover 的默认行为,否则 drop 不会触发
dropZone.addEventListener('dragover', (e: DragEvent) => {
e.preventDefault();
e.dataTransfer!.dropEffect = 'move';
});
dropZone.addEventListener('drop', (e: DragEvent) => {
e.preventDefault();
const id = e.dataTransfer!.getData('text/plain');
const draggedEl = document.getElementById(id);
if (draggedEl) {
dropZone.appendChild(draggedEl);
}
});
完整的拖拽排序示例:
const list = document.getElementById('sortableList') as HTMLUListElement;
let draggedItem: HTMLElement | null = null;
list.addEventListener('dragstart', (e: DragEvent) => {
draggedItem = e.target as HTMLElement;
draggedItem.classList.add('dragging');
});
list.addEventListener('dragover', (e: DragEvent) => {
e.preventDefault();
const afterElement = getDragAfterElement(list, e.clientY);
if (draggedItem) {
if (afterElement) {
list.insertBefore(draggedItem, afterElement);
} else {
list.appendChild(draggedItem);
}
}
});
list.addEventListener('dragend', () => {
draggedItem?.classList.remove('dragging');
draggedItem = null;
});
function getDragAfterElement(container: HTMLElement, y: number): HTMLElement | null {
const items = [...container.querySelectorAll<HTMLElement>('li:not(.dragging)')];
return items.reduce<{ offset: number; element: HTMLElement | null }>(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
}
return closest;
},
{ offset: Number.NEGATIVE_INFINITY, element: null }
).element;
}
Q6: HTML5 的离线存储方案有哪些?
答案:
| 方案 | 容量 | 数据类型 | 同步/异步 | 生命周期 | 适用场景 |
|---|---|---|---|---|---|
localStorage | ~5MB | 字符串 | 同步 | 永久(手动清除) | 用户偏好、Token |
sessionStorage | ~5MB | 字符串 | 同步 | 标签页关闭 | 表单暂存、临时状态 |
IndexedDB | 几百MB+ | 结构化数据 | 异步 | 永久 | 大量离线数据、文件缓存 |
Cache API | 依浏览器 | Request/Response | 异步 | 永久 | Service Worker 离线缓存 |
// 简单键值 -> localStorage
localStorage.setItem('theme', 'dark');
// 大量结构化数据 -> IndexedDB
const request = indexedDB.open('myDB', 1);
// 网络请求缓存 -> Cache API(配合 Service Worker)
caches.open('v1').then((cache) => {
cache.add('/api/data');
});
各存储方案的详细对比、安全注意事项和最佳实践,请参考 前端存储技术。
Q7: <!DOCTYPE html> 的作用是什么?不写会怎样?
答案:
<!DOCTYPE html> 是一条文档类型声明,告诉浏览器使用哪种标准来解析文档。
作用:触发浏览器的标准模式(Standards Mode) 进行渲染。
不写 DOCTYPE 的后果:浏览器会进入怪异模式(Quirks Mode),表现如下:
| 特性 | 标准模式 | 怪异模式 |
|---|---|---|
| 盒模型 | content-box(width 不含 padding/border) | border-box(width 包含 padding/border) |
| 图片底部间隙 | inline 元素有基线对齐间隙 | 无间隙 |
| 百分比高度 | 需父元素有明确高度 | 可以不需要 |
| CSS 解析 | 严格按规范 | 兼容旧浏览器的非标准行为 |
console.log(document.compatMode);
// "CSS1Compat" -> 标准模式
// "BackCompat" -> 怪异模式
<!DOCTYPE html>不是 HTML 标签,而是一条给浏览器的指令- HTML5 的 DOCTYPE 不需要引用 DTD(HTML4 需要)
- 必须写在文档的第一行,前面不能有任何字符(包括空行和注释)
Q8: contenteditable 属性有什么用?有什么坑?
答案:
contenteditable 属性可以让任何 HTML 元素变为可编辑区域,用户可以直接在页面上输入和修改内容。
<div contenteditable="true">
这段文字可以直接编辑
</div>
应用场景:
- 富文本编辑器(如 Notion、语雀的编辑器底层就使用了 contenteditable)
- 内联编辑(点击文字直接修改)
- 所见即所得的 HTML 编辑
常见的坑:
| 问题 | 说明 |
|---|---|
| 浏览器差异 | 按回车生成的标签不同:Chrome 生成 <div>,Firefox 生成 <br>,Safari 生成 <div> |
| XSS 风险 | 用户可以粘贴任意 HTML,包括 <script> 或事件属性,必须做内容过滤 |
| 光标控制 | 程序化控制光标位置很复杂,需使用 Selection API |
| 撤销/重做 | 浏览器内置的 undo/redo 行为不可控,需要自行实现命令栈 |
| 输出不一致 | innerHTML 的输出在不同浏览器中结构不同 |
| 粘贴处理 | 从 Word/网页粘贴会带入大量脏样式,需要拦截 paste 事件清洗 |
const editor = document.querySelector('[contenteditable]') as HTMLDivElement;
// 拦截粘贴事件,只保留纯文本
editor.addEventListener('paste', (e: ClipboardEvent) => {
e.preventDefault();
const text = e.clipboardData?.getData('text/plain') ?? '';
// 使用 insertText 命令插入纯文本
document.execCommand('insertText', false, text);
});
// 获取编辑后的内容时做 HTML 过滤
function sanitizeHTML(html: string): string {
const div = document.createElement('div');
div.textContent = html; // textContent 会自动转义
return div.innerHTML;
}
在真实项目中,不建议直接基于 contenteditable 从零开发富文本编辑器。推荐使用成熟的方案如 Slate.js、ProseMirror、TipTap 等,它们在 contenteditable 基础上解决了浏览器差异、命令管理、插件化等问题。