语义结构必须在交互逻辑介入前锁定,HTML骨架不可依赖JS动态补全。例如商品列表需在初始HTML中用<main><section><article><nav>等标签构建结构,而非空<div>由JS填充,否则导致屏幕阅读器漏读、SEO失效、SSR失败、首次渲染空白、aria-live无法声明、预加载器失灵及事件委托失控。

语义结构必须在交互逻辑介入前就锁定
交互越复杂,HTML骨架越不能靠JS动态补全。比如一个带筛选、拖拽、分页的商品列表,<main>里嵌套的<section>、<article>、<nav>必须在初始HTML中就存在,而不是等JS加载后用innerHTML拼出来。否则屏幕阅读器会漏读、SEO无法索引、服务端渲染(SSR)直接失效。
常见错误是把整个列表区域写成一个空<div id="product-list"></div>,再由JS填充。这会导致:
- 首次渲染空白,用户等待时间感知变长
-
aria-live区域无法提前声明,动态更新时辅助技术无响应 - 浏览器预加载器无法提前发现图片、链接资源
事件委托必须绑定到语义容器,而非document
动态增删的交互元素(如可删除的待办项、可拖拽的卡片),监听事件不能直接绑在每个子节点上,但也不能一股脑绑到document——那样会干扰其他模块的事件流,且性能不可控。
正确做法是找一个稳定的、语义明确的父容器:
立即学习“前端免费学习笔记(深入)”;
- 用
<ul class="todo-list">承载任务项,事件委托就绑在它身上 - 避免用
<div class="container">这种无语义包装器,它无法表达内容意图 - 如果父容器本身是动态插入的,确保它有唯一且语义化的
id或data-*属性,比如data-role="task-list"
自定义元素(Custom Elements)不是“炫技”,而是隔离复杂度的刚需
当一个交互组件包含状态管理、样式封装、外部API调用(比如搜索建议下拉、实时价格计算器),硬塞进普通<div>里会让HTML变成“JS配置文件”。这时候customElements.define()不是可选项,而是工程止损点。
关键约束条件:
- 必须继承
HTMLElement,不能只用class模拟——否则无法被querySelector原生识别 - Shadow DOM启用前,先确认是否真需要样式隔离;很多场景用
:host+ CSS变量更轻量 - 避免在
connectedCallback里做重操作(如fetch),改用requestIdleCallback或async延迟加载
微交互的HTML钩子必须可预测、不可覆盖
一个按钮点击后显示加载态、成功后变绿色、失败后抖动——这些状态变化不该靠JS反复classList.add/remove一堆临时类名,而应由HTML提供稳定锚点。
推荐模式:
- 所有交互元素默认带
data-state="idle",JS只改这个值,CSS用[data-state="loading"]匹配 - 禁用状态统一用
disabled属性(<button disabled>),而非仅靠opacity: 0.5和pointer-events: none - 动画触发不依赖JS添加
animate类,而用animationend事件监听原生动画完成,避免JS与CSS时序错乱
最易被忽略的是:复杂交互中,HTML不再是“一次写完”,而是持续参与状态流转的参与者。它的质量不体现在多漂亮,而在于能否让JS少猜、让CSS少修、让无障碍工具少绕路。



















