跳到主要内容

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 多格式 + 字幕
<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邮箱格式验证带 @ 校验
urlURL 格式验证带协议校验
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 联想

datalist 搜索建议
<input list="frameworks" placeholder="选择框架" />
<datalist id="frameworks">
<option value="React" />
<option value="Vue" />
<option value="Angular" />
<option value="Svelte" />
</datalist>
TypeScript 中读取表单值
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 基础结构
<canvas id="myCanvas" width="400" height="300"></canvas>
Canvas 2D 基础绘图
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 示例
<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>
特性CanvasSVG
渲染方式位图(像素)矢量(DOM 节点)
缩放失真不失真
事件绑定需手动计算坐标可直接绑定 DOM 事件
适用场景游戏、图片处理、大量粒子图标、图表、地图
性能元素多时更优DOM 节点多时会卡顿

6. Web Storage

HTML5 提供了 localStoragesessionStorage,替代了过去只能用 Cookie 进行客户端数据存储的局面。

Web Storage 基础用法
// localStorage - 持久存储,关闭浏览器不丢失
localStorage.setItem('theme', 'dark');
const theme: string | null = localStorage.getItem('theme');
localStorage.removeItem('theme');

// sessionStorage - 会话存储,关闭标签页即清除
sessionStorage.setItem('token', 'abc123');
特性CookielocalStoragesessionStorage
容量~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在目标区域释放放置区域
拖拽示例 HTML
<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 渲染。

使用 Web Worker
// 主线程
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 API 基本用法
// 添加新历史记录(不刷新页面)
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 读取。

data-* 属性
<div
id="user"
data-user-id="42"
data-role="admin"
data-full-name="张三"
>
用户信息
</div>
通过 dataset 读取
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')
React 中使用 data-* 属性
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">
contenteditable 富文本
<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 引用简化为一行:

HTML5 DOCTYPE
<!DOCTYPE html>

而 HTML4 的 DOCTYPE 需要引用外部 DTD:

HTML4 DOCTYPE(Strict)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
XHTML 1.0 DOCTYPE(Transitional)
<!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>机器可读的时间/日期

作用

  1. SEO 优化:搜索引擎能更准确地理解页面结构和内容权重
  2. 可访问性:屏幕阅读器可通过语义标签提供更好的导航体验(如按区域跳转)
  3. 代码可读性:开发者看到标签名就能理解内容含义,无需依赖 class 名
  4. 维护性:结构清晰,减少沟通成本

详细内容请参考 语义化与可访问性


Q2: HTML5 的 video 和 audio 标签有哪些常用属性?如何实现自定义播放器?

答案

常用属性一览

属性videoaudio说明
srcoo媒体源
controlsoo显示原生控制栏
autoplayoo自动播放(需配合 muted
loopoo循环播放
mutedoo默认静音
preloadoo预加载策略
posterox视频封面
width / heightox尺寸

自定义播放器思路

  1. 隐藏原生 controls,自行设计 UI
  2. 通过 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搜索框带清除按钮

好处

  1. 原生验证:浏览器自动校验格式(如 email 是否包含 @),不需要额外写正则
  2. 移动端体验:自动弹出与输入类型匹配的键盘,减少输入成本
  3. 无障碍支持:屏幕阅读器可识别输入类型,提供更好的辅助
  4. 减少依赖:很多场景不再需要引入第三方日期选择器、滑块组件
利用原生验证
<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 获取

dataset API
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 中的用法

React 中使用 data-*
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-boxwidth 不含 padding/border)border-boxwidth 包含 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 事件清洗
防止 XSS 和处理粘贴
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.jsProseMirrorTipTap 等,它们在 contenteditable 基础上解决了浏览器差异、命令管理、插件化等问题。


相关链接