必须使用 customelements.define() 创建 web component 才能实现真正可用的自定义评分组件;纯 html+css 无法复用、无法响应属性变更、不支持无障碍与表单集成,且不能实现 value 双向同步和动态 name/id 隔离。

纯 HTML + CSS 无法实现真正可用的自定义评分组件,customElements.define() 是必须的起点;不封装成 Web Component 的“自定义”只是样式换皮,无法复用、无法隔离状态、无法响应属性变更。
为什么不能只靠 class + CSS 实现「自定义」
所谓“自定义”,核心是可复用、可配置、可嵌入任意上下文。用 .star-rating 类写死在页面里,改一颗星的颜色就得全局搜替换;想让某处显示 3 颗星、另一处显示 10 颗星?得写两套 CSS 和两套 HTML 结构。更关键的是:无法通过 <my-rating max="7" value="4.5"></my-rating> 这种方式声明式控制——这正是 Web Component 提供的语义能力。
- 直接用
div+span模拟星星,tab键无法聚焦、screen reader读不出当前值、表单提交时拿不到数据 - 所有 radio 共享同一个
name,表格里多行评分会互相干扰,除非手动加唯一前缀(但这就不是“自定义组件”,而是拼字符串) - CSS 无法响应
value属性变化,比如 JS 动态设置el.value = 3.5,纯 CSS 没法触发半星渲染
customElements.define() 必须接管 DOM 构建
不能把整段含 input[type="radio"] 的 HTML 当作模板字符串塞进去就完事。Web Component 必须在 connectedCallback 中动态生成真实表单控件,并确保每个实例的 name 唯一、id 不冲突、for 关联准确。
- 用
Math.random().toString(36).slice(2)或globalThis.__ratingIdCounter++生成实例级name,避免跨组件污染 - 每颗星的
input必须有独立id,对应label[for]才能点击生效;隐藏方式只能是position: absolute; opacity: 0;,禁用display: none - 必须监听
change事件而非click,且绑定到 shadow DOM 内部的input组上,否则外部 JS 无法可靠捕获
如何支持 value 属性双向同步
原生 input 有 valueAsNumber,但自定义组件没有。你得手动实现 getter/setter,并在 set 时触发重绘。
- setter 中调用
this._renderStars(),根据新value设置对应input:checked状态,并处理半星逻辑(用两个重叠label+clip-path截断) - getter 返回当前选中值,优先取
input[name="xxx"]:checked, fallback 到this._internalValue - 必须在
attributeChangedCallback中监听value和max变化,否则通过 HTML 属性设置初始值会失效
无障碍与表单集成常被忽略的关键点
很多 Web Component 评分组件在表单里提交失败,或被屏幕阅读器跳过,问题出在三个地方:
- 没包裹在
fieldset里 —— 导致 Safari 重置按钮无法还原状态,且legend缺失使屏幕阅读器无法关联标签 - 没暴露
aria-valuenow、aria-valuemin、aria-valuemax—— 即使视觉正常,AT(辅助技术)仍认为这是普通 div - 没实现
formAssociated: true和internals.setFormValue()—— 这意味着它不会出现在formdata事件中,后端收不到值
真正可用的自定义评分组件,不是画几颗星星,而是让浏览器把它当做一个原生表单控件对待。这点一旦漏掉,再漂亮的动画也没法上线。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











