[博客翻译]Dart中JavaScript互操作的历史


原文地址:https://medium.com/dartlang/history-of-js-interop-in-dart-98b06991158f


随着Dart 3.3版本的到来,Flutter Beta版现在支持了WebAssembly(Wasm),这得益于Dart与JavaScript交互的一个重要里程碑。让我们回顾一下Dart与JavaScript互操作性长达十年的历程。

自Dart于2011年发布之初,互操作性就是其核心关注点。Dart设计为可嵌入且跨平台,可以在独立虚拟机上运行,嵌入浏览器,甚至编译为JavaScript。2015年Flutter诞生时,我们已经准备好了将其嵌入其中。现在,我们对目标WasmGC运行时充满期待。

初期,我们的工作是快速暴露出Dart被嵌入的每个平台的能力。这就是SDK平台特定库的由来:dart:io暴露了VM上的文件系统,dart:html暴露了Web上的浏览器API等。这些库看起来和普通Dart库一样,但在幕后隐藏了一些复杂的底层原生原语来使它们工作。这是我们最早发明的互操作形式,虽然表达性强,但仅限于SDK库。

1_dhBQqKXU46sGKjx36LNf0w.webp

在Web上,开发者需要访问更多的不仅仅是浏览器API。于是,我们在2013年引入了dart:js,以便可以访问JavaScript库。

// 简短的JavaScript代码,演示Dart/JS互操作 

window.myTopLevel = { 

  field1: 0, 

  method2() { 

    return this.field1; 

  } 

} 

// 通过'dart:js'访问 (2013) 

import 'dart:js' as js; 

void main() { 

  // 这行有拼写错误!:( 

  var object = js.context['myTopLevl']; 

  object['field1'] = 1; 

  // 这个调用会因noSuchMethod错误失败,因为method2返回int,哎呀 

  object.callMethod('method2', []).substr(1); 

}

当时我们就知道dart:js不是我们想要的编程模型。使用字符串来访问JavaScript中的名称,无法在编译时发现问题,更别提代码补全了!实现也很昂贵,严重依赖盒子和深拷贝。因此,我们继续在2014年和2015年提出想法,直到发布了package:js的v0.6版本。

// 通过'package:js'访问 (2015) 

import 'package:js/js.dart'; 

// 魔法注解允许我们声明API签名 

@JS() 

class MyObject { 

  external int get field1; 

  external void set field1(int value); 

  external String method2(); 

} 

@JS() 

external MyObject get myTopLevel; 

void main() { 

  // 访问代码更少出错:分析器可以检查这些符号是否匹配声明,还有代码补全! 

  var object = myTopLevel; 

  object.field1 = 1; 

  // 但是类型没有检查,不安全地在int上调用了substring方法 

  object.method2().substring(1); 

}

有了package:js,我们终于有了一个高效且用户友好的开放API。只需在抽象类上添加一些注解,就可以访问JavaScript API。一切就像魔法一样,直到它不再满足需求。使用package:js还有很多不能做的事情,比如直接访问浏览器API、重命名成员、转换、附加Dart逻辑等。为了弥补这些问题,我们还推出了dart:js_util,这是一个轻量级且高效的低级API,类似于dart:js,作为备用。

所有package:js的局限性困扰着我们,但我们束手无策。我们需要Dart语言提供更多的支持才能做得更好。

那时,我们已经开始着手进行Dart语言有史以来最大的变革——使其成为静态类型的语言。讽刺的是,当我们在2018年随着Dart 2.0发布新类型系统时,互操作性反而变得更糟!除了早期的限制,使得package:js特别的魔法也有其阴暗面——无法检查类型的正确性。这意味着我们的互操作性成为了我们原本静态类型语言中的不安全来源。

然后,我们的旅程转向了改进Dart和JS互操作性的协同努力。遵循明确的原则(符合直觉、表达性强、可组合、精确、易于理解、实用、非魔法化、完整),我们转向了一个以类型和静态分派为基础的设计,这挑战了Dart语言。接下来是一起并行演进的过程。

2019年,Dart 2.7添加了静态扩展方法。你可以将自定义的Dart逻辑附加到JS互操作类上,无需使用包装器就能将JS Promise转换为Dart Future。

2021年,我们发布了@staticInterop,随着package:js v0.6.4的发布,JS互操作性终于足够强大,可以直接暴露以前只能通过SDK库(如dart:html)管理的浏览器API。

到了2023年,当我们随着Dart 3.0放弃了不安全的空安全时,我们看到了取得的进步,我们的设计和@staticInterop工作表明,我们已经准备好解决长期以来的类型安全性问题。

同年,我们引入了编译到WasmGC,并利用JS互操作性在其中运行丰富的框架,如Flutter Web。这激发了对JS Types的工作,以清晰地定义编程模型中的Dart和JS边界,并找到在Wasm和JS编译目标中一致地处理JS的方法。我们还开始了扩展类型的语言实验,这是Dart 3.3中推出的功能,它弥合了Dart语言和JS互操作之间的差距。多年来,JS互操作具有一些行为,如类型擦除,这些在Dart中没有任何其他东西匹配。有了扩展类型,JS互操作终于变得符合Dart的语法习惯,并在Dart开发工具中得到了应有的支持。

尽管沿途有许多转变和曲折,但整个十年间有一件事始终如一:那就是Dart社区的积极参与。社区成员早早地测试和贡献dart:js,后来影响了package:js的设计。他们编写工具来解决功能缺失(package:js_wrapping),并尝试通过自动生成Dart API来提高生产力(package:js_facade_gen, package:js_bindings, package:typings)。每一个贡献都使Dart的互操作性设计变得更好。对每一位做出贡献的人,感谢你们让这次冒险如此激动人心!

最终,我们来到了2024年。在Dart 3.3中,我们发布了dart:js_interop,以及最新的JS互操作解决方案package:web,使得编译Flutter到Wasm成为可能。

// 通过'dart:js_interop'访问 (2024) 

import 'dart:js_interop'; 

// 声明使用扩展类型,与package:js声明非常相似。 

// 主要区别在于:它们是静态分派的。 

extension type MyObject._(JSObject _) implements JSObject {
  external int get field1;
  external void set field1(int value);
  external String method2();
}

@JS()
external MyObject get myTopLevel;

void main() {
  var object = myTopLevel;
  object.field1 = 1;
  // At last, access is sound - this line fails with a type error
  // when returning from method2.
  object.method2().substring(1);
}
  • dart:js_interop是一种基于扩展类型的静态、健全、惯用、表达和一致的interop形式,能够公开任何JavaScript或浏览器API。
  • package:web使用dart:js_interop来做13年前dart:html曾经做过的事情,但在JavaScript和WasmGC中都支持这种方式。

今天,我们很高兴地庆祝Dart/JS互操作的一种新形式及其实现的未来。了解我们的过去,我们确信这不是旅程的结束,而是我们历史上令人兴奋的一点。

我们迫不及待地想看看你会用它做什么!