一文读懂 web 标准的基石:web IDL
本文为 HTML 标准解读系列文章,其他文章详见这里。
在HTML标准的2.6小节,我们第一次遇到了 IDL 片段,他定义了HTMLALLCollection
的接口:
接下来,像这样的 IDL 片段贯穿了整个标准,或长或短,或简单或复杂。于是,弄懂 web IDL 就变成了一个必须要做的事情了:
不仅仅是 HTML 标准,DOM 标准、ECMAScript 标准也是使用 web IDL 来定义接口的。如果你想读懂任何这些标准,就绕不开 web IDL。
理解 web IDL 可以让你以更专业、更高效的方式了解一个标准定义的对象,而不是使用 MDN 这种二手资料。
理解 web IDL 有助于深刻理解接口之间的继承关系,增加知识碎片的连接,搭建健壮的知识网络。比如,当你在看一个HTMLCollection接口的时候,你会发现至少有这些方法/属性返回值是使用了这个接口:
然后,你还可以看到有以下这些接口继承了 HTMLCollection:
再进一步延伸,你还可以继续查看哪些对象和 API 使用了这些接口。于是,就是这样,原本看是毫无关联的知识便建立了正确且有意义的连接。
本文,我将会基于web IDL标准、并以 HTML、DOM 标准里面的几个 IDL 片段为例子,来为你提供理解 web IDL 的基本框架。我的目标是读者读完本文,能够明白 web IDL 大致怎么一回事,并有底气读懂所有的 IDL 片段。
为什么要有 web IDL?
web IDL(Interface description language),是一门描述接口的语言。
在不同的场景下,接口有不同的含义。在硬件层面,我们有硬件接口,如 USB 接口。在人机交互方面,我们有 User Interface(UI);在客户端和服务端之间,我们还有前后端接口;
但是 web IDL 所指的接口,是面向对象编程语言里,语言层面上的「接口」。以一个 TypeScript 的代码片段为例子:
这个片段定义了一个Pingable
的接口。这个接口规定了:所有实现(implements)这个接口的类,都必须有一个ping
方法,所以Sonar
可以正常被编译,而Ball
会报错。
这个Pingable
,就是面向对象编程语言中语言层面的「接口」。这么做的好处是:Sonar
类的使用者不需要知道ping
具体是怎么实现的,只需要知道他有一个ping
方法就可以了。当ping
方法的实现逻辑进行了变动,比如更换成process.stdout.write('ping!')
的时候,Sonar
类的使用者不需要修改代码来适应新的改动,从而降低了耦合性。
这是一种叫「基于接口而非实现编程」的编程风格:将接口和实现分离,封装不稳定的实现,暴露稳定的接口。 一些编程语言如 Java,天生支持接口类,而像 JavaScript 这样的动态语言只能通过 TypeScript 或鸭子类型间接做到这一点。
同时,这也是每个 web 标准在做的事情:他们定义了各种各样 ECMAScript 对象接口、DOM 元素接口,不同的浏览器是如何实现这些接口的,网页开发者不需要关心,开发者只需要基于接口的定义去进行编码即可。
又为了让这些接口的实现不与特定的语言绑定,于是就有了 web IDL。web IDL 定义了一套描述接口的语言规则,基于这套规则,你可以使用不同的编程语言来实现同样的接口。 于是,一方面你能看到浏览器是使用 C++写的,另一方面,你又能看到像JSDOM这样使用 nodeJS 来实现 DOM 和 HTML 的项目。
web IDL 语法概览
以下我将从四个我认为 web IDL 里最基本、最重要、出现频率最高的方面来展开,这四个部分也构成了整个 web IDL 的基本框架。它们分别是:
接口的成员(members)
接口的继承
Extended Attribute 扩展属性
Mixin 接口和 Partial 接口
从属性、方法到所有成员
我们以开篇提到的HTMLCollection接口作为第一个例子:
如果把这个 IDL 片段翻译成“人话”,大致是这样的:
这是一个名为
HTMLCollection
的接口。这个接口有以下成员:
一个只读属性:类型为
unsigned long
, 名为length
;一个名为
item
的方法,接受一个类型为unsigned long
、名为index
的参数,返回值的类型可能是Element
或者Null
;一个名为
nameItem
的方法,接受一个类型为DOMString
、名为name
的参数,返回值的类型可能是Element
或者Null
。
getter
关键词表示item
和nameItem
是特殊的方法,可以以属性的方式进行访问。所以collection.item(index)
与collection[index]
是等价的;collection.namedItem(name)
与collection[name]
是等价的。
你可以通过在本地测试来加深理解。比如document.images
的值是一个实现了HTMLCollection
接口的对象,于是,你可以通过Object.getPrototypeOf(document.images)
看到该接口声明的所有属性和方法。
一般来说,标准中不会只给你抛出一个 IDL 片段就完事了。如有必要,他会在片段下面解释每个属性或者方法的意义、调用的算法步骤等等。
在 web IDL 中,除了注释以外,所有被大括号{}
扩住的语句都被称之为成员(members) 。上面的 IDL 片段有两种类型的成员,length
属性的成员类型是regular attribute/常规属性
,而name
和nameItem
方法的成员类型是special operation/特殊操作
。web IDL 定义了 11 种成员,我在文末为你总结了一张表格,列出了每一种成员的功能概括、格式、实际应用的例子,让你可以快速掌握所有的成员类型。
接口的继承
在 web IDL 中,如果一个接口继承另一个接口,会使用冒号:
表示,比如HTMLFormControlsCollection接口继承了 HTMLCollection:
forms.elements
会返回一个实现了这个接口的对象,你可以在谷歌首页执行document.forms[0].elements
看到这一点。
接口的继承关系会在原型链上得到反映。HTMLFormControlsCollection 实例的原型链是这样的:
基于接口的继承关系,你甚至可以拉出一条完整的 HTML 接口继承图谱。只不过看起来会很复杂。比如就有人用 d3 画了一张以EventTarget接口为起点的继承关系图 。
Extended Attribute 扩展属性
上面我为了讲解方便,刻意省略 IDL 片段中的一些内容,完整的HTMLCollection
的接口应该是这样的:
用[]
括起来的部分称为扩展属性 ,是 web IDL 中的一种标记方式,表示这个接口的具有一些特殊行为。
比如,HTMLCollection 接口有两个扩展属性,一个是[Exposed=Window]
,另一个是[LegacyUnenumerableNamedProperties]
:
[Exposed=Window]
表示 HTMLCollection 接口的实例只能在主线程中使用,不能在 worker 中使用。如果一个接口的实例既能在 worker 中使用,也能在主线程中使用,那么需要用[Exposed=(Window,Worker)]
表示。[LegacyUnenumerableNamedProperties]
:在 web IDL 中,像item
这样可以通过index
属性来访问的 getter 方法称为index properties
;像nameItem
这样可以通过name
属性访问的 getter 方法称之为name properties
;[LegacyUnenumerableNamedProperties]
则表明这个接口中的name properties
是不可枚举的,所以使用Object.getOwnPropertyDescriptor
查看nameItem
对应的集合时,enumerable
的值是false
。
另一个例子,我在讲结构化克隆的时候提到过,标准使用[Serializable]
扩展属性标记一个可被序列化的接口,用[Transferable]
扩展属性来标记一个可转移对象。
当你在阅读 IDL 片段的时候,你会遇到大量的扩展属性。幸运的是,大部分扩展属性都是重复的,并且标准都会给你贴上对应解释的链接,所以我们只要沿着链接去理解,想要弄懂它的意义并不难。
mixin 接口与 partial 接口
上面讲的 3 个方面,都是 web IDL 用来描述接口的某种特性的。而接下来讲的 mixin 接口和 partial 接口,纯粹是 IDL 为了提升描述接口时的简洁性所设计的一种辅助功能。
比如,HTMLBodyElement
接口元素使用了 mixin:
这里的HTMLBodyElement includes WindowEventHandlers;
表示HTMLBodyElement
接口包含了WindowEventHandlers
mixin 接口里所有的成员。web IDL 使用interface mixin
来声明一个 mixin 接口,WindowEventHandlers
mixin 接口如下:
一个 mixin 接口可以被一个或多个接口包含。除了HTMLBodyElement
,Window
接口、HTMLFrameSetElement
接口也包含了WindowEventHandlers
。试想一下,如果没有 mixin 接口这样的设计,那么这里所说的 3 个接口都需要在自己的 IDL 片段中添加这样一长串的事件属性,文档的内聚性就会变得很低,阅读体验也会变得很差。
mixin 让你可以把接口进行组合,而 partial 则允许只展示接口的一部分。这有助于在解释接口的时候把读者的注意放在最关键的地方上。
比如这个例子:
总结与延伸
短短两千字的文章,我没法给你做到对 web IDL 的全面覆盖。一些小的功能点,比如Dictionaries 、Typedefs 以及具体的数据类型我没有讲到,但是有了上面的基本框架,再去理解这些内容并不困难。除此以外,Web IDL 还有很大的一部分篇幅是讲如何与 ECMAScript 绑定的,鉴于这是解读 HTML 标准的系列文章,所以就先不在这里讲了。
成员类型总结
版权声明: 本文为 InfoQ 作者【水鱼兄】的原创文章。
原文链接:【http://xie.infoq.cn/article/e1b343ad68132ebc7c4ccf8f6】。文章转载请联系作者。
评论