写点什么

一文读懂 web 标准的基石:web IDL

作者:水鱼兄
  • 2022 年 10 月 10 日
    日本
  • 本文字数:4931 字

    阅读完需:约 16 分钟

本文为 HTML 标准解读系列文章,其他文章详见这里


HTML标准的2.6小节,我们第一次遇到了 IDL 片段,他定义了HTMLALLCollection的接口:


[Exposed=Window, LegacyUnenumerableNamedProperties]interface HTMLAllCollection {  readonly attribute unsigned long length;  getter Element (unsigned long index);  getter (HTMLCollection or Element)? namedItem(DOMString name);  (HTMLCollection or Element)? item(optional DOMString nameOrIndex);
// Note: HTMLAllCollection objects have a custom [[Call]] internal method and an [[IsHTMLDDA]] internal slot.};
复制代码


接下来,像这样的 IDL 片段贯穿了整个标准,或长或短,或简单或复杂。于是,弄懂 web IDL 就变成了一个必须要做的事情了:


  1. 不仅仅是 HTML 标准,DOM 标准、ECMAScript 标准也是使用 web IDL 来定义接口的。如果你想读懂任何这些标准,就绕不开 web IDL。

  2. 理解 web IDL 可以让你以更专业、更高效的方式了解一个标准定义的对象,而不是使用 MDN 这种二手资料。

  3. 理解 web IDL 有助于深刻理解接口之间的继承关系,增加知识碎片的连接,搭建健壮的知识网络。比如,当你在看一个HTMLCollection接口的时候,你会发现至少有这些方法/属性返回值是使用了这个接口:


   // 元素搜索方法   document.getElementsByTagName()   document.getElementsByTagNameNS()   document.getElementsByClassName()   // 获取一类元素   document.images   document.embeds   document.plugins   document.links   document.forms   document.scripts   document.applets   document.anchors   // 特定元素上的属性   map.areas   table.tBodies   table.rows   tbody.rows   tr.cells   select.selectedOptions   datalist.options   fieldset.elements   // node的属性   node.children
复制代码


然后,你还可以看到有以下这些接口继承了 HTMLCollection:


   HTMLFormControlsCollection   HTMLOptionsCollection
复制代码


再进一步延伸,你还可以继续查看哪些对象和 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 的代码片段为例子:


interface Pingable {  ping(): void;} class Sonar implements Pingable {  ping() {    console.log("ping!");  }} class Ball implements Pingable {  // 报错:Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.  pong() {    console.log("pong!");  }}
复制代码


这个片段定义了一个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接口作为第一个例子:


interface HTMLCollection {  readonly attribute unsigned long length;  getter Element? item(unsigned long index);  getter Element? namedItem(DOMString name);};
复制代码


如果把这个 IDL 片段翻译成“人话”,大致是这样的:


这是一个名为HTMLCollection的接口。这个接口有以下成员:

  • 一个只读属性:类型为unsigned long, 名为length

  • 一个名为item的方法,接受一个类型为unsigned long、名为index的参数,返回值的类型可能是Element或者Null

  • 一个名为nameItem的方法,接受一个类型为DOMString、名为name的参数,返回值的类型可能是Element或者Null

getter关键词表示itemnameItem是特殊的方法,可以以属性的方式进行访问。所以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/常规属性,而namenameItem方法的成员类型是special operation/特殊操作 。web IDL 定义了 11 种成员,我在文末为你总结了一张表格,列出了每一种成员的功能概括、格式、实际应用的例子,让你可以快速掌握所有的成员类型。

接口的继承

在 web IDL 中,如果一个接口继承另一个接口,会使用冒号:表示,比如HTMLFormControlsCollection接口继承了 HTMLCollection:


interface HTMLFormControlsCollection : HTMLCollection {  // inherits length and item()  getter (RadioNodeList or Element)? namedItem(DOMString name); // shadows inherited namedItem()};
复制代码


forms.elements会返回一个实现了这个接口的对象,你可以在谷歌首页执行document.forms[0].elements看到这一点。


接口的继承关系会在原型链上得到反映。HTMLFormControlsCollection 实例的原型链是这样的:


[Object.prototype: Object的原型][HTMLCollection.prototype: HTMLCollection的接口原型对象][HTMLFormControlsCollection.prototype: HTMLFormControlsCollection的接口原型对象][HTMLFormControlsCollection的实例]
复制代码


基于接口的继承关系,你甚至可以拉出一条完整的 HTML 接口继承图谱。只不过看起来会很复杂。比如就有人用 d3 画了一张以EventTarget接口为起点的继承关系图

Extended Attribute 扩展属性

上面我为了讲解方便,刻意省略 IDL 片段中的一些内容,完整的HTMLCollection的接口应该是这样的:


[Exposed=Window, LegacyUnenumerableNamedProperties]interface HTMLCollection {  readonly attribute unsigned long length;  getter Element? item(unsigned long index);  getter Element? namedItem(DOMString name);};
复制代码


[]括起来的部分称为扩展属性 ,是 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:


[Exposed=Window]interface HTMLBodyElement : HTMLElement {  [HTMLConstructor] constructor();
// also has obsolete members};
HTMLBodyElement includes WindowEventHandlers;
复制代码


这里的HTMLBodyElement includes WindowEventHandlers; 表示HTMLBodyElement接口包含了WindowEventHandlersmixin 接口里所有的成员。web IDL 使用interface mixin来声明一个 mixin 接口,WindowEventHandlersmixin 接口如下:


interface mixin WindowEventHandlers {  attribute EventHandler onafterprint;  attribute EventHandler onbeforeprint;  attribute OnBeforeUnloadEventHandler onbeforeunload;  attribute EventHandler onhashchange;  attribute EventHandler onlanguagechange;  attribute EventHandler onmessage;  attribute EventHandler onmessageerror;  attribute EventHandler onoffline;  attribute EventHandler ononline;  attribute EventHandler onpagehide;  attribute EventHandler onpageshow;  attribute EventHandler onpopstate;  attribute EventHandler onrejectionhandled;  attribute EventHandler onstorage;  attribute EventHandler onunhandledrejection;  attribute EventHandler onunload;};
复制代码


一个 mixin 接口可以被一个或多个接口包含。除了HTMLBodyElementWindow接口、HTMLFrameSetElement接口也包含了WindowEventHandlers。试想一下,如果没有 mixin 接口这样的设计,那么这里所说的 3 个接口都需要在自己的 IDL 片段中添加这样一长串的事件属性,文档的内聚性就会变得很低,阅读体验也会变得很差。


mixin 让你可以把接口进行组合,而 partial 则允许只展示接口的一部分。这有助于在解释接口的时候把读者的注意放在最关键的地方上。


比如这个例子


partial interface Window {  undefined captureEvents();  undefined releaseEvents();
[Replaceable, SameObject] readonly attribute External external;};
The captureEvents() and releaseEvents() methods must do nothing. // 解释接口
复制代码

总结与延伸

短短两千字的文章,我没法给你做到对 web IDL 的全面覆盖。一些小的功能点,比如DictionariesTypedefs 以及具体的数据类型我没有讲到,但是有了上面的基本框架,再去理解这些内容并不困难。除此以外,Web IDL 还有很大的一部分篇幅是讲如何与 ECMAScript 绑定的,鉴于这是解读 HTML 标准的系列文章,所以就先不在这里讲了。

成员类型总结


发布于: 刚刚阅读数: 4
用户头像

水鱼兄

关注

前端开发 2019.01.15 加入

还未添加个人简介

评论

发布
暂无评论
一文读懂web标准的基石:web IDL_水鱼兄_InfoQ写作社区