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

本文为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标准的系列文章,所以就先不在这里讲了。

成员类型总结

成员类型描述格式应用实例
Constants声明一个实数const type constant_identifier = 42;MediaError接口 使用Constants来义错误代码(code):const unsigned short MEDIA_ERR_ABORTED = 1;
Regular Attribute常规属性readonly? attribute type identifier;HTMLCollectionlength 属性。
Static Attributes静态属性static readonly? attribute type identifier;HTML和DOM标准中没有有这个成员类型的接口
Stringifiers可以单独使用,也可以用在属性上,表示对象转化为string的结果stringifier;stringifier attribute DOMString identifier;Location接口 的href属性定义: stringifier attribute USVString href;
所以location.toString() === location.href // true
Regular Operations常规方法return_type identifier(/* arguments... */);DOMStringList接口定义了一个contains方法:boolean contains(DOMString string);
constructor operation构造器方法,声明这个成员表示可以通过构造器来创建实例。constructor(/* arguments... */)Event接口 声明了一个construtor:constructor(DOMString type, optional EventInit eventInitDict = {});
Special Operations特殊方法。
特殊关键词有gettersetterdeleter
/* special_keyword */ return_type identifier?(/* arguments... */);HTMLCollectionitemnameItem方法
Static Operations静态方法static return_type identifier(/* arguments... */);HTMLScriptElement接口的supports方法是一个静态方法: static boolean supports(DOMString type);
Iterable declarations表示这个接口可被遍历iterable<value_type>;iterable<key_type, value_type>;NodeList接口有一个iterator: iterable<Node>;;意味着可以用for..of..进行遍历。
Asynchronously iterable declarations异步遍历声明async iterable<value_type>;async iterable<key_type, value_type>;HTML和DOM标准中没有有这个成员类型的接口
Maplike declarations具有map特性的成员maplike<key_type, value_type>;HTML和DOM标准中没有有这个成员类型的接口
Setlike declarations具有set特性的成员setlike<type>;HTML和DOM标准中没有有这个成员类型的接口
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值