数据属性与语义化 HTML

深入理解 data-* 属性的正确用法、dataset API、命名规范,以及语义化 HTML 的最佳实践。

数据属性与语义化 HTML(Data Attributes and Semantic HTML)

一、data-* 属性

1.1 基本用法

data-* 属性允许在 HTML 元素上存储额外的数据,供 JavaScript 读取和使用。

<div id="user" data-user-id="123" data-role="admin">
  张三
</div>
const element = document.getElementById('user');

// 读取 data 属性
console.log(element.dataset.userId);  // "123"
console.log(element.dataset.role);     // "admin"

// 设置 data 属性
element.dataset.userId = '456';
element.dataset.newAttr = 'value';

// 删除 data 属性
delete element.dataset.role;

1.2 命名转换规则

data-* 属性的命名在 dataset 对象中有特定的转换规则:

<!-- HTML: kebab-case -->
<div data-user-id="123" data-max-zoom-level="10">

<!-- JavaScript: camelCase -->
element.dataset.userId      // "123"
element.dataset.maxZoomLevel  // "10"

1.3 注意事项

// ❌ 不能直接用点运算符访问
element.dataset.user-id  // 语法错误

// ✅ 使用方括号或 camelCase
element.dataset['user-id']
element.dataset.userId

// ⚠️ 以数字开头的属性名会变化
<div data-1st-place="winner">
element.dataset['1stPlace']  // "winner"

二、data-* 的正确应用

2.1 存储结构化数据

<!-- 存储产品信息 -->
<div class="product"
     data-product-id="P001"
     data-price="299.00"
     data-currency="CNY"
     data-stock="100">
  <h3>产品名称</h3>
  <p>¥299.00</p>
</div>

<!-- 存储配置选项 -->
<button
  data-action="delete"
  data-item-id="123"
  data-confirm="确定要删除吗?">
  删除
</button>
// 产品卡片点击处理
document.querySelectorAll('.product').forEach(card => {
  card.addEventListener('click', () => {
    const { productId, price, currency, stock } = card.dataset;
    console.log(`Product ${productId}: ${currency}${price}, Stock: ${stock}`);
  });
});

// 确认删除
document.querySelector('[data-action="delete"]').addEventListener('click', (e) => {
  const { itemId, confirm: message } = e.target.dataset;
  if (confirm(message)) {
    deleteItem(itemId);
  }
});

2.2 组件状态存储

// 标签页组件
class TabComponent {
  constructor(container) {
    this.container = container;
    this.tabs = container.querySelectorAll('[data-tab]');
    this.panels = container.querySelectorAll('[data-tab-panel]');

    this.init();
  }

  init() {
    this.tabs.forEach(tab => {
      tab.addEventListener('click', () => {
        this.activate(tab.dataset.tab);
      });
    });
  }

  activate(tabId) {
    // 移除所有 active 状态
    this.tabs.forEach(t => delete t.dataset.active);
    this.panels.forEach(p => delete p.dataset.active);

    // 设置 active 状态
    this.container.querySelector(`[data-tab="${tabId}"]`).dataset.active = '';
    this.container.querySelector(`[data-tab-panel="${tabId}"]`).dataset.active = '';
  }
}

2.3 国际化文本存储

<button data-i18n-key="button.submit" data-i18n-params='{"name": "John"}'>
  Submit
</button>
// 获取翻译
function t(key, params = {}) {
  const element = document.querySelector(`[data-i18n-key="${key}"]`);
  let text = translations[key] || key;

  // 替换参数
  Object.entries(params).forEach(([k, v]) => {
    text = text.replace(`{${k}}`, v);
  });

  return text;
}

三、语义化 HTML

3.1 语义元素

<!-- 导航 -->
<nav aria-label="主导航">
  <ul>
    <li><a href="/">首页</a></li>
    <li><a href="/about">关于</a></li>
  </ul>
</nav>

<!-- 主要内容 -->
<main>
  <article>
    <header>
      <h1>文章标题</h1>
      <time datetime="2024-01-15">2024年1月15日</time>
    </header>
    <section>
      <p>正文内容...</p>
    </section>
    <footer>
      <address>联系作者</address>
    </footer>
  </article>
</main>

<!-- 侧边栏 -->
<aside>
  <nav aria-label="页面导航">
    <ul>
      <li><a href="#section1">第一节</a></li>
      <li><a href="#section2">第二节</a></li>
    </ul>
  </nav>
</aside>

<!-- 页脚 -->
<footer>
  <p>&copy; 2024 公司名称</p>
</footer>

3.2 语义化的好处

  1. 可访问性:屏幕阅读器可以正确解析页面结构
  2. SEO:搜索引擎更好地理解页面内容
  3. 代码可维护性:开发者更容易理解代码意图
  4. 跨设备兼容:不同设备可以正确渲染

3.3 常见语义错误

<!-- ❌ 错误:使用 div 做按钮 -->
<div onclick="submit()">提交</div>

<!-- ✅ 正确:使用 button -->
<button type="submit">提交</button>

<!-- ❌ 错误:标题层级跳跃 -->
<h1>一级标题</h1>
<h3>三级标题</h3>

<!-- ✅ 正确:连续层级 -->
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>

<!-- ❌ 错误:强调位置错误 -->
<p><div>重要信息</div></p>

<!-- ✅ 正确:正确嵌套 -->
<p><strong>重要信息</strong></p>

四、data-* 与其他存储方案对比

4.1 存储位置选择

方案 适用场景 容量 持久性
data-* 少量静态数据 无限制 DOM 存活期间
data-* + dataset 组件状态 无限制 DOM 存活期间
localStorage 需要持久化 ~5-10MB 永久(直到清除)
sessionStorage 当前会话 ~5-10MB 标签页关闭
JavaScript 变量 运行时数据 无限制 函数作用域

4.2 data-* 的限制

// ⚠️ data 属性不适合存储:
// 1. 敏感数据(用户可查看和修改)
const userId = element.dataset.userId;  // 任何人都可以在 DevTools 中看到

// 2. 大量数据(影响 DOM 性能)
element.dataset.hugeData = largeObject;  // data 属性应该是字符串

// 3. 需要频繁更新的数据(每次修改都触发重排)
// 应该使用 CSS 类或 JavaScript 变量

五、最佳实践

5.1 命名规范

<!-- ✅ 推荐:使用语义化的 kebab-case -->
<div data-user-profile data-loading-state>

<!-- ❌ 不推荐:过于简写 -->
<div data-up d-ls>

<!-- ✅ 推荐:带有前缀避免冲突 -->
<div data-modal-id="123" data-modal-visible="true">

5.2 组件模式

// 将组件逻辑封装
class Component {
  constructor(element) {
    this.element = element;
    this.config = {
      // 从 data 属性读取配置
      theme: element.dataset.theme || 'light',
      size: element.dataset.size || 'medium',
    };
    this.state = {
      // 运行时状态存储在 dataset 中
      active: false,
    };
  }

  updateState(key, value) {
    this.state[key] = value;
    this.element.dataset.state = JSON.stringify(this.state);
  }
}

参考资料

延展阅读