1. Web API设计原则
设计原则直接受《道德Web原则》[ETHICAL-WEB]中提出的伦理框架指导。 这些原则针对开发Web平台所承担的更高层次伦理责任,给出了具体实用的建议。
1.1. 以用户需求为先(相关方优先级)
如需权衡取舍,应始终把用户需求放在首位。
同样,设计API之初,要确保理解并记录该API旨在解决的用户需求。
互联网是为终端用户服务的: 对Web平台做出的任何改变,都可能 影响大量人群, 甚至深刻改变某个人的生活。[RFC8890]
用户需求优先于网页作者需求, 作者需求优先于用户代理实现者需求, 实现者需求优先于规范制定者需求, 规范制定者需求优先于理论纯粹性。
如同所有原则, 这并非绝对。 作者编写难易影响内容如何传递给用户。 用户代理需权衡有限工程资源,影响功能如何到达作者。 规范制定者也有资源限制, 而理论考量反映了各方的基础需求。
另见:
1.2. 访问网页应当安全
在添加新功能时, 设计时需确保用户对“访问网页一般是安全的”这一预期得以保持。
Web因其超链接结构而得名。 为了保持Web的活力, 用户需要确信仅仅访问某个链接不会对其计算机安全或 隐私等重要方面造成影响。
例如,若某API允许网站 检测辅助技术的使用, 可能会让依赖这些技术的用户在访问未知网页时感到不安全, 因为任何网页都能检测到这些隐私信息。
只要用户能合理预期安全, 就能自行判断选择Web技术或其他技术。 例如,用户可能选择网页点餐而非安装APP, 因为安装本地应用的风险大于访问网页。
为了让Web的实际安全性与用户预期相符, 添加新功能时可以采取补充性措施:
-
可改进Web的用户界面,让用户明确知晓Web能做什么、不能做什么;
-
可改变Web的技术基础,使其符合用户对隐私的预期;
-
可考虑哪些情况下应提升用户预期,并据此同时调整技术基础和用户期望。
某些新功能即使带来安全风险,整体仍可能提升用户安全, 比如让用户在网页上完成某任务比安装本地应用更安全。 但这种收益需与用户对网页安全的合理预期进行权衡。
另见:
1.3. 可信用户界面必须值得信赖
考虑新功能是否影响可信用户界面。
用户依赖地址栏、安全指示器、权限提示等可信用户界面, 来判断自己与谁、以何种方式互动。 这些界面必须可设计为让用户信任 并验证 信息的真实性,且不会被网站伪造或劫持。
若新功能让不可信用户界面可模仿可信界面, 会让用户难以分辨信息是否可信。
例如,JavaScript alert()
允许页面显示类似浏览器的模态对话框,
这常被用来诱导用户访问诈骗网站。
若今天提出此功能,可能不会被采纳。
1.4. 向用户请求有意义的同意
为满足用户需求, 网页可能需用到某些有潜在风险的功能。 这些功能应设计为让用户能 有效地表达同意, 并能真正拒绝授权。
用户要做出有意义的同意,需:
-
理解自己要授权哪些权限
-
能有效地选择是否授权。
如果某功能强大到需要用户同意, 却无法向普通用户解释其含义, 说明功能设计可能需重新考虑。
如弹出权限提示、 用户未授权, 则网页不应做任何用户认为已拒绝的事。
通过请求同意, 可让用户了解网页具备或不具备哪些能力, 增强其对Web安全性的信心。 但新功能的用户收益 必须足以抵消用户每次被要求授权的负担。
在规范中,可用请求使用权限和提示用户选择算法(见[permissions])实现请求同意。
拒绝最有效的方式是让网站无法 区分拒绝与其它常见情形, 让网站更难强迫用户授权。
例如, 地理定位API 可访问用户位置信息。 某些场景下有助于用户(如地图应用), 但在其他场景可能危险,尤其在用户不知情的情况下。 为让用户决定网页是否可用其位置, 应弹出权限提示。 若用户拒绝,网页无法获得位置信息。
另见:
1.5. 在场景中适当使用身份信息
让用户掌控 自己在Web不同场景下展示的身份信息, 并保持透明。
“身份”是个复杂概念,可有多种理解, 包括个人如何展现/看待自己,与他人、团体或机构的关系,以及行为与他人对待方式。 Web架构中,“身份”常用来指代标识符及附属信息。
涉及或依赖标识符及其附属个人数据的功能, 存在超越单一API或系统的隐私风险,包括被动生成的数据(如行为数据),也包括主动收集的数据(如表单填写)。
针对这类功能,应理解其使用场景, 包括与Web其它功能配合使用的方式。 确保用户能适当表达同意。 设计API时仅收集 最少必需数据。 除非确实需要持久标识符,否则应用临时标识符。
1.6. 支持全类型设备与平台(媒体独立性)
尽量确保Web功能可在不同输入输出设备、屏幕尺寸、交互模式、平台和媒体中可用。
Web的主要价值之一是极高的灵活性: 网页可在各种终端设备和屏幕尺寸下显示, 可用于打印媒体, 并支持多种交互方式。 新功能应与Web平台的现有灵活性保持一致。
这些功能仍可适配多种场景, 无法原生支持时可退化处理——如移动设备上的轻触会触发click事件。
功能设计也应保证最简用法同样具备灵活性。
有时某些功能在部分实现或平台上未支持,但在其它平台可用。 此时应保证代码可优雅降级或polyfill。 参见§ 2.6 新功能应可检测。
1.7. 谨慎添加新能力
添加新能力时需考虑现有功能和内容。
Web包含许多可扩展点以支持新功能; 例如见HTML § 1.7.2 可扩展性。
在添加新项前,应考虑与现有类似能力的集成。 如此带来更优设计方案,但不能仅靠加项实现, 仍有可能采纳;见§ 1.8 需先理解现有用法再移除或更改能力。
不要在未确认前就假定某项更改或移除不可行。
1.8. 需充分理解现有用法后再移除或更改能力
移除或更改功能时优先考虑兼容现有内容。
一旦大量内容依赖某个行为, 就不建议移除或更改该行为。 移除/更改功能是可行的, 但前提是充分了解其对现有内容的影响范围与性质, 这可能需调研现有内容的使用情况。
理解现有用法的义务同样适用于内容依赖的所有功能, 包括厂商私有功能和被认为是实现bug的行为。 Web特性不仅由规范定义, 还由内容的实际用法决定。
1.9. 让Web比你遇到时更好
为Web平台添加新能力时, 应提升整体平台质量。
平台某处有缺陷, 不能成为在该缺陷上继续扩展/添加的理由, 否则会进一步降低平台质量。
即使现有设计模式有问题, 也不应在新API或平台功能中延续此模式。 应尽量通过新能力改善平台质量, 弥补现有缺陷。
如需扩展有已知缺陷的平台功能, 应考虑如何修正这些问题。 不必完全修复,但扩展时应尽量改进, 以免新用法加剧缺陷危害。
Web平台各部分可独立演化。 当前某技术的问题可能在后续迭代中修正。 若重复这些问题,将使修复更难。 遵循此原则能保证平台质量随时间改善。
1.10. 最小化用户数据
设计功能时应仅使用完成用户目标所需的最少数据。
数据最小化 可减少数据泄露或滥用的风险。
Web API应便于网站请求、收集和/或传输较少、更细粒度或更具体的数据, 而不是使用模糊或批量数据。 API还应为用户提供数据粒度与控制权, 尤其针对个人数据的传递。 如需更多数据以实现新功能,应在用户同意(如权限提示或用户激活)后开放。
2. 跨语言的API设计
2.1. 优先选择简单方案
深入寻找能够满足用户需求的简单方案。
简单方案通常优于复杂方案,虽然可能更难发现。简单的特性更易于用户代理实现和测试,更有可能实现互操作,也更容易让作者理解。尤其要确保你的特性能让最常见的用例易于实现。
确保你的用户需求定义明确。这样可以防止范围膨胀,并确保API真正满足所有用户的需要。当然,复杂或罕见的用例也值得解决,不过它们的方案可能更复杂。如 Alan Kay 所说,“简单的事情应当简单,复杂的事情应当可能。”
但需要注意的是,常见用例通常很简单,但常见性和复杂性并不总是相关。
另见:
2.2. 权衡高层与底层API
高层API让用户代理能以多种方式代表用户进行干预,比如确保可访问性、隐私或易用性。
TAG推荐使用字体选择器(高层API)(原文链接)而非字体枚举API(底层API),因为它满足绝大多数用例,同时保护用户隐私,不会像通用字体枚举API那样带来指纹识别风险。原生字体选择器还自带可访问性,并为终端用户提供一致体验。
底层API为作者提供实验空间,让高层API能随用例模式的演化自然产生,也能在高层API无法满足需求时提供“逃生通道”。
底层构件并非总能作为Web API暴露,原因包括保护用户安全和隐私,或避免将Web API绑定到特定硬件。但高层API应尽量以底层API为基础构建,这也有助于确定API需要多高层。
良好的分层方案应保证易用性与功能强大之间的权衡曲线连续,避免出现因用例复杂度略增而导致代码复杂度陡增的“断崖”。
2.3. 慎重命名
API命名要谨慎。好的命名能让作者更容易正确使用API。
详细命名建议请见命名原则一节。
2.4. 保持一致性
API设计时应参考先例,并尽量保持一致。
API易用性与一致性之间常有张力,尤其当现有先例可用性较差时。有时打破一致性以提升易用性是合理的,但前提是提升非常显著。
Web平台逐步演化,常有多种互相矛盾的先例。可以根据流行度(优先更流行的先例)、易用性(优先更易用的先例)、API年龄(优先更新的先例)等综合权衡。
内部一致性与外部一致性也常有冲突。内部一致性是与系统其它部分的一致,外部一致性是与外界的一致。Web平台通常有三层:API所属技术内部一致性(如CSS),与Web平台其它部分一致,以及部分场景下与外部领域的一致。此时应考虑多数用户是谁。大多数API的目标用户熟悉其所属技术,应倾向于与此保持一致。
关于命名一致性另有专门章节,见命名一致性。
2.5. 遵循特性规范中的指导
使用某项特性时,要遵循其规范中的指导。
为其它规范调用的特性,应在规范中给出使用指导,以确保整个平台的正确和一致使用。
部分指导举例如下:
-
使用HTTP构建协议, 尤其是定义头字段、 其它HTTP RFC、 以及HTTP工作组风格指南
使用他人规范时要与相关社区沟通,这有助于告知社区新用法,并为双方规范创造完善机会。
2.6. 新特性应可检测
为作者提供编程方法检测你的特性是否可用,让网页内容能优雅处理特性不可用的情况。
现有特性可能因多种原因页面不可用,常见原因有尚未实现,或仅在安全上下文中可用。
作者不应为每种情形都写不同代码,这样即使作者只关注某一种情形,代码也能适配全部。
某特性可用但因缺少所需设备而无法使用时,最好暴露特性可用性,并另有方法检测设备是否存在。这样作者可区分“设备不可用”与“特性不可用”,例如建议用户连接或启用设备。
作者应始终能通过JavaScript检测特性,在某些情况下也应能在对应语言中检测(如CSS中的@supports)。
某些场景下,不适合支持特性检测。是否可检测应以用户需求为依据。如果支持检测会违反用户需求或设计原则,则不应支持检测。
检测特性可用性不代表检测是否已获得同意。一般来说,检测实现与检测授权应分离。有时为了支持拒绝请求,需禁用特性检测。
如果某特性一般不向开发者开放,也不适合支持检测。例如,隐私浏览模式在规范中有定义,但不向作者开放。为了满足用户需求,不应支持检测隐私浏览模式。
另见:
2.7. 为人类设计文本格式
设计文本格式时应便于人类生成和使用。文本格式也提升了透明度。
优先考虑可读性而非紧凑性。文件体积可用工具优化,随着时间推移通常不是首要考虑。如果体积是绝对优先,也许不应采用文本格式。
接触文本格式的人应能通过文本编辑器轻松生成或修改内容。手动编辑会产生各类错误,用户可能难以发现和修正。
人们期望编辑时有一定灵活性。明确定义语法灵活性(如空白、引号、分隔符等)能保证内容易编辑且结果一致。
限定格式受语法错误影响的范围可提升健壮性和人类可用性。规范需完整定义处理和错误机制,保证全部输入都有一致结果。
如果某格式仅供机器使用,二进制格式往往更高效,也能避免人类直接编写和编辑内容。
2.8. 考虑仅在安全上下文开放新特性
如果某特性在无认证、完整性或保密性(仅安全上下文具备)下会对用户造成风险,应始终限定在安全上下文中使用。
其他特性方面,TAG成员未有统一结论。有些人认为所有新特性(除扩展现有特性外)都应限定在安全上下文,这有助于推广HTTPS,让用户整体更安全。
有人认为,只有有已知安全或隐私影响的特性才应限定在安全上下文,这降低了利用新特性的门槛,适用于无安全/隐私影响的新网页。
SecureContext
]
扩展属性,限定接口、命名空间或其成员(如方法和属性)在安全上下文中使用。
但部分API(如分发事件)应在规范正文中明确限制为安全上下文。此时可考虑是否能为[SecureContext
]扩展机制,为未来API开发提供便利。
但如无法优雅处理特性不可用,限定特性只在安全上下文可用可能会给可在安全/非安全环境通用的代码(如库)带来问题。
2.9. 不要暴露隐私浏览模式的使用
确保你的特性不会让作者检测到隐私浏览模式。
部分人使用隐私浏览模式以保护自身安全,因此“正在使用隐私浏览模式”可能是敏感信息。若此信息被泄露给掌控其权力的他人(如雇主、家长、伴侣或国家行为者),可能造成伤害。
鉴于此类风险,网站不应能检测隐私浏览模式是否已启用。
另见:
2.10. 考虑API在隐私浏览模式下的行为
如有必要,规范应指明API在隐私浏览模式下的不同表现。
例如,若API会暴露信息,使他人可将用户在隐私与非隐私模式下的活动关联起来,应考虑缓解措施,如引入噪声或使用权限提示让用户更好地知情同意(见§ 1.4 请求用户有意义的同意)。
隐私浏览模式让用户能无痕浏览网页,因此提供客户端存储的API不应在隐私浏览模式解除后保留数据。应做到不向网站暴露任何可检测的API差异。
如用户代理同时与某站点有普通和隐私会话,隐私会话中的存储变更不应暴露给普通会话,反之亦然。(storage事件不应在另一会话的window对象上触发。)
另见:
2.11. 不要暴露辅助技术的使用
确保你的API不会让作者检测到用户正在使用辅助技术。
Web平台必须对残障人士可访问。如网站能检测到用户使用辅助技术,可能拒绝或限制其服务。
依赖辅助技术的人常是社会易受伤害成员,他们的辅助技术使用情况是敏感信息。API若暴露此信息,可能泄露给有害他意者(包括国家行为者)。
有时有人提出旨在提升辅助技术用户体验的特性,但会副作用暴露辅助技术使用。虽出于善意,但却违反了§ 1.2 访问网页应当安全,需寻找替代方案。
另见:
2.12. 强大API需用户激活
部分强大API会生成侵入性界面(如自动播放音频)、暴露用户数据(如操作剪贴板)、后台活动无明显指示(如访问本地存储)、或促使用户与可信界面交互(如权限提示、硬件功能)。这些API应设计为必须有用户意图的指示(如用户激活)后才能运行,表明用户正在主动与网页交互。
用户激活的详细定义见HTML标准。设计API时应根据用户体验和风险,决定激活是全局一次(sticky)、周期性(transient)还是每次调用一次(transient consuming)。
注意,用户激活在多数情况下必要,但并不总是充分保护用户不受侵扰行为,还需寻求有意义的同意。
2.13. 支持非完全激活的BFCached文档
如可能,规范应指明特性在非完全激活的BFCached(前进/后退缓存)文档中的表现。
如你的特性涉及如下任何事项:
-
与文档“外部”交互(如向文档发送信息)
-
支持跨文档交互/资源共享(如持有锁)
-
文档被缓存为非完全激活后可能异常(如假定文档状态不会跨多次导航)
应按BFCached文档支持指南规定,指明特性与非完全激活BFCached文档的兼容方案。
注意:文档变为非完全激活也可能因其它原因(如iframe被移除),此指导只关注BFCache相关情形,不涵盖其它非激活来源。
2.14. 优先考虑可用性而非第三方工具兼容性
设计新特性时应以可用性为首要目标,第三方工具兼容性为次要目标。
Web平台受益于丰富工具生态,便于开发。新特性语法常与第三方工具冲突导致故障,尤其是这些工具常用于Web特性原型开发。
Web平台特性通常比第三方工具寿命更长,因此应优先保证特性的最佳语法和功能。
有时冲突会影响大量网站,需重设特性语法以避免冲突。
但此类情况应属例外。
决定是否因新语法破坏第三方工具时要考虑多因素,如破坏严重程度、工具流行度等。
最重要因素常是为避免破坏第三方工具而更改语法是否会严重损害Web特性的可用性?如有多个可用性相近方案,通常优先选择对第三方工具干扰最小者。
但如为避免破坏第三方工具而导致特性可用性显著下降,除非大量网站因此受损,否则通常不可接受。
语言还应提供可扩展机制,让作者能扩展语言而不破坏未来原生功能,以减少类似困境。
2.15. 通过组合简单类型构建复杂类型
定义可作为超类型替代的子类型。避免接口继承,除非父类型所有属性和行为对子类型都适用。
继承下,子类型能作为父类型的实例,需提供相同属性和方法,并保持行为一致。若子类型改变了某项行为,使用父类型处理时可能异常。
类型系统理论中,这称为Liskov替换原则。
更简单做法通常是避免继承,通过组合复用已有能力。新类型可定义属性以持有所需组件。
HTMLInputElement
很难,因为input
元素很复杂,专用化HTMLInputElement
必须保持全部复杂性。自定义元素可通过组合需要与表单交互的项,无需应对HTMLInputElement
的复杂性。
3. HTML
本节详细介绍通过HTML暴露的功能的设计原则。
3.1. 仅为类似功能复用HTML属性名
如果你要添加一个通过HTML属性指定的功能, 请检查其他元素上是否已有用于类似功能的属性名。 复用已有的HTML属性名,可以让作者利用现有知识, 保持语言的一致性, 并让词汇量更精简。
如果复用现有HTML属性, 尽量保持其语法与原属性一致。
for
属性最初用于
label
元素,指定关联的表单元素。
后来又被
output
元素复用,用于指定哪些元素的输入值参与或影响了计算。
后者语法更宽泛:可以接收以空格分隔的一组id,而前者只接收一个id。
但两者都遵循相同的语法,如果其中一个接收id列表,另一个接收selector,那就是反模式。
反之亦然: 如果你要添加的功能与现有属性功能不相似,则不要复用已有HTML属性名。
type
属性在
input
和
button
元素上用于细化元素类型,
而在其它元素(如
link
、
script
、
style
)
则用于指定MIME类型。
这是反模式,其中一组属性应该用不同的名字。
3.2. 短列表用空格分隔属性,长列表用多个元素
当为元素指定可为列表的元数据时,
通常做法是用空格分隔,并作为DOMTokenList
暴露。
class
属性接收以空格分隔的类名列表。
classList
是一个DOMTokenList
,可添加/移除类名。
sandbox
属性接收以空格分隔的沙箱标志。
iframe.sandbox
是DOMTokenList
,可添加/移除沙箱标志。
与Web平台其它部分保持一致很重要, 即使这意味着用其他字符分隔值。
无论语法如何,属性只应用于短列表。 长列表不建议嵌入属性, 而应用单独元素表示列表项(及元数据)。 这些元素可以是目标元素的子元素,也可通过属性链接。
极少数情况下需要其他权衡。
3.3. 不要阻塞HTML解析器
确保你的设计不要求HTML解析器为处理外部资源而暂停。
页面解析时, 浏览器会发现页面所需资源, 并决定并行加载优先级。 若有资源阻止后续资源的发现,则解析流程会被打断。 最坏情况下,浏览器串行下载资源,不能并行。 最好情况下,浏览器基于猜测解析进行队列下载,但猜测可能出错。
阻塞解析器的特性一般是为了在后续内容前向解析器注入新内容。
比如传统的<script src="…">
元素可用document.write(…)
注入解析器。
由于上述性能问题,新的特性不能这样设计。
3.4. 避免阻塞渲染的功能
需要在页面渲染前加载资源或执行操作的功能, 往往导致页面空白(或显示前一页),体验很差。
仅在整体用户体验得到提升时才考虑这种功能。 一个典型例子是为加载和处理样式表而阻塞渲染。 否则就会有无样式内容闪现(FOUC),这是不希望看到的。
另见§ 10.2.1 某些API应只暴露给dedicated worker。
3.5. 保持属性同步
新的内容属性应有同名IDL属性,并保持状态同步。 若同步的IDL属性命名不一致,会造成混淆,应避免。
input
的
value
、
option
的
selected
、
以及
input
的
checked
,
对应的IDL属性并不是同名的,
而是defaultValue
、
defaultSelected
、
defaultChecked
。
3.6. 包含URL属性应按主要用途命名
如果元素允许用户跳转到属性中的URL,属性名应为href
,如
a
元素的
href
属性。
注意:回头看,
form
的
action
属性其实应该叫href
。
如果元素会加载给定URL的资源,属性名应为src
,如
img
元素的
src
属性,或
script
元素的
src
属性。
注意:HTML有一些遗留的不一致做法,不建议效仿,如
link
的
href
属性可能用于跳转,也可能用于加载资源,取决于
rel
属性的值。
若属性标识的URL只是元素用途的辅助信息,如
video
的
poster
、
q
的
cite
、
或
a
的
ping
,
则应按语义命名该属性。
注意:含有URL的属性在IDL中应用USVString
表示;
见§ 8.2 合理表示字符串。
3.7. 每个HTML元素只赋予单一用途
每个HTML元素都应有明确的用途。 HTML属性可修饰元素语义, 但不应根本改变其用途。
与其定义能以不同模式运行的元素, 不如为每种模式分别设立不同元素。 这样更便于作者使用。
通过属性在同一元素上过载语义以选择不同行为, 只有在无论属性值为何,元素用途都不变时才适用。
反例包括过度专用和依赖上下文确定语义的元素。
4. 层叠样式表(CSS)
本节详细介绍通过CSS暴露的功能的设计原则。
4.1. 根据层叠需求分离CSS属性
根据哪些值适合独立设置,决定应将哪些值分组为同一个CSS属性,哪些应单独设为不同属性。
CSS层叠 允许不同规则或不同样式表中的声明 相互覆盖。 应同时被覆盖的一组值应归为同一属性,以便一同层叠。 同理,应能独立覆盖的值应分成不同属性。
而initial-letter-align属性则应单独设立, 因为它用于设置整个文档的对齐策略,是一种通用样式选择, 并与文档所用的书写体系(如拉丁、斯拉夫、阿拉伯)相关。
4.2. 合理选择CSS属性是否继承
根据属性的效果在祖先和后代元素同时设置时,是应被覆盖还是叠加,决定该属性是否继承。
如果在后代元素上设置该属性需覆盖(而非叠加)祖先设置的效果,则该属性应继承。
如果后代元素上的设置是独立的效果,会在祖先设置基础上叠加,则该属性不应继承。
如某非继承属性的规范要求处理元素时查找祖先属性值(可能还很慢),这是“代码异味”,说明该属性其实应该继承。 如某继承属性的规范要求处理元素时忽略与父元素相同的属性值,也是“代码异味”,说明该属性其实不该继承。
如果background-image属性被继承, 则规范需增加不少复杂度以避免半透明图片在每个后代元素上重复显示。 这种复杂度可能要求属性值与父元素一致时行为不同, 这就属于上述“代码异味”,说明该属性其实不该继承。
若font-size属性不继承, 则初始值可能需要沿祖先链查找最近的非默认值祖先。 这也是上述“代码异味”,说明该属性其实应该继承。
4.3. 根据继承方式选择计算值类型
根据CSS属性如何继承(包括依赖其他属性的情况)选择其计算值类型。
继承意味着元素获得与父元素相同的计算值。 所以在到达计算值前的处理步骤会影响被继承的值, 之后的步骤(如used value)则不会。
但此时计算值是<number> 1.4, 不是<length> 28px。 (used value为28px。)
line-height属性可被继承到拥有不同font-size的元素, 这些元素上依赖line-height的属性, 必须使用自身相关的font-size, 而不是继承来源元素的font-size。
< body style = "font-size: 20px; line-height: 1.4" > < p > 这段正文行高为28px。</ p > < h2 style = "font-size: 200%" > 这个标题行高为56px,不是28px,即使line-height声明在body上。 这意味着40px字体不会溢出行高。</ h2 > </ body >
这些数字值通常是line-height的首选值,因为它们比长度值继承性更好。
另见:
4.4. 合理命名CSS属性与值
CSS属性名通常是名词,其值名通常是形容词(有时是名词)。
属性和值中的词用连字符分隔。一般避免缩写。
尽量用词根而非带语法前后缀的形式(如用“size”而非“sizing”)。
属性的值列表应选用可扩展方式,以便后续添加新值。 避免用yes、no、true、false等或等价复杂词。
避免在属性名中用“mode”或“state”,因为属性本身就是设置模式或状态。
有关命名的一般建议见§ 12 命名原则。
4.5. 内容默认应可见且可访问
设计CSS属性或CSS布局系统(通常为display
属性的值)时,
应确保内容默认可见、可访问且可用。
overflow: hidden
或left: -40em
)时才可能出现这种情况。
不应因默认行为(如display: flex
或position: relative
)而导致此类问题。
5. JavaScript语言
5.1. Web API使用JavaScript实现
设计Web的命令式API时,应使用JavaScript。 可以充分利用语言特有的语义和约定,无需通用化。
CustomElementRegistry.define()
方法接受
Constructor Method引用。
这利用了JavaScript最近引入的类机制,以及方法引用在JavaScript中易于使用的事实。
5.2. 保持运行至完成语义
如有状态变化源自JavaScript执行上下文外,应在任务之间将变化传递到JavaScript, 如排队任务, 或作为更新渲染的一部分。
与C++、Rust等低级语言不同, JavaScript历来只允许一次执行一段代码。 因此,JavaScript作者默认函数运行期间数据不会意外变化。
非开发者操作导致的变化和异步变化, 不应在其他JavaScript执行期间发生,包括微任务之间。
while
循环)或await
已解析Promise
后,开发者通常不会预期如下情况:
-
HTML解析器从网络加载新内容导致DOM更新
-
img.width
因图片数据加载而变化 -
Gamepad
按钮状态变化 -
scrollTop
变化,即使视觉上可滚动 -
同步方法因异步状态变化而表现不同。例如,若
LockManager
有同步方法,其行为取决于其它窗口的并发调用。
这些变化不是当前脚本造成,因此不应在当前任务期间发生。
数据可由开发者操作同步更新。
node.remove()
会同步改变DOM,且可立即观察到。
少数情况可违反此规则:
-
获取当前时间,如
Date.now()
和performance.now()
,但也可用如document.timeline.currentTime
的任务范围一致时间。 -
用于中断同步工作的函数,如
IdleDeadline.timeRemaining()
。 -
用于保护用户防止UI突变的状态,如transient activation。注意
navigator.userActivation.isActive
违反了此场景应用方法的建议。
5.3. 不暴露垃圾回收机制
确保你的JavaScript Web API不会让作者获知垃圾回收的时机。
不同用户代理垃圾回收时机不同,且随性能优化可能变化。 若API暴露垃圾回收时机,程序在不同环境下会表现不同, 作者需额外编写兼容代码。 若大量代码依赖特定时机实现,也会让用户代理更难优化垃圾回收策略。
因此,不应暴露任何作为弱引用的API,如属性因垃圾回收变为
。
JavaScript代码中的对象和数据生命周期应可预测。
getElementsByTagName
返回一个HTMLCollection
对象,
若对同一Document对象用同一标签名多次调用,可能重用该对象。
实际上,
只有对象未被垃圾回收才会返回同一对象。
因此,行为依赖垃圾回收时机。
若getElementsByTagName
现在设计,建议要么始终重用结果,要么每次创建新HTMLCollection
。
getElementsByTagName
并未指出其行为依赖垃圾回收时机。
相比之下,像WeakRef
或FinalizationRegistry
等API则明确让作者预期与垃圾回收的关系。
6. 设计JavaScript API
6.1. 合理使用WebIDL字典、接口和命名空间
为新API选择合适的WebIDL机制。
WebIDL提供多种结构定义Web API。 字典、接口和命名空间各自适合不同用途。
目标是确保API符合人体工程学、一致且自然,避免“伪类”或无功能的类等陷阱。
配置或只读输入数据请用字典
当API部分表示临时数据,尤其是接受一组参数、配置或选项时,选用字典。
字典适用于未持久存储或未变更的数据;仅在调用时使用。
例如,Web Share中的ShareData
成员:
dictionary ShareData { USVString title ; USVString text ; USVString url ; };
常见用法如下:
await navigator.share({text: "Text being shared" });
字典易于扩展,后续可方便添加可选字段。
字典成员默认可选,需时可标记为required
。
字典非常符合JavaScript习惯(idiomatic)。
直接传{ ... }
是JS中最自然的配置方式。
字典因用户代理处理方式也较具前向兼容性。 未实现的字典成员会被忽略,新成员可无缝添加,旧代码不受影响。
字典适用于生命周期中无需按类型区分的对象(如instanceof
检查只会是Object
)。
字典是“值传递”给方法(即被复制)。 浏览器引擎从JS对象转换到WebIDL时会剔除未知成员,传入API后修改值无效。
再举ShareData
字典例:
const data= { "text" : "Text being shared" , // 浏览器未实现"whatever"参数时会忽略 "whatever" : 123 , }; let p= navigator. share( data); // 调用.share()后再修改无效 data. text= "New text" ;
有功能、状态与身份请选接口
接口大致等价于JavaScript类。 当规范需要把状态(包括可见属性和内部“槽”)与操作(方法)绑定在一起时,用接口。
接口与字典不同:
-
可有带状态的实例,
-
可有只读属性,
-
赋值可有副作用,
-
可检查对象身份(即可用
instanceof
判断全局范围的某类实例),
定义接口还会暴露到全局作用域,便于规范静态方法。例如URL接口的canParse()
静态方法:
if ( URL. canParse( someURL)) { // 处理逻辑... }
有状态接口应有构造函数,若类无状态则不应添加构造函数,
否则是“伪类”(实例没做静态方法不能做的事)。如DOMParser
或DOMImplementation
就是伪类示例。
为接口数据提供序列化方法
添加序列化方法,把接口实例转换为多数应用可用的形式。
.toJSON()
方法让接口实例可生成有用的JSON序列化;
.toBlob()
可提取接口的二进制表示。
这样更自然与API配合使用。例如GeolocationPosition
接口提供toJSON()
方法:
const position= await new Promise(( resolve, reject) => { navigator. geolocation. getCurrentPosition( resolve, reject); }); const message= JSON. stringify({ user: userId, time: Date. now(), position, // .stringify()会自动调用.toJSON() });
行为型工具请用命名空间避免“伪类”
命名空间用于分组一组静态属性和方法,没有原型。
例如JS中的Math
、Intl
、Atomics
和Console
对象。
只有一两个小静态函数时,新建命名空间未免小题大做,挂到已有对象可能更合适。
反之,命名空间过大或过宽时应优化组织或分区。
“伪命名空间”
WebIDL接口可有属性,可通过非可构造接口属性创建“伪命名空间”。
典型例子是navigator
对象的各种属性。
navigator
本身有接口定义(Navigator
),
其属性又通过接口实例暴露进一步功能。
例如:
-
navigator.credentials
- Credentials Management API的CredentialsContainer
接口。 -
navigator.geolocation
- Geolocation规范的Geolocation
接口。 -
navigator.permissions
- Permissions规范的Permissions
接口。
当你需要引用某实例(如特定浏览环境下的权限navigator.permissions
),
即使该实例无可见状态时,伪命名空间很有用。
6.2. 属性应像数据属性一样工作
[WEBIDL]属性应表现为简单JS对象属性。
实际上IDL属性是访问器属性(getter/setter)。 要让它们像JS对象属性:
-
getter不能有可观察的副作用。
-
getter不应执行复杂操作。
-
保证
obj
总为真,不能每次返回新值。. attribute=== obj. attribute -
尽量保证
obj
后. attribute= xobj
为真(如需转换则不强求)。. attribute=== x
如属性不这样工作,建议用方法替代。
6.3. 考虑对象应为实时还是静态
API如访问表示内部状态的对象,需决定该对象是否随状态变化而实时更新。
始终表示当前状态的是实时对象, 只表示创建时状态的是静态对象。
实时对象
如对象允许作者变更内部状态,应为实时对象。
例如DOM的Node
都是实时对象,便于作者按当前状态修改文档。
实时对象属性可在访问时计算,而非创建时。 若数据计算复杂,实时对象无需提前算好所有数据。
实时对象也可节省内存,无需为静态版本复制数据。
静态对象
如对象表示可能变化的列表,多数情况下应为静态对象。这样遍历列表时无需担心中途列表变化。
getElementsByTagName
返回实时对象表示列表,因此遍历时需谨慎:
let list= document. getElementsByTagName( "td" ); for ( let i= 0 ; i< list. length; i++ ) { let td= list[ i]; let tr= document. createElement( "tr" ); tr. innerHTML= td. outerHTML; // 此操作会把td从列表移除,导致遍历结果不可预测 td. parentNode. replaceChild( tr, td); }
querySelectorAll()
会返回静态对象,正是因为规范作者发现getElementsByTagName
导致问题。
URLSearchParams
虽然表示列表,但不是静态对象,因为它用于变更URL的查询字符串。
注意:对maplike和setlike类型此建议未必适用,因为这些类型设计时就考虑了迭代时变更。
如无法在访问时计算属性,静态对象可避免为未使用的对象持续更新直至被回收。
如静态对象表示频繁变化的状态,应通过方法返回,而不是作为属性。
另见:
6.4. 访问器应表现为属性而非方法
IDL属性描述对象属性或getter,产生对象状态信息。
-
getter不能有(可观察)副作用。有副作用时应用方法。
-
getter不应抛异常。getter应表现为普通数据属性,数据属性读取不会抛异常。无效状态应通过拒绝写入而非读取时抛异常。已有getter更新为抛异常会破坏向后兼容,因API用户枚举或包装API时未预期异常。
-
getter不应执行阻塞操作。如需阻塞,应用方法。
-
对象未变时,getter每次应返回同一对象,即
obj.property === obj.property
始终成立。每次返回新值不允许,否则应用方法。
注意:阻塞操作反例如getteroffsetTop
会执行布局。
定义IDL属性时,尽量保存setter赋值后getter可返回的原值,即obj.property = x
后obj.property === x
为真(如需归一化或类型转换可酌情)。
返回对象可为实时或静态:
-
实时时,每次返回同一对象,直到状态变化需返回新对象。可由属性、getter或方法返回。
-
静态时,每次返回新对象,应用方法返回。
6.5. 可选与基础参数应用字典传递
API方法应优先用字典参数,而不是一串可选参数。
这样调用代码更易读,方法签名更易记,也便于后续扩展,尤其有多个同类型参数时。
new Event( "example" , { bubbles: true , cancelable: false })
比下面可读性更高:
new Event( "example" , true , false )
强制参数也可考虑用字典传递,如此更易读,尤其是基础类型参数。
字典本身应为可选参数, 这样如作者接受所有默认选项,可不传额外参数。
另见:
6.6. 方法参数应尽可能可选
如API方法参数有合理默认值,应设为可选并指定默认值。
布尔参数建议默认值为false
。
默认值应为大多数作者会选择的值,如明显选项。布尔属性名也应选用让false
为常用值。
API列表类型参数建议如下:
-
方法参数用
sequence<T>
-
方法返回值用
sequence<T>
-
属性用
ObservableArray<T>
另见:
6.7. 合理命名可选参数
可选参数命名应让默认行为一目了然,且不要用否定命名。
addEventListener()
接收一个包含once
选项的对象,表示监听器不会重复调用。
该选项原可命名为repeat
,此时默认应为true
。API作者未用noRepeat
,而用once
,这样默认行为无需否定命名。
其它例子:
-
passive
优于active
-
isolate
优于connect
-
private
优于public
另见:
6.8. 合理使用重载
如方法行为因参数变化而显著不同,通常应定义不同方法,而不是重载一个方法。
如方法某参数可为单值或数组,适当重载有用。传递选项字典也是好模式,灵活性更高。
但如后续参数因首参改变,或参数类型决定方法行为,可能影响代码可读性和API功能发现,不建议这样做。
6.9. 类应尽量有构造函数
API中的类如适用应有构造函数。
默认[WEBIDL]接口生成不可构造类:用new X()
实例化会抛TypeError
。
要可构造,需为接口添加构造函数操作并定义实例创建算法。
这样JS开发者可实例化类以测试、mock或与第三方库交互,也能继承该类,否则JS无法继承。
但并非所有场景都适用。例如:
-
部分对象表示特权资源,需工厂方法创建以访问资源。
-
部分对象生命周期受控,需专用方法创建和访问。
-
部分对象为抽象基类,不应实例化也不应被继承。
Event
类及其所有派生接口均可构造,便于测试事件处理代码,作者可构造Event
传递给事件处理方法。
Window
类不可构造,因为创建新窗口是特权操作有副作用,应用window.open()
方法创建。
ImageBitmap
类不可构造,因其表示不可变、可绘制的位图,生成过程需异步。应用createImageBitmap()
工厂方法创建。
DOMTokenList
类目前不可构造,导致无法为自定义元素暴露其token属性为DOMTokenList
。
Navigator
、
History
、
Crypto
等类不可构造,因为它们是单例,表示每窗口信息访问。
这种场景可用WebIDL命名空间特性,但这些特性设计早于命名空间,且超出现有命名空间能力。
如API需此类单例,建议用命名空间,如用有问题可反馈WebIDL问题。
工厂方法可辅助构造函数,但一般不应完全替代。如有基类和多个子类时,工厂方法可根据参数创建合适子类。通常工厂方法是派生类的静态方法。
6.10. 在合适场景下保持同步
在可能的情况下,设计新API时优先使用同步API。
同步API更易于使用,无需搭建额外的基础设施(如将函数声明为async
)。
一般来说,API应为同步,如果满足以下经验法则:
-
API不需要权限提示或设备选择等弹窗。
-
API实现不会因锁、文件系统或网络访问(如进程间通信)而阻塞。
-
执行时间短且确定。
6.11. 用Promise设计异步API
如API方法需异步,使用Promise而非回调函数。
在Web平台统一使用Promise,便于API组合使用,例如链式调用Promise。 用Promise的代码通常比用回调函数的代码易懂。
API可能需要异步,如果:
-
用户代理需要弹窗请求权限,
-
部分信息需从磁盘读取或从网络请求,
-
用户代理需在其他线程或进程做大量工作才能返回结果。
另见:
何时使用回调函数
仅在需要立即响应时使用同步回调。
某些API场景需同步操作,特别是涉及延迟敏感行为时。 例如,浏览器需在事件冒泡结束后立即决定后续处理,媒体处理不能总等到下一个任务边界。 同步行为无法用Promise建模,应使用显式回调或事件。
在Promise reaction回调中, 不能要求在固定数量microtask内完成操作。
这样能保护Promise组合:如await
、包装辅助函数、日志、Promise.race()
或多个.then()
分支都会多排一个microtask,不应明显改变被包裹代码行为。
事件处理器是同步调用和取消的。
开发者代码如需调用
event.
或用
preventDefault()
fetchEvent.
选择响应对象,都需要用回调而不是传给
respondWith()
Promise.
的反应。
then()
非事件的例子如
captureController.
方法,会把用户选定的屏幕捕获窗置前。应用可根据用户选择决定。
问题是延迟处理会让站点有机会进行点击劫持攻击。
setFocusBehavior()
不是要求在getDisplayMedia()
的promise
reaction microtask内调用该方法,
而是要求在同一promise reaction任务中调用,且有1秒超时限制。
6.12. 用AbortSignal取消异步API/操作
如异步方法可取消,允许作者通过选项字典传入AbortSignal
。
const controller= new AbortController(); const signal= controller. signal; geolocation. read({ signal});
统一用AbortSignal
取消异步操作,可让代码更简单。
比如,可用一个AbortSignal
控制多个并发操作,再用对应AbortController
一次性取消(如用户按“取消”,或SPA导航时)。
即使不能保证一定能取消,也可以用AbortController
,
因为abort()
是请求而非保证。
6.13. 常量与枚举用字符串表示
常量或枚举值用字符串表示。
字符串方便开发者检查值、阅读相关代码。 JS引擎用整数没性能优势。 WebIDL枚举类型的值也是字符串。
如需表示多个属性组合(其他语言用位掩码),用字典对象代替。 字典对象可像单一位掩码一样传递。
WorkerType
参数设置。
只有两个值("classic"和"module"),本可用布尔类型,字符串无损且代码更清晰。
6.14. 如需同步与异步方法,默认异步,同步为例外
如极少数场景需同步与异步方法共存,默认用异步,同步为例外。
因平台当前限制,部分Web平台领域缺乏异步支持,因此同时支持同步方法有助于可用性。 这类情况应视为例外,且应有将同步方法弃用的明确路径,随平台能力提升。
多数情况下,同步变体应以Sync()
后缀区分。
6.15. 字节数组用Uint8Array返回
API如返回字节数组,应用Uint8Array
,而不是ArrayBuffer
。
ArrayBuffer
无法直接读取,开发者需要创建如Uint8Array的视图来读取数据。
直接提供Uint8Array
可省去此步骤。
如缓冲区字节可自然解释为其他TypedArray类型,则应用对应类型。
例如字节表示Float32值时,应用Float32Array
。
6.16. 有副作用的函数应返回undefined
如函数目的是产生副作用而非计算值,应指定返回undefined
。
站点通常不会依赖此返回值,因此未来如有新需求可轻松改为返回有意义值。
HTMLMediaElement
的
play()
方法最初定义为返回undefined
,
因其目的是改变媒体元素状态。
播放请求可能因多种原因失败,因此play()
被改为返回Promise
。
如果API最初定义为返回非undefined
(如返回媒体元素,用于API链式调用),则不兼容于后续增强此API可用性。
另见:
7. 事件设计
7.1. 一次性事件用Promise实现
遵循Promise规范编写指南中的一次性事件建议。
7.2. 可取消事件不要用Promise实现
参见何时使用回调函数。
7.3. 事件应在相关Promise resolve前触发
如Promise异步算法会调度事件,应在Promise resolve前调度事件。
Promise resolve时,会排队microtask运行reaction回调。 microtask在JS栈清空时处理。 事件分发是同步的,监听器之间JS栈会清空。 因此,若Promise resolve在相关事件分发前,所有因Promise reaction排队的microtask会在事件第一个和第二个监听器间调用。
先分发事件可避免这种交错。所有事件监听器都在任何Promise reaction回调前调用。
7.4. 不要自创事件监听机制
创建允许作者开始/停止通知生成过程的API时,用现有事件机制监听通知。 为底层过程提供独立API控制。
BluetoothRemoteGATTCharacteristic
全局对象上提供startNotifications()
方法,将对象加入“活动通知上下文集”。
用户代理收到蓝牙设备通知时,会在活动通知上下文集内的BluetoothRemoteGATTCharacteristic
对象上触发事件。
参见:
7.5. 始终添加事件处理属性
API如添加新事件类型,应在所有可能处理该事件的EventHandler
接口上添加对应的onyourevent
事件处理IDL属性。
持续定义事件处理IDL属性很重要:
-
有助于平台一致性
-
可用于事件支持的功能检测(见§ 2.6 新特性应可检测)
如事件需被HTML和SVG元素处理,应在GlobalEventHandlers
接口mixin上添加事件处理IDL属性,而不是直接加到相关元素接口。
同理,窗口事件应加到WindowEventHandlers
,而不是Window
。
7.6. 用事件实现通知
事件不应用于触发变更,只用于通知变更已完成。
7.7. 防范递归风险
API如包含耗时或复杂算法,需防止算法正在运行时再次调用。
API方法如启动长算法,应用事件通知用户代码进度。但用户代码处理事件时可能再次调用API方法,导致复杂算法递归运行。相同事件可能再次触发,相同处理器又被调用,依此类推。
为防止此情况,应确保“递归”调用API方法时立即返回,不再进入算法。此技术称“守卫”算法。
AbortSignal
的
add、
remove和signal abort
都会先检查信号是否aborted,
如已aborted,算法不再继续。
此处很多复杂逻辑在signal abort步骤中实现, 这些步骤会遍历算法集合,由add和remove方法管理。
例如,
ReadableStreamPipeTo
会在AbortSignal
的算法集合中注册算法,
当触发signal abort步骤时执行,
通过abort()
调用关联的AbortController
。
此算法可能会resolve Promise导致代码运行,其中包括对AbortSignal
方法的调用。
由于signal abort会遍历算法集合,
运行期间不应允许集合被修改。
而且signal abort触发的代码可能又导致递归调用signal abort, 因此如信号已在signal abort步骤中,不应重复运行,防止递归。
注意:提前终止时如算法本会保证关键状态一致,需确保提前终止前调整状态,否则会造成状态不一致和用户可见bug。
注意:提前终止时抛异常需谨慎,考虑开发者调用场景及是否预期处理异常,例如此算法是否唯一会抛异常?
并非总能用守卫技术。算法如有太多入口无法全部检查时,可选择延迟调用作者代码到后续任务或microtask。 这样可避免递归栈,但仍可能陷入无休止的后续任务循环。
如算法可能在不同线程/进程运行,事件应始终延迟,以确保在正确任务队列处理。
守卫与延迟都有权衡:
守卫算法保证:
-
事件触发时,守卫算法结束到事件触发间状态不会变化。
-
算法中触发的事件(如状态变更通知事件)可立即触发,无需等下一个任务。
-
事件处理器中可直接访问实例对象的相关状态,无需事件中额外附带状态。
如事件选择延迟:
-
无法保证算法完成后事件一定排在任务队列首位。
-
任何其它任务都可能改变对象状态,需在事件中包含相关状态。
-
通常需新建Event子类带新属性保存状态。
例如
ProgressEvent
有loaded
、total
等属性。
-
-
如算法不同部分需协调,需定义状态机(明确状态转换),确保延迟事件触发时状态检查/修改行为可预期。
如Payment Request API中,
PaymentRequest
的[[state]]
内部槽显式记录对象状态转移。 -
如延迟事件无需额外状态或状态机,可能仅用于通知算法完成。此时API应返回
Promise
而非触发事件。 见§ 7.1 一次性事件用Promise实现。
注意:本节所述暴露递归可能性的事件有时称“同步事件”,不建议使用此术语,因为暗示可以异步分发事件。所有事件都是同步分发的。所谓“异步事件”其实是延迟触发事件。
7.8. 状态建议使用普通Event
如有可能,建议使用指定type
的普通Event
,
并将状态信息保存于target
对象。
通常不需要为Event
创建新的子类。
7.9. 合理使用事件与观察者
一般建议优先使用EventTarget
和通知用Event
,
而不是Observer模式,除非你的特性无法良好适配EventTarget
。
采用EventTarget
可让你的特性受益于基类的改进,比如新加入的once
选项。
如果使用事件会带来问题,如不可避免的递归,则可考虑采用Observer模式。
MutationObserver
、
Intersection Observer、
Resize Observers、
IndexedDB Observers
都是Observer模式的例子。
MutationObserver
替代了已废弃的
DOM Mutation Events,
因开发者发现
DOM Mutation Events
-
触发频率过高
-
没有事件传播,导致太慢不实用
-
容易导致递归,难以防范
Mutation Observer的优势:
-
可以批量收集变更,在变更应用后统一通知观察者;
-
无需经历事件捕获和冒泡阶段;
-
提供更丰富的API以表达发生了哪些变更。
注意:事件也可以批量通知,但DOM Mutation Events未设计为此。事件不总需要参与事件传播,但针对DOM节点的事件通常会参与。
Observer模式的工作方式如下:
-
每个Observer实例在构造时传入回调函数,可选传入自定义观察选项。
-
实例通过
observe()
方法开始观察目标对象,传入要观察的目标引用。选项可在此处传入而不是构造时。发生变化时会调用构造时传入的回调。 -
回调接收变更记录,包含变化的详细信息。可一次性接收多个记录。
-
作者可通过
unobserve()
或disconnect()
方法停止观察。 -
可选地,提供一个方法立即返回未送达的变更记录,如
takeRecords()
。
IntersectionObserver
典型用法如下:
function checkElementStillVisible( element, observer) { delete element. visibleTimeout; // 处理任务队列中的观察结果 processChanges( observer. takeRecords()); if ( 'isVisible' in element) { delete element. isVisible; logAdImpressionToServer(); // 停止观察该元素 observer. unobserve( element); } } function processChanges( changes) { changes. forEach( function ( changeRecord) { var element= changeRecord. target; element. isVisible= isVisible( changeRecord. boundingClientRect, changeRecord. intersectionRect); if ( 'isVisible' in element) { // 元素变为可见 element. visibleTimeout= setTimeout(() => { checkElementStillVisible( element, observer); }, 1000 ); } else { // 元素变为隐藏 if ( 'visibleTimeout' in element) { clearTimeout( element. visibleTimeout); delete element. visibleTimeout; } } }); } // 创建IntersectionObserver实例 var observer= new IntersectionObserver( processChanges, { threshold: [ 0.5 ] }); // 观察广告元素 var ad= document. querySelector( '#ad' ); observer. observe( ad);
(示例代码改编自IntersectionObserver explainer。)
如需使用Observer模式,你需定义:
-
新的Observer对象类型
-
用于观察选项的对象类型
-
用于变更记录的对象类型
额外工作换来的好处:
-
实例可在观察时或创建时自定义。Observer的构造函数或observe方法可传入选项,每个回调可定制观察内容。
addEventListener()
无法做到。 -
可通过Observer的
disconnect()
或unobserve()
方法轻松停止多回调监听。 -
可提供如
takeRecords()
方法,立即获取相关数据,无需等待事件触发。 -
Observer为单一用途,无需指定事件类型。
Observer和EventTarget
的共同点:
-
都可在创建时自定义。
-
都能批量收集并随时分发事件。
EventTarget
不必同步,可用microtask、idle、动画帧等时机,无需Observer实现特殊时序或批量。 -
都无需参与DOM树(冒泡/捕获和取消)。大多数
EventTarget
是DOM树中的Node
, 但很多事件是独立的,如IDBDatabase
、XMLHttpRequestEventTarget
。 即使用Node
,事件可设计为不冒泡不可取消。
IntersectionObserver
(作为EventTarget
子类)的例子:
const io= new ETIntersectionObserver( element, { root, rootMargin, threshold}); function listener( e) { for ( const changeof e. changes) { // ... } } io. addEventListener( "intersect" , listener); io. removeEventListener( "intersect" , listener);
与Observer版本对比:
-
用相同选项观察多个元素更麻烦;
-
无法立即请求数据;
-
移除同一事件的多个监听器更繁琐;
-
作者需额外指定重复的
"intersect"
事件类型。
两者共有:
-
都能批量处理;
-
时序一致(基于JS事件队列);
-
作者仍可自定义监听内容;
-
事件不参与捕获或冒泡。
这些设计均可实现上述特性。
另见:
8. Web IDL、类型与单位
8.1. 合理使用数值类型
如设计API需用数字,建议采用如下[WEBIDL]数值类型,除非有特定原因:
unrestricted double
-
任意JavaScript数字,包括无穷和NaN
double
-
任意JavaScript数字,排除无穷和NaN
- [
EnforceRange
]long long
-
JavaScript数字,范围-263到263,四舍五入为整数。超出范围将抛出
TypeError
。 - [
EnforceRange
]unsigned long long
-
JavaScript数字,范围0到264,四舍五入为整数。超出范围将抛出
TypeError
。
JavaScript只有一种数值类型Number
:
IEEE 754双精度浮点,含±0、±Infinity和NaN。
[WEBIDL]数值类型
规定了如何将任意JavaScript数字转换为具有特定属性的子集。
无论是方法还是属性setter,数字传给IDL接口时会应用这些规则。
如需额外规则,可在算法中指定。
octet
(8位,[0, 255]),会取数字的模。如将300转为octet
,结果为300模255,即45,可能令人意外。
可用[EnforceRange
]octet
,超出范围则抛TypeError;
或[Clamp
]octet
,超出范围则截断为最大值(如300变255)。
bigint
仅用于期望超出253或小于-253的值。
API不应同时支持BigInt
和Number
,
无论是多态支持还是分别提供只差类型的API。
否则会因隐式转换丢失精度,违背BigInt
的初衷。
8.2. 合理表示字符串
设计操作字符串的Web平台特性时,除非有特定原因,建议用DOMString
。
大多数字符串操作无需解释字符串中的码元,因此DOMString
是最佳选择。
特定场景下可用USVString
或ByteString
,详见下文。[INFRA] [WEBIDL]
USVString
是Web IDL用于表示标量值字符串的类型。
若字符串常用算法操作标量值
(如百分号编码),
或操作无法处理输入中的代理项
(如需传递字符串到原生平台API),
应用USVString
。
反映IDL属性
若内容属性定义为包含URL
(如
href
),
应用USVString
。
[HTML]
ByteString
仅用于表示如HTTP协议这类不区分字节和字符串的数据,不是通用字符串类型。如需表示字节序列
,请用Uint8Array
。
8.3. 时间度量统一用毫秒
如果你设计的API需要时间度量,统一使用毫秒作为单位。
即使秒(或其他单位)在API领域更自然,统一用毫秒能保证API之间的互操作性。 这样作者无需转换时间单位,也不用记住不同API用何种单位。
这种惯例起源于setTimeout()
和
Date
API,
并延续至今。
注意:高精度时间通常用浮点数表示分数毫秒, 而不是用更小单位如纳秒的整数。
8.4. 用合适类型表示时间和日期
在平台上表示日期时间时,使用DOMHighResTimeStamp
类型。
DOMHighResTimeStamp
可用于比较时间戳,不受用户本地时间影响。
DOMHighResTimeStamp
的值表示以毫秒为单位的时间值。
详细见[HIGHRES-TIME]。
不要用JavaScript的Date
类来表示具体的日期时间值。
Date
对象是可变的(值可更改),且无法使其不可变。
Date
,
见下述链接:
-
Frozen date objects?(es-discuss)
-
从Web IDL移除Date (Web IDL Bugzilla)
8.5. 错误用Error或DOMException表示
Web API中的错误应用ECMAScript错误对象(如Error
)
或DOMException
表示,
无论是异常、Promise拒绝值还是属性。
9. 封装设备/浏览器能力的API
现在Web平台正开发与设备交互的新API。 例如,作者希望用Web连接 麦克风和摄像头、 通用传感器(如陀螺仪、加速度计)、 蓝牙和 USB外设、 汽车等。
同样也适用于主机系统或外部服务可选提供的能力,包括需用户付费访问的能力。
这些能力可由底层操作系统或本地第三方库提供。 API可作为“封装器”抽象原生功能, 不引入重大复杂性,同时保证浏览器API安全性。 这些称为封装API(wrapper APIs)。
本节包含设计此类能力API时需考虑的原则。
9.1. 不要暴露多余的能力信息
遵循数据最小化原则, 如需让网站访问能力信息,只暴露完成任务所需的最少数据。
首先,认真考虑是否真的需要暴露这些信息。 评估你的用户需求能否用更弱的API满足。
暴露设备存在、附加信息或设备标识符,都会增加用户隐私受损风险。
用户拒绝访问设备或能力时,不应暴露该能力是否存在。 在此场景减少信息泄露比授权后更重要。
信息越具体, 指纹数据 被网站收集的可能性越大, 信息也越可能涉及用户敏感内容。
如无法设计更弱的API,暴露设备信息时应遵循以下原则:
- 标识符信息要尽量少
-
设备标识符只包含最少可识别信息, 例如品牌、型号等建议不用。 通常可用随机生成的标识符代替。 确保标识符不可猜、不复用。
- 用户掌控
-
用户清除浏览数据时,应一并清除设备标识符。
- 敏感信息需权限保护
-
如无法匿名创建设备标识符,应限制访问。 用户必须能对网页访问此信息有意义地同意。
- 绑定标识符到同源模型
-
同一物理设备应为每个origin生成唯一标识符。
如同一origin多次请求同一设备,返回同一标识符(除非用户已清除数据)。 这样作者不用处理同设备多份副本。
- 如需持久化
-
如设备标识符生成耗时,应允许作者保存,以便下次会话使用。 需保证同origin同设备每次生成标识符一致。
另见:
9.2. 谨慎开放设备选择与枚举API
尽量避免枚举设备。如无法避免,暴露的信息要最少。
如API暴露多个设备的存在、能力或标识符, § 9.1 不要暴露多余的能力信息中的风险会按设备数倍增。 同理,应评估你的用户需求能否用更弱API满足。[LEAST-POWER]
如API目的是让用户选择设备, 可能无需向脚本暴露设备列表。 可用由用户代理提供的设备选择器API即可。 这种API:
-
用户掌控,
-
未获得用户同意前不暴露任何设备信息,
-
默认不暴露用户环境指纹数据,
-
每次只暴露一个设备的信息。
设计允许用户选择设备的API时,可能也需暴露“有设备可选”这一信息。 这会给网站带来一位指纹数据,因此不如完全不暴露的API安全。
RemotePlayback
接口不暴露可用远程播放设备列表。
而是允许用户通过用户代理的设备选择器选取设备。
它能让网站检测是否有可用远程播放设备, 网站可据此显示或隐藏用于弹出选择器的控件。
权衡在于这种API让作者多获得一点信息, 但能让UI不易让用户困惑——只有有设备时才显示弹出按钮。
如必须暴露设备列表, 尽量只暴露满足用户需求的最小子集。
比如,API可让网站请求筛选或约束过的设备列表, 这样设备数量更少。 但如允许作者用不同约束多次请求,仍可能获得全部列表。
最后,如必须暴露某类设备的完整列表, 请务必严格规定设备的排列顺序。 这样可减少互操作性问题,也有助于缓解指纹风险。 (排序也可能暴露信息,见指纹缓解 § 6.2 标准化)
注意:API不应按实现自定义顺序暴露完整设备列表, 但为Web兼容性有时不得不这样做。
9.3. 以用户需求为基础设计,而非底层能力
将原生能力带入Web时,应以用户需求为基础暴露新能力。
避免直接翻译现有原生API到Web。
而应分析原生API能实现的功能及对应的用户需求,设计满足这些需求的API,即使实现依赖原生API。
尤其要注意不要直接暴露底层原生API的生命周期和数据结构。如有可能,考虑对新硬件的灵活适配。
也就是说,新API设计应以预期用法为依据,而非底层硬件、设备或原生API当前实现方式。
9.4. 积极保障安全
将原生能力带到Web平台时,应尽量防御性设计。
原生能力引入Web平台会带来诸多影响。 用户可能不希望网站知道其计算机有何能力。因此,任何跨越逻辑origin边界的访问都应权限控制。
例如,若设备可存储状态,且该状态可被多个origin同时读取, 则一组可读写状态的API实际上就是一个破坏Web同源模型的旁路通道。
因此,即使设备允许非独占访问,也建议按origin强制独占访问,甚至进一步限制为仅当前激活标签页可访问。
此外,API设计时应确保应用能优雅处理物理中断,比如设备被拔掉。
9.5. 用Web平台原则适配原生API
将原生操作系统API适配到Web时,需确保新Web API遵循Web平台设计原则。
- API应可多平台实现
-
设计封装API时,要考虑不同平台如何提供其功能。
理想情况下所有实现都完全一致,但有时确实需要暴露仅支持部分平台的选项。 如遇此情况,应说明作者如何编写可在所有平台运行的代码。 见§ 2.6 新特性应可检测。
- 底层协议应开放
-
与外部硬件或服务交换数据的API,不应依赖闭源或专有协议。 依赖非开放协议会削弱Web开放性。
- API设计应兼容离线场景
-
如API依赖远程服务,需保证用户因各种原因无法访问服务时API仍能正常工作。
- 避免增加指纹追踪面
-
封装API可能无意间增加用户被指纹追踪风险。 请参考TAG关于未授权追踪的说明。
10. 其他API设计考量
10.1. 为新功能支持polyfill
polyfill对新功能推广极为有益。 技术架构组TAG关于Polyfill与Web演进的建议, 在开发新功能时应参考,特别包括:
-
“可polyfill”不是必须,但确实有好处
-
鼓励开发polyfill
10.2. API尽可能支持dedicated worker
开放新功能时,请考虑是否适合暴露给dedicated worker(通过DedicatedWorkerGlobalScope
接口)。
许多功能本可直接在dedicated worker上运行,不支持会限制用户以非阻塞方式运行代码。
支持dedicated worker时存在一些挑战,尤其是需要用户输入(权限弹窗、选择器等)时。 虽然这些困难可能让规范作者不愿支持dedicated worker,但仍建议设计时考虑dedicated worker支持, 以免后续暴露API时遇到不必要的障碍。
10.2.1. 部分API仅暴露给dedicated worker
开发者更倾向于用简单代码,因此通常会用API的最简方式。
应避免添加阻塞渲染的功能。见§ 3.4 避免阻塞渲染的功能
如API最简用法易导致渲染阻塞或页面“卡顿”,用户体验会变差。 (在低性能设备上问题更突出,这些设备更多被弱势群体使用。请记住,Web是为所有人服务。)
因此,易阻塞主线程的API,不应暴露在Window
接口。
限定这类API只在DedicatedWorkerGlobalScope
接口暴露,
能让开发者默认获得最佳用户体验。
ScriptProcessorNode
在Web Audio API中已被AudioWorklet
替代,
因为主线程使用ScriptProcessorNode
经常导致用户体验很差。[WebAudio]
10.3. 仅纯计算能力可全局暴露
开放新功能时,请评估是否适合在所有环境全局暴露
(通过[Exposed=*]
注解或所有全局接口)。
只有纯计算能力才可全局暴露。 即不执行I/O,也不影响用户代理或用户设备状态。
TextEncoder
接口将字符串转为UTF-8字节,是纯计算接口,通用性强,应该全局暴露。
localStorage会影响用户代理状态,因此不应全局暴露。
技术上console
可能影响用户代理(如开发者工具日志)或设备(如写日志文件),
但这些影响无法被运行代码观察,且console全局可用的实用性远大于弊端。
此外,依赖事件循环的功能不应全局暴露。 并非所有全局环境都有事件循环。
timeout
方法依赖事件循环,不应全局暴露。
其它AbortSignal
功能为纯计算,可全局暴露。
[Exposed=*]
注解也需谨慎使用。
如某功能在未全局暴露的其它功能缺席时用处不大,则默认也不全局暴露。
Blob
接口是纯计算,但Blob对象主要用于I/O或由I/O产生。
按保守原则,Blob不应全局暴露。
10.4. 规范添加新数据格式
新增数据格式时,务必定义对应MIME类型,并扩展现有API支持此类型。
Web新能力有时涉及新增数据格式,如图片、视频、音频、文本或浏览器需处理的其他数据类型。 新格式应使用严格验证的标准MIME类型,文本类新格式仅支持UTF-8编码文本。
虽有历史媒体格式未严格要求MIME类型(有时依赖头部检测), 这仅用于兼容旧格式,不应作为新格式设计标准。
规范作者还应将新格式集成进现有API,使其在输入(如ReadableStream解码)和输出(如WriteableStream编码)都安全可用。
例如添加新图片格式时,先定义新MIME类型,再为HTMLImageElement添加对应解码器(及编码器),
并扩展HTMLCanvasElement.toBlob()
和HTMLCanvasElement.toDataURL()
等输出接口。
浏览器为兼容旧格式支持MIME类型嗅探,但不建议扩展模式匹配算法, 因为有安全隐患。新格式建议严格使用MIME类型。
新MIME类型应有规范,并向IANA注册。
10.5. 优先扩展已有manifest文件而非新建
如果你的功能需要manifest文件, 请先考虑是否可以扩展现有的manifest规范。
新的Web特性应尽量做到自包含和自描述,理想情况下不需要额外的manifest文件。 现有的一些manifest文件包括:
-
Web App Manifest,包含与Web应用相关的功能。
-
Payment Method Manifest,用于Web支付API中的支付方式。
-
Publication Manifest,用于部分Web出版物工作组标准。
-
Origin Policy,用于设置安全策略。
我们鼓励大家优先扩展已有的manifest文件。 应尽量将变更纳入原规范,或至少与规范编辑讨论扩展内容。 讨论将有助于达成更优的设计,并更好地与平台集成。
设计manifest新键值时,请确保它们确实有需求(即能实现明确的用例)。 同时请检查是否已有类似的键。如果现有键值对基本满足需求,优先协作扩展原规范以支持你的用例。
但如果你的功能需要大量特定领域的复杂元数据,则新建manifest可能是合理的。
如果manifest文件的领域与现有manifest不同,可能需要新建manifest文件。 例如,抓取时机不同,或manifest复杂度较高。应用元数据应加到Web App Manifest或作为其扩展。 专用于特定应用或需与非浏览器互操作的manifest,可能需采用不同方式。 Payment Method Manifest、Publication Manifest和Origin Policy就是此类例子。
举例来说,如果你只有一条元数据,即使抓取时机与现有manifest不同,也建议使用现有manifest(或最好让设计不需manifest)。 但如确实需要大量特定领域的复杂元数据,新建manifest则是合理选择。
注意:所有情况下,命名规范应保持一致(参见§ 12 命名原则)。
注意: 按原则,现有manifest都采用小写、下划线分隔命名。 有时需要在DOM API中复用manifest中的字典,这时名称需转换为驼峰式。 例如image resource。 因此,如果一个键可以明确用单词表达,建议这样做。
10.6. 序列化时要考虑消费者
当你添加或扩展涉及解析器或序列化器的功能时,应考虑序列化的效果。序列化结果的相关方包括:-
用户——因为序列化结果可能直接展示给最终用户
-
工具——可能以多种方式依赖序列化输出(比如判断解析器是否需要纠正输入错误)
-
Web API——因为序列化结果可能作为参数传入其它Web平台API
要考虑语言特定的预期——例如,在某些语言中,空白的有无可能很重要。 不同语言对浮点数精度的接受程度也有差异。 序列化结果:
-
应符合开发者预期(比如序列化CSS属性时,输出不应与CSS作者手写的差异很大)
-
幂等性——解析器输出的序列化结果应该能反复解析-序列化后仍得到自身
-
不应加剧错误积累——把API序列化输出反复输入API,内部状态应保持一致
10.7. 确保功能对开发者友好
任何新功能都应对开发者友好。 虽然“友好”难以量化,至少要考虑以下几点。
异常中的错误文本应当通用,而开发者工具(如开发者控制台)中的错误信息必须有意义。 当开发者遇到错误时,信息要具体,不能过于宽泛。
理想情况下,面向开发者的错误信息应包含足够内容,帮助定位问题。
声明式特性(如CSS),实现时可能需额外工作以提升调试性。 规范中定义调试要求不仅让功能更友好,也保证用户开发体验一致。
一个将调试性作为规范一部分的好例子是Web Animations。
10.8. 使用最佳加密算法,并预期其不断演进
只采用经过安全领域专家公正评审的加密算法, 并确保所选算法经过验证且为最新。 加密协议和算法不仅可能过时或变得不安全,还会快速演进。
10.9. 不要通过客户端提示暴露新信息
使用Client Hints时,不要暴露网页本身无法访问的新信息。
客户端提示是重要优化手段,但不能成为暴露信息给网站的唯一方式。正如 RFC 8942 §4.1 中定义Client Hint时所说:
因此,依赖本文件定义Client Hint头的特性 不得提供应用程序本不能通过用户代理获得的新信息,如已有请求头、HTML、CSS或JavaScript。
如你要添加新客户端提示,且其暴露了网页无法通过其它方式获取的信息, 请优先考虑通过API暴露这些信息。
11. 编写高质量规范
本文档主要关注Web API设计, 但API设计者也应撰写他们所设计API的规范。
11.1. 明确规范要求的目标读者
既要描述作者如何用你的API写出优质代码,也要说明API实现者如何处理写得不好的代码。
与其它平台相比,Web设计目标之一是对格式不规范内容的强容错, 使得用老标准的网页在新用户代理上仍可浏览, 也降低了作者的学习门槛。
为此,Web规范作者需描述如何处理格式不规范内容,以及规范内容。
实现者需要理解“支持的语言”,而不仅仅是作者应使用的“符合规范语言”,因为前者更复杂。
11.2. 规范要完整、避免歧义
规范功能时,要确保信息足够,让作者不必为不同实现编写不同代码。
规范不够具体时,实现者可能做出不同选择,导致作者需写额外代码适配差异。
实现者不应依赖其它实现细节来避免上述问题。 规范本身应足够完整和清晰。
注意:这并不意味着实现不能有不同的渲染,或权限弹窗等用户界面不一样。
注意:如规范信息不明确,应由实现者反馈bug。
11.2.1. 算法需定义清楚
算法应写得清晰简明。
最常见的算法写法是明确的步骤序列,通常类似伪代码。
showModal()
方法以编号步骤描述,清楚说明何时抛出异常,何时运行规范其它部分的算法。
写步骤序列时,想象它是一段函数式代码。
-
明确输入输出,为算法和变量命名,明确算法何处返回结果或错误。
-
尽量避免写有副作用的算法。
在详细说明前先总结算法目的,让读者决定是否需要读步骤细节。 例如下面这些步骤,确保每个顶级浏览上下文最多有一个待处理的X回调。
有时纯步骤序列不是写算法的最佳方式。 比如,可能需要定义或复用正式语法/文法以避免重复,或定义状态机专用状态。 用这些额外结构时,也应遵循前述建议。
尽量按实际实现方式描述算法,这可能让规范书写更难,但有助于实现者直接对照规范实现。 否则不同实现可能做出不同决策,导致后续特性只能在部分实现上可行。
CSS选择器在阅读时是从左到右,但实际实现是从右到左匹配。 这样可以更快判断最具体项是否命中,避免无谓工作。 CSS选择器匹配算法按此方式描述,而不是按CSS作者常见的阅读顺序。
另见:
11.2.2. 用显式标志表示状态
写算法时,避免用文字描述状态,应用显式标志。
显式标志能清楚指明不同错误情况下状态是否变化,以及标志指示的状态何时重置。
11.3. 兼顾互操作性与可实现性
规范功能如同所有工程工作,都需权衡取舍。
有时,即使所有人都尽力,也无法找到可互操作规范,又被所有实现者认同可实现的方案。
选择方案时应以最终用户利益为先。
首先,分析可实现性问题的本质。 可能实现者认为存在用户风险,决定不实现该特性。 此时,最好不要规范该特性,已发布的实现也应撤回。 注意撤回特性可能导致用户与作者困惑。
有时同一API所有引擎都能实现,但行为无法完全互操作。 只要行为有差异,就要考虑最终用户影响。 这种方式的不足是行为差异无法特性检测。 但如果未来实现能统一行为,网站无需更新即可受益。
注意:作者可能认为主流实现的行为是正确的,其它行为是bug,这反而巩固了主流实现地位。
backdrop-filter属性在不同实现间有明显可见的行为差异。 当前维护者认为共享API的好处大于互操作性代价。
如行为差异严重且无法统一,可能更适合规范不同API,各实现者选择可实现的替代方案。
这种方式的风险是:如果实现最终能统一行为,则需要长期支持多个API,增加开发者负担和维护成本。
针对同一用例暴露两个API,迫使作者通过特性检测为不同浏览器编写不同代码。
注意:风险在于作者可能认为主流实现支持的API才是“正确”的,或根本不知道其它实现支持的API,从而进一步巩固主流实现地位。
如选择此方案,应尽量减少API差异。可考虑将非标准部分设为大规范的小组件。
[ENCRYPTED-MEDIA]和[payment-request]规范都通过将非标准组件(如内容解密模块和支付方式)与主API解耦,来最小化API差异。
有时标准组织会规范API,明知部分实现者不愿实现。 作者可用特性检测,仅在API可用时使用,用户在支持API的环境下可受益。 这种结果总有问题,尤其只有一个实现者愿意实现时最糟。 有些标准组织明文禁止规范只有单一实现者感兴趣的功能。 即使不受此限制,也强烈不建议规范仅有单一实现者的功能。
还有可能上述所有方案都不可行。虽最佳选择可能是不规范该功能,但也有风险:某些实现者可能会以非标准API发布该功能。
11.4. 避免monkey patch
monkey patch是指在已有规范基础上叠加新功能,以扩展、重写或修改原规范行为。 Monkey patch通常被视为不良实践,应尽量避免,原因如下,但有时不得不采用(见如何应对不可避免的monkey patch)。示例:规范A定义某内部算法,规范B直接覆盖或修改该算法功能,而不是用公开的扩展点。
monkey patch本质上假设底层功能不会改变,这会导致多个问题:
-
如原规范变动,monkey patch内容可能不再适用,导致失效或困惑。
-
多个规范对同一规范做monkey patch时,应用顺序不同可能导致行为不一致。
-
原规范作者可能不知道你的monkey patch,错失协作修复机会。更糟的是,所修改行为可能与其它问题或行为关联,使问题更复杂。
-
实现者维护代码时仅参考原规范,可能误将monkey patch行为当作bug而回滚。
Wikipedia还描述了monkey patch的其它陷阱。
11.4.1. 如必须monkey patch该怎么做
有时monkey patch不可避免(如新规范设计阶段)。如必须monkey patch现有规范,请确保:-
在你的规范中,明确标记monkey patch为对其它规范的临时变更,用类似如下语言:
-
用引用原文或链接定义项明确指出你要修改的内容。替换整个定义时用链接,修改编号步骤时包含当前编号,但不要只用编号(编号会变)。如要修改定义中多个位置,可将整个定义粘贴到
<blockquote>
中,用<ins>
和<del>
标记变化。在HTTP fetch第4.2步前,“如request的redirect mode为"follow",则将request的service-workers mode设为"none"。”插入如下步骤:
-
新的第一个步骤。
对于CSS2.1块级格式化[CSS2],CSS 2 § 10.3.3中“over-constrained”计算规则应被此处指定的对齐规则取代,相应margin属性的used值不再为over-constraint做修正。
在定义
和URLSearchParams
.delete()
时,追加URLSearchParams
.has()
optional USVString value
参数:partial interface URLSearchParams {undefined delete (USVString name ,optional USVString value );boolean has (USVString name ,optional USVString value ); };按如下方式修改font-size-adjust定义:
Name: font-size-adjust Value: none | [ ex-height | cap-height | ch-width | ic-width | ic-height ]? [ from-font | <number> ] Initial: none Applies to: all elements and text Inherited: yes Percentages: N/A Computed value: a number or the keyword none关键词none,或一组度量关键词和<number>Canonical order: per grammar Animation type: 如关键词不同则离散,否则 by computed value type -
-
保持monkey patch简短。如对算法增补多步,建议在规范中定义单独算法,由monkey patch调用。
-
替换或添加算法步骤时,务必使新步骤能被编辑者直接原样粘贴到上游算法中。控制流如“return”或“abort”应返回或终止整个上游算法,而非仅monkey patch部分。
-
在本社区审查并达成共识后,提交issue至原规范,请上游社区审查你的monkey patch。社区可能建议更优方案,需认真对待。他们或许能告知现有扩展点或快速创建新扩展点,让你比预期更快移除monkey patch。
-
更新上述
<p class="issue">
块,指向你提交的上游issue。 -
如获得足够支持可合并到上游规范,配合规范维护者完成合并。
-
上游规范集成后,从你的规范中移除monkey patch。
注意,monkey patching不同于“模块化”,后者以自包含方式扩展技术,不影响其它底层规范(如CSS模块),被视为良好实践。
通常也可以用WebIDL的partial interface、 partial dictionary等方式扩展规范。 但即使如此,也强烈建议与被扩展规范作者协作。
12. 命名原则
名字的含义来源于:
-
标识(名字本身)
-
使用(人们随着时间理解名字的方式)
-
上下文(如左侧的对象)
12.1. 用常用词命名
API命名必须易读且用美式英语。 要记住大多数Web开发者并非英语母语者。 名字应尽量用大多数英语使用者初见即能理解的常用词汇。
可读性优先于简洁。但短名字通常更清晰。 有时规范中可以用技术术语或著名术语,尤其在API定义处。
12.2. 用ASCII字符命名
名字必须遵守本地语言约束,例如CSS标识符规则等, 并应在ASCII范围。
12.3. 命名要广泛征询意见
API命名要广泛咨询。
你可能会在意想不到的地方找到好名字或灵感。
-
其它平台或主流库对类似API怎么命名?
-
问终端用户和开发者,他们如何称呼API操作或处理的事物。
-
查阅其它Web平台规范,并向相关领域的人员征询意见。
-
同时考虑命名是否具有包容性。
特别关注那些有明确原理说明的建议。
遵循Web一致性命名
在为在其它技术栈中也有暴露的特性或API命名时,应优先采用Web生态系统的命名约定,而不是其他社区的约定。NFC标准用media
指Web中称为MIME
type的内容。
这种情况下,Web NFC的特性或API命名应优先采用与MIME type
一致的命名。
使用包容性语言
应尽量使用包容性语言。
比如, 应用blocklist和allowlist代替blacklist和whitelist, 用source和replica代替master和slave。
如需描述泛指角色,如作者或用户, 应用通用代词“they”、“their”等。 例如:“A user may wish to adjust their preferences”。
12.4. 用描述用途的名字
命名应反映其用途,而不是实现方式。
反映用途的名字更具前向兼容性。 Web上的API很难移除, 所以名字应比实现细节更长久。
尤其要避免用 代码名、 品牌名、 或底层技术细节命名。 这些容易过时,将来可能需要替换。
Remote Playback API没有用它受启发的专有系统名(如Chromecast或AirPlay)命名, 而是选用描述API功能的通用词。[REMOTE-PLAYBACK]
WebTransport API实现了QUIC协议的网络能力, 但名称反映的是更通用的数据传输能力。 [WebTransport][RFC9000]
12.5. 一致命名事物
命名方案应追求一致性,避免混淆。相关名字的集合应在以下方面一致:
-
词性(名词、动词等)
-
否定——比如一组名字应都描述允许的内容,或者都描述被拒绝的内容
布尔属性 vs. 返回布尔值的方法
布尔属性、选项或API参数如询问其参数,
不应以is
为前缀;而无副作用、作用相同的方法应以is
为前缀,以与平台其它部分一致。
遵循现有API的大小写规则
虽然历史上并非总能统一,Web平台API设计已形成如下规则:
大小写规则 | 例子 | |
---|---|---|
方法和属性 (Web IDL属性、操作和字典键) | 驼峰式 | createAttribute() compatMode
|
类和mixin (Web IDL接口) | 帕斯卡式(首字母大写) | NamedNodeMap NonElementParentNode
|
API中的缩写词 | 全大写,但方法或属性首词例外 | HTMLCollection innerHTML bgColor
|
API中重复缩写词 | 同样规则 | HTMLHRElement RTCDTMFSender |
“identity”/“identifier”的缩写 | Id ,但方法或属性首词例外
| getElementById() pointerId id
|
枚举值 | 小写,用连字符分隔 | "no-referrer-when-downgrade"
|
事件 | 小写,连写 |
|
HTML元素和属性 | 小写,连写 | figcaption maxlength |
JSON键 | 小写,下划线分隔 | short_name |
ismap
属性在
img
元素上映射为
isMap
属性(在HTMLImageElement
上)。
JSON键规则适用于通过HTTP发送或磁盘存储的特定JSON文件格式,不适用于一般JS对象键。
重复缩写在平台中尤其不统一,典型反例有XMLHttpRequest
和
HTMLHtmlElement
。
不要效仿,应始终大写缩写,即使重复。
工厂方法名以create
或from
开头
工厂方法命名应以create
或from
开头,后面可加更具体名词。
如工厂方法构造新空对象,用create
前缀;
如从已有数据创建对象,用from
前缀。
工厂方法应是例外,仅在确有合理理由时使用。
典型例子是创建对象还需关联父对象,比如document.createXXX()
。
如需从源对象转换为目标对象,用from
前缀。
比如Foo.fromBar()
表示用Bar
对象创建Foo
对象。
常见通用工厂方法命名为create()
或from()
。
避免发明其它前缀或用遗留前缀,除非为保持同一对象下现有工厂方法一致。
比如document.initXXX()
,但新工厂方法不应再用此约定。
12.6. 危险特性应有警示
如有可能,削弱开发者保障的特性应以“unsafe”开头以便警示。
比如,
内容安全策略(CSP)
可防止某些内容注入漏洞,
但CSP也有削弱自身保障的特性,如unsafe-inline
关键字允许内联脚本,降低CSP的保护能力。
13. 其它资源
其它关于如何编写规范的建议:
致谢
本文档内容集结于TAG现任和前任成员在TAG 设计评审过程中的经验。 感谢所有向我们申请设计评审的人。
TAG感谢 Adrian Hope-Bailie, Alan Stearns, Aleksandar Totic, Alex Russell, Alice Boxhall, Andreas Stöckel, Andrew Betts, Anne van Kesteren, Benjamin C. Wiley Sittler, Boris Zbarsky, Brian Kardell, Charles McCathieNevile, Chris Wilson, Dan Connolly, Daniel Ehrenberg, Daniel Murphy, David Baron, Domenic Denicola, Eiji Kitamura, Eric Shepherd, Ethan Resnick, fantasai, François Daoust, Henri Sivonen, HE Shi-Jun, Ian Hickson, Irene Knapp, Jake Archibald, Jeffrey Yasskin, Jeremy Roman, Jirka Kosek, Kenneth Rohde Christiansen, Kevin Marks, Lachlan Hunt, Léonie Watson, L. Le Meur, Lukasz Olejnik, Maciej Stachowiak, Marcos Cáceres, Mark Nottingham, Martin Thomson, Matt Giuca, Matt Wolenetz, Michael[tm] Smith, Mike West, Nick Doty, Nigel Megitt, Nik Thierry, Ojan Vafai, Olli Pettay, Pete Snyder, Philip Jägenstedt, Philip Taylor, Reilly Grant, Richard Ishida, Rick Byers, Rossen Atanassov, Ryan Sleevi, Sangwhan Moon, Sergey Konstantinov, Stefan Zager, Stephen Stewart, Steven Faulkner, Surma, Tab Atkins-Bittner, Tantek Çelik, Tobie Langel, Travis Leithead, 和 Yoav Weiss 对本文档及其前身HTML设计原则的贡献。
特别感谢 Anne van Kesteren 和 Maciej Stachowiak, 编辑了HTML设计原则文档。
如果你对本文档有贡献但未列名,请通知编辑以便更正遗漏。