data-* 自定义数据属性
data-* 自定义数据属性允许在 HTML 元素上存储额外的自定义数据,这些数据不会影响元素的渲染,但可以通过 JavaScript 的 dataset API 进行读写。data-* 属性是前端开发中实现数据驱动视图、组件状态管理以及 CSS 状态控制的常用手段。
前置知识
阅读本节前,建议先了解:spellcheck 拼写检查
基础概念
什么是 data-* 属性
data-* 是 HTML5 引入的全局属性,允许开发者在任意 HTML 元素上存储自定义的、仅供内部使用的数据。这些数据不会对页面渲染产生任何影响,浏览器会自动忽略它们。
html
<!-- data-* 属性的基本格式 -->
<div data-user-id="12345" data-role="admin" data-last-login="2024-01-15">
张三的管理面板
</div>data-* 属性的命名规则:
| 规则 | 说明 | 正确示例 | 错误示例 |
|---|---|---|---|
前缀必须为 data- | 区分标准属性 | data-name | name, my-name |
| 至少包含一个字符 | data- 后不能为空 | data-x | data- |
| 只能用小写字母 | 大小写不敏感但约定小写 | data-userId | data-UserID(浏览器会转为小写) |
| 不能包含 XML 冒号 | 避免与 XML 命名空间冲突 | data-my-prop | data-my:prop |
| 连字符分隔 | 多词使用连字符 | data-user-name | data_userName(CSS 属性选择器) |
dataset API
浏览器会将 data-* 属性映射到元素的 dataset DOMStringMap 对象上。转换规则:
data-前缀被移除- 连字符
-转为驼峰命名(camelCase) - 属性值始终是字符串类型
html
<!-- HTML 中的 data-* 属性 -->
<div id="user"
data-user-id="1001"
data-first-name="张三"
data-is-active="true"
data-scores="95,87,92">
</div>
<script>
const userEl = document.getElementById('user');
// 读取 data-* 属性
console.log(userEl.dataset.userId); // "1001"
console.log(userEl.dataset.firstName); // "张三"
console.log(userEl.dataset.isActive); // "true"(注意:字符串,不是布尔值)
console.log(userEl.dataset.scores); // "95,87,92"
// 设置 data-* 属性
userEl.dataset.lastName = '李四'; // 添加 data-last-name
userEl.dataset.isActive = 'false'; // 修改 data-is-active
// 删除 data-* 属性
delete userEl.dataset.scores; // 删除 data-scores
</script>转换对照表
| HTML 属性 | dataset 键名 |
|---|---|
data-id | dataset.id |
data-user-name | dataset.userName |
data-is-active | dataset.isActive |
data-max-score | dataset.maxScore |
data-x-http-header | dataset.xHttpHeader |
语法
HTML 中的用法
html
<!-- 在各种元素上使用 data-* -->
<li data-index="0" data-category="tech">文章标题</li>
<button data-action="delete" data-confirm="确定删除吗?">
删除
</button>
<tr data-record-id="A001" data-status="active">
<td>客户信息</td>
</tr>
<img src="photo.jpg" data-src="hd-photo.jpg" data-caption="风景照" alt="风景">CSS 中的用法
css
/* 使用属性选择器匹配 data-* */
[data-role="admin"] {
border: 2px solid gold;
}
/* data-* 属性值作为 CSS 变量 */
.card[data-theme="dark"] {
--bg: #1a1a2e;
--fg: #eaeaea;
background-color: var(--bg);
color: var(--fg);
}
/* data-state 控制样式 */
[data-state="open"] .panel {
max-height: 500px;
}
[data-state="closed"] .panel {
max-height: 0;
}JavaScript 中的用法
javascript
// 创建 data-* 属性
element.dataset.newAttr = 'value';
// 读取
const val = element.dataset.newAttr;
// 修改
element.dataset.newAttr = 'newValue';
// 删除
delete element.dataset.newAttr;
// 检查是否存在
if ('newAttr' in element.dataset) {
// 属性存在
}
// 获取所有 data-* 属性名
const keys = Object.keys(element.dataset);详细说明
数据类型处理
data-* 属性值始终是字符串,需要进行类型转换:
javascript
const card = document.querySelector('.card');
// 字符串 - 直接使用
const name = card.dataset.name;
// 数字 - 使用 Number() 或 parseInt()
const id = Number(card.dataset.id);
const max = parseInt(card.dataset.maxItems, 10);
// 布尔值 - 使用严格比较
const isActive = card.dataset.active === 'true';
// JSON 数据 - 使用 JSON.parse()
try {
const config = JSON.parse(card.dataset.config);
console.log(config.theme, config.layout);
} catch (e) {
console.error('解析 JSON 失败:', e);
}
// 数组 - 使用 split() 或 JSON.parse()
const tags = card.dataset.tags.split(',');
const items = JSON.parse(card.dataset.items);惰性加载(Lazy Loading)模式
data-* 常用于实现图片和内容的惰性加载:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>惰性加载示例</title>
<style>
img[data-src] {
opacity: 0;
transition: opacity 0.3s ease;
}
img.loaded {
opacity: 1;
}
</style>
</head>
<body>
<!-- 使用 data-src 保存真实图片地址 -->
<img class="lazy-img"
src="placeholder.svg"
data-src="https://picsum.photos/800/600"
data-srcset="https://picsum.photos/400/300 400w,
https://picsum.photos/800/600 800w"
alt="风景照片">
<script>
// IntersectionObserver 实现惰性加载
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
}, { rootMargin: '100px' });
lazyImages.forEach(img => imageObserver.observe(img));
</script>
</body>
</html>配置驱动组件
使用 data-* 属性驱动组件行为,避免为每种配置编写不同的 JavaScript:
html
<!-- 通过 data-* 配置行为 -->
<button class="confirm-btn"
data-message="确定要删除此记录吗?"
data-confirm-text="确定"
data-cancel-text="取消"
data-danger="true">
删除记录
</button>
<button class="confirm-btn"
data-message="确定要退出编辑吗?未保存的更改将丢失。"
data-confirm-text="保存并退出"
data-cancel-text="继续编辑"
data-danger="false">
退出编辑
</button>
<script>
document.querySelectorAll('.confirm-btn').forEach(btn => {
btn.addEventListener('click', () => {
const message = btn.dataset.message || '确定执行此操作吗?';
const confirmText = btn.dataset.confirmText || '确定';
const cancelText = btn.dataset.cancelText || '取消';
const isDanger = btn.dataset.danger === 'true';
// 创建确认弹窗
const confirmed = createConfirmDialog({
message,
confirmText,
cancelText,
danger: isDanger
});
if (confirmed) {
executeAction(btn);
}
});
});
</script>CSS 状态驱动
使用 data-* 属性控制 CSS 状态,实现纯数据驱动的样式切换:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CSS 状态驱动示例</title>
<style>
/* 状态切换动画 */
.toggle-card {
--duration: 0.3s;
padding: 20px;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all var(--duration) ease;
}
/* 使用 data-state 控制样式 */
.toggle-card[data-state="expanded"] {
grid-column: span 2;
background: #f0f7ff;
}
.toggle-card[data-state="expanded"] .detail-content {
display: block;
}
.toggle-card[data-state="collapsed"] .detail-content {
display: none;
}
/* 使用 data-color 控制主题 */
.theme-card[data-theme="blue"] {
--primary: #2563eb;
--primary-light: #dbeafe;
}
.theme-card[data-theme="green"] {
--primary: #16a34a;
--primary-light: #dcfce7;
}
.theme-card[data-theme="red"] {
--primary: #dc2626;
--primary-light: #fee2e2;
}
.theme-card {
border-left: 4px solid var(--primary);
background: var(--primary-light);
}
/* 进度指示器 */
.progress[data-value] {
--value: attr(data-value); /* 实际需 JS 配合 */
}
</style>
</head>
<body>
<div class="toggle-card" data-state="expanded">
<h3>可展开卡片</h3>
<div class="detail-content">
<p>这是展开后显示的详细内容...</p>
</div>
</div>
<div class="theme-card" data-theme="blue">
<h4>蓝色主题</h4>
</div>
<script>
// 切换 data-state
document.querySelectorAll('.toggle-card').forEach(card => {
card.addEventListener('click', () => {
const state = card.dataset.state === 'expanded' ? 'collapsed' : 'expanded';
card.dataset.state = state;
});
});
</script>
</body>
</html>排序与筛选
data-* 属性非常适合在表格和列表中实现客户端排序与筛选:
html
<table id="product-table">
<thead>
<tr>
<th data-sort="name">产品名称</th>
<th data-sort="price" data-type="number">价格</th>
<th data-sort="stock" data-type="number">库存</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr data-name="笔记本电脑" data-price="5999" data-stock="23">
<td>笔记本电脑</td>
<td>¥5,999</td>
<td>23</td>
</tr>
<tr data-name="无线鼠标" data-price="129" data-stock="156">
<td>无线鼠标</td>
<td>¥129</td>
<td>156</td>
</tr>
<tr data-name="机械键盘" data-price="459" data-stock="67">
<td>机械键盘</td>
<td>¥459</td>
<td>67</td>
</tr>
</tbody>
</table>
<script>
const table = document.getElementById('product-table');
const rows = Array.from(table.querySelectorAll('tbody tr'));
// 排序功能
table.querySelector('thead').addEventListener('click', (e) => {
const th = e.target.closest('[data-sort]');
if (!th) return;
const key = th.dataset.sort;
const isNumeric = th.dataset.type === 'number';
const direction = th.dataset.order === 'asc' ? -1 : 1;
rows.sort((a, b) => {
let valA = a.dataset[key];
let valB = b.dataset[key];
if (isNumeric) {
valA = Number(valA);
valB = Number(valB);
}
return valA > valB ? direction : -direction;
});
// 重新插入排序后的行
rows.forEach(row => table.querySelector('tbody').appendChild(row));
th.dataset.order = th.dataset.order === 'asc' ? 'desc' : 'asc';
});
</script>实战示例
完整的标签页组件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Data-* 标签页组件</title>
<style>
/* 标签页容器 */
.tabs {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
/* 标签按钮 */
.tab-btn {
padding: 10px 20px;
border: none;
background: #f8fafc;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn[data-active="true"] {
background: #3b82f6;
color: white;
font-weight: 600;
}
/* 面板 - 通过 data-tab 匹配 */
.tab-panel {
display: none;
padding: 20px;
}
.tab-panel[data-visible="true"] {
display: block;
}
</style>
</head>
<body>
<div class="tabs" data-default-tab="overview">
<div class="tab-header">
<button class="tab-btn" data-tab="overview" data-active="true">概述</button>
<button class="tab-btn" data-tab="features" data-active="false">特性</button>
<button class="tab-btn" data-tab="docs" data-active="false">文档</button>
</div>
<div class="tab-panel" data-tab="overview" data-visible="true">
<h2>组件概述</h2>
<p>这是一个使用 data-* 属性驱动的标签页组件。</p>
</div>
<div class="tab-panel" data-tab="features" data-visible="false">
<h2>特性列表</h2>
<ul>
<li>支持多个标签页</li>
<li>支持默认激活标签</li>
<li>支持键盘导航</li>
</ul>
</div>
<div class="tab-panel" data-tab="docs" data-visible="false">
<h2>使用文档</h2>
<p>在 HTML 中使用 data-tab 属性标识标签页。</p>
</div>
</div>
<script>
class DataTabs {
constructor(container) {
this.container = container;
this.buttons = container.querySelectorAll('.tab-btn[data-tab]');
this.panels = container.querySelectorAll('.tab-panel[data-tab]');
this.defaultTab = container.dataset.defaultTab;
this.init();
}
init() {
// 绑定点击事件
this.buttons.forEach(btn => {
btn.addEventListener('click', () => this.switchTab(btn.dataset.tab));
});
// 键盘导航
this.container.addEventListener('keydown', (e) => {
const buttons = Array.from(this.buttons);
const currentIndex = buttons.indexOf(document.activeElement);
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
e.preventDefault();
const next = e.key === 'ArrowRight'
? (currentIndex + 1) % buttons.length
: (currentIndex - 1 + buttons.length) % buttons.length;
buttons[next].focus();
buttons[next].click();
}
});
}
switchTab(tabId) {
// 更新按钮状态
this.buttons.forEach(btn => {
btn.dataset.active = btn.dataset.tab === tabId ? 'true' : 'false';
btn.setAttribute('aria-selected', btn.dataset.tab === tabId);
});
// 更新面板可见性
this.panels.forEach(panel => {
panel.dataset.visible = panel.dataset.tab === tabId ? 'true' : 'false';
});
}
}
// 初始化
document.querySelectorAll('.tabs').forEach(tabs => new DataTabs(tabs));
</script>
</body>
</html>注意事项
避免存储敏感数据
data-* 属性对任何能查看 HTML 源代码的人都是可见的:
html
<!-- 错误:不要在 data-* 中存储敏感信息 -->
<div data-user-password="secret123" data-api-key="sk-abc123">
用户面板
</div>
<!-- 正确:敏感数据通过安全渠道传输 -->
<div data-user-id="1001">
用户面板
</div>不要过度使用
data-* 属性应该用于存储少量、简单的自定义数据。如果数据量大或结构复杂,应考虑其他方案:
| 场景 | 推荐方案 |
|---|---|
| 简单配置值 | data-* 属性 |
| 结构化配置对象 | <script type="application/json"> |
| 大量数据 | JavaScript 对象 / API 请求 |
| 状态管理 | 状态管理库(如 Pinia) |
| 组件 props | Web Components / 框架 props |
性能考虑
javascript
// 较慢:频繁读写 dataset
for (let i = 0; i < 1000; i++) {
element.dataset.counter = String(i);
}
// 较快:使用变量缓存,最后一次性写入
let counter = 0;
for (let i = 0; i < 1000; i++) {
counter = i;
}
element.dataset.counter = String(counter);最佳实践
命名规范
html
<!-- 推荐:使用有意义的、符合 BEM 风格的命名 -->
<div
data-component="modal"
data-modal-id="delete-confirm"
data-modal-size="large"
data-modal-closable="true"
>
<div data-modal-role="header">确认删除</div>
<div data-modal-role="body">此操作不可撤销</div>
<div data-modal-role="footer">
<button data-modal-action="confirm">确定</button>
<button data-modal-action="cancel">取消</button>
</div>
</div>
<!-- 避免:过于简短或无意义的命名 -->
<div data-x="1" data-y="abc" data-z="true">
...
</div>与 ARIA 属性配合
html
<!-- data-* 驱动行为 + ARIA 表达语义 -->
<div
role="tablist"
data-tabs="main-nav"
aria-label="主导航标签页"
>
<button
role="tab"
data-tab="home"
data-active="true"
aria-selected="true"
aria-controls="panel-home"
id="tab-home"
>
首页
</button>
<button
role="tab"
data-tab="about"
data-active="false"
aria-selected="false"
aria-controls="panel-about"
id="tab-about"
>
关于
</button>
</div>
<div
role="tabpanel"
data-tab="home"
data-visible="true"
aria-labelledby="tab-home"
id="panel-home"
>
首页内容
</div>下一节
继续学习:role 与 aria-* 无障碍