- 前端
- UI组件
【免费下载链接】fast
The adaptive interface system for modern web experiences.
导读
本文围绕 API 文档 AttributeDefinition.name property 展开,深入剖析@microsoft/fast-element中AttributeDefinition.name属性的定义、产生方式与运行机制。name是连接「组件类属性」与「HTML 属性」的枢纽:它以组件属性(property)名字符串的形式存在,是AttributeDefinition元数据中区分「属性名」与「HTML attribute 名」的关键字段。读完本文,你将掌握name的签名与语义、它与attribute、Owner、mode、converter等兄弟字段的关系,以及它如何通过attr()装饰器、AttributeDefinition.collect()静态方法进入FASTElementDefinition的propertyLookup/attributeLookup双向映射表,最终驱动observedAttributes的注册与属性变化回调。
AttributeDefinition.name:属性签名与语义
官方签名的精确含义
在 sites/website/src/docs/1.x/api/fast-element.attributedefinition.name.md 中,该 API 文档给出了完整的定义:
readonly name: string;文档描述为:"The name of the property associated with the attribute."——即与属性相关联的组件属性(property)名称。注意这里的关键措辞:
readonly:该字段在AttributeDefinition实例创建后不可再被修改,它属于不可变元数据;- 类型为
string:它就是一个普通的字符串,保存的是组件类上属性的名字,例如"count"、"userName"或"fooBar"; - 语义上它是「property name」,而非「HTML attribute 名」。
为什么必须区分 name 与 attribute?
在实际 DOM 中,HTML 属性(attribute)名与组件属性(property)名并非总是相同。常见的命名差异包括:
| 对比项 | 含义 | 典型示例 |
|---|---|---|
name(property) | 组件类上定义的 JavaScript 属性名 | userName、fooBar、count |
attribute(HTML attribute) | 暴露到 HTML 标签上的属性名 | user-name、foo-bar、count |
Owner | 拥有该属性的类构造函数 | class MyComponent extends FASTElement |
正是name与attribute的分离,使得 fast-element 能够在保持 JS 端 camelCase 属性风格的同时,遵循 HTML 规范中 kebab-case(短横线)attribute 命名约定。
源码实现:name 在 AttributeDefinition 类中的位置
在 attributes.ts 中,AttributeDefinition类的实现完整展示了name的声明与初始化:
export class AttributeDefinition implements Accessor { private readonly fieldName: string; private readonly callbackName: string; private readonly hasCallback: boolean; private readonly guards: Set<unknown> = new Set(); /** * The class constructor that owns this attribute. */ public readonly Owner: Function; /** * The name of the property associated with the attribute. */ public readonly name: string; /** * The name of the attribute in HTML. */ public readonly attribute: string; public readonly mode: AttributeMode; public readonly converter?: ValueConverter; public constructor( Owner: Function, name: string, attribute: string = name.toLowerCase(), mode: AttributeMode = reflectMode, converter?: ValueConverter, ) { this.Owner = Owner; this.name = name; this.attribute = attribute; this.mode = mode; this.converter = converter; this.fieldName = `_${name}`; this.callbackName = `${name}Changed`; this.hasCallback = this.callbackName in Owner.prototype; if (mode === booleanMode && converter === void 0) { this.converter = booleanConverter; } } // ... }从构造函数可以提炼出name的三个核心用途:
- 直接存储:
this.name = name将传入的属性名原样保存为公开只读字段; - 派生私有存储字段:
this.fieldName = \_${name}`生成内部存储槽(如_count`),用于保存属性在元素实例上的实际值; - 派生变更回调名:
this.callbackName = \${name}Changed`生成观察回调名(如countChanged),并立即检查Owner.prototype上是否存在该回调(hasCallback` 标志)。
这三个用途说明:name不仅是一段文档字符串,而是整个属性响应式链路的命名基准。
默认值规则
构造函数中attribute: string = name.toLowerCase()展示了一条重要约定:当未显式指定 HTML attribute 名时,fast-element 默认使用属性名的小写形式作为 attribute 名。因此:
- 属性
count默认映射到 attributecount; - 属性
userName默认映射到 attributeusername(全部小写)。
如果希望显式采用 kebab-case(如user-name),则需要在配置中显式指定attribute字段,或依赖attr()装饰器配置。
name 的产生:attr() 装饰器与 AttributeDefinition.collect()
装饰器层面的录入
name的源头是组件类上的@attr()装饰器。在 attributes.ts 中,attr()支持两种调用形式:
// 形式一:直接用于属性(非调用形式) @attr class MyComponent extends FASTElement { count = 0; // 等价于 @attr() count } // 形式二:带配置(调用形式) @attr({ attribute: "data-count", mode: "fromView" }) class MyComponent extends FASTElement { count = 0; }装饰器内部通过AttributeConfiguration.locate($target.constructor).push(config)把配置写入AttributeConfiguration元数据定位器中(基于createMetadataLocator实现,见 platform.ts)。当以非调用形式使用时(arguments.length > 1分支),装饰器会把被装饰属性的名字自动写入config.property = $prop,这个property字段正是后续name的输入来源。
collect():将配置组装为 AttributeDefinition
静态方法AttributeDefinition.collect()(见 attributes.ts)负责把装饰器收集到的AttributeConfiguration(以及FASTElementDefinition中手写的attributes数组)统一组装成AttributeDefinition实例:
public static collect( Owner: Function, ...attributeLists: (ReadonlyArray<string | AttributeConfiguration> | undefined)[] ): ReadonlyArray<AttributeDefinition> { const attributes: AttributeDefinition[] = []; attributeLists.push(AttributeConfiguration.locate(Owner)); for (let i = 0, ii = attributeLists.length; i < ii; ++i) { const list = attributeLists[i]; if (list === void 0) continue; for (let j = 0, jj = list.length; j < jj; ++j) { const config = list[j]; if (isString(config)) { attributes.push(new AttributeDefinition(Owner, config)); } else { attributes.push( new AttributeDefinition( Owner, config.property, // ← 这里成为 name config.attribute, config.mode, config.converter, ), ); } } } return attributes; }两种输入形态都清晰可见:
- 字符串形态:直接以字符串作为
name(new AttributeDefinition(Owner, config)),此时attribute自动取name.toLowerCase(),mode取默认值"reflect"; - 配置对象形态:
config.property成为name,其余字段(attribute、mode、converter)可分别定制。
继承层次中的聚合
在 attributes.pw.spec.ts 中,有专门针对继承聚合的测试:BaseElement声明attributeOne,ComponentA声明attributeTwo,ComponentB覆盖attributeTwo为 getter。测试断言:
ComponentA收集到 2 个属性(attributeOne+attributeTwo);ComponentB收集到 1 个属性(attributeOne,因为其attributeTwo是 getter,装饰器不会为其生成定义)。
这验证了collect()通过createMetadataLocator沿原型链向上聚合配置、再结合每个类的实际属性形态去重的能力——每个生成的AttributeDefinition的name都是该链上被去重后的唯一属性名。
name 在 FASTElementDefinition 中的流向:propertyLookup 与 attributeLookup
AttributeDefinition实例最终进入 FASTElementDefinition 的构造过程。其中关键代码如下:
const attributes = AttributeDefinition.collect(type, nameOrConfig.attributes); const observedAttributes = new Array<string>(attributes.length); const propertyLookup = {}; const attributeLookup = {}; for (let i = 0, ii = attributes.length; i < ii; ++i) { const current = attributes[i]; observedAttributes[i] = current.attribute; propertyLookup[current.name] = current; // name → AttributeDefinition attributeLookup[current.attribute] = current; // attribute → AttributeDefinition Observable.defineProperty(proto, current); } Reflect.defineProperty(type, "observedAttributes", { value: observedAttributes, enumerable: true, }); this.attributes = attributes; this.propertyLookup = propertyLookup; this.attributeLookup = attributeLookup;这里name扮演了键角色:
propertyLookup[current.name] = current:以name(property 名)为键建立「属性名 → AttributeDefinition」映射;observedAttributes数组则填充current.attribute,最终通过Reflect.defineProperty挂到类静态属性上,供浏览器 custom element 机制调用attributeChangedCallback;Observable.defineProperty(proto, current)用AttributeDefinition作为Accessor在原型上定义响应式 getter/setter,其内部调用Observable.track(source, this.name)(见 attributes.ts),用name作为可观察依赖的追踪键。
由此形成完整的双向查询能力:
| 查询方向 | 入口 | 用法 |
|---|---|---|
| 已知 property 名 → 取定义 | definition.propertyLookup["userName"] | 模板绑定、代码内访问属性元数据 |
| 已知 HTML attribute 名 → 取定义 | definition.attributeLookup["user-name"] | attributeChangedCallback分发、SSR 水合 |
| 浏览全部属性 | definition.attributes | 调试、遍历、扩展机制 |
name 在模板绑定中的可观察性
name直接参与 fast-element 的模板编译与响应式系统:getValue中Observable.track(source, this.name)意味着模板里任何对{{count}}等绑定值的读取,都会以name为标识建立依赖记录;当setValue写入新值时(见 attributes.ts),会通过((source as any).$fastController as Notifier).notify(this.name)以同样的name触发通知。读与写两侧使用同一个name字符串,是属性级响应式能够精确工作的前提。
name 与 attribute 的命名策略:kebab-case 映射
运行时默认映射
如前文构造函数所示,运行时@attr()默认把name.toLowerCase()作为 attribute 名。例如属性firstName在未指定attribute时,其 HTML attribute 为firstname。如果团队希望遵循 Web Components 社区更常见的 kebab-case 约定(first-name),应显式配置:
@attr({ attribute: "first-name" }) firstName = "";声明式模板中的 attribute-name-strategy
fast-element 还通过扩展机制(见 attribute-map.ts)为声明式模板提供了attribute-name-strategy配置:
"camelCase"(默认):绑定键视为 camelCase 属性名,HTML attribute 名由 kebab-case 转换推导(fooBar→foo-bar);"none":绑定键直接同时用作属性名与 attribute 名,不做任何归一化。
该扩展在运行时通过Observable.getAccessors(this.classPrototype).map(a => a.name)(attribute-map.ts)读取所有 accessor 的name,再按策略生成AttributeDefinition并合并进definition.attributeLookup与observedAttributes。这印证了name不仅服务于装饰器定义属性,也是声明式模板/扩展体系读取「属性清单」的标准接口。
name 的典型使用场景与最佳实践
场景一:在组件代码中获取属性元数据
通过FASTElementDefinition的公开字段可以按 property 名反向查询属性定义:
import { FASTElementDefinition } from "@microsoft/fast-element"; const def = FASTElementDefinition.getByType(MyComponent); const countDef = def.propertyLookup["count"]; // → AttributeDefinition console.log(countDef.name); // "count" console.log(countDef.attribute); // "count"(默认小写) console.log(countDef.mode); // "reflect" | "boolean" | "fromView"场景二:在属性变更回调中区分触发源
name派生的回调名${name}Changed可用于实现依赖联动:
class TemperatureWidget extends FASTElement { @attr({ mode: "fromView" }) celsius = 0; celsiusChanged(oldValue: number, newValue: number) { // 依据 name 派生回调触发,处理单位换算 console.log(`celsius: ${oldValue} → ${newValue}`); } }最佳实践小结
- 命名即契约:
name是 camelCase 的 property 名,应保持语义清晰;HTML attribute 名(attribute)另设字段管理,二者不要混用; - 显式指定 attribute 名:当默认的小写映射不符合 kebab-case 团队约定时,使用
@attr({ attribute: "kebab-name" })显式声明; - 利用回调派生:
${name}Changed回调名由name自动派生,无需额外声明监听器即可实现属性联动; - 只读不可变:
name是readonly,不要在运行时改写定义元数据,如需扩展属性应走AttributeDefinition.collect()或声明式扩展机制。
总结
AttributeDefinition.name表面上只是readonly name: string一行签名,实际上它是 fast-element 属性系统的「命名轴心」:由@attr()装饰器或AttributeConfiguration提供输入,经AttributeDefinition.collect()组装为实例后,同时驱动私有存储字段(_name)、变更回调(nameChanged)、可观察依赖追踪(Observable.track/notify的键)、propertyLookup反向索引以及默认 attribute 名推导(name.toLowerCase())。理解name与attribute的区分及name在整个定义-注册-响应式链路上的流转,是深入掌握 @microsoft/fast-element 自定义元素属性体系的关键一步。相关源码可继续研读 attributes.ts、fast-definitions.ts 与 attributes.pw.spec.ts。
- 前端
- UI组件
【免费下载链接】fast
The adaptive interface system for modern web experiences.
相关推荐
在 Svelte 项目中通过 sv 社区插件 `@shadcn-svelte/sv` 一键接入 shadcn-svelte
在 Svelte 项目中通过 sv 社区插件 @shadcn svelte/sv 一键接入 shadcn svelte @shadcn svelte/sv 是
前端UI组件深入掌握 @typespec/xml 装饰器:从属性映射到 XML 命名空间的完整实战指南
深入掌握 @typespec/xml 装饰器:从属性映射到 XML 命名空间的完整实战指南 导读 @typespec/xml 是 TypeSpec 官方提供的
编程语言编译器后端FAST Element 的 attr() 装饰器:自定义元素 HTML 属性声明的完整指南
FAST Element 的 attr 装饰器:自定义元素 HTML 属性声明的完整指南 导读 attr 是 FAST Element 中用于声明自定义元素 H
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考