在当前的 Flutter beta 版本中,由于 Dart 3.3 达到了令人振奋的 JavaScript 互操作性里程碑,支持 Wasm 刚刚落地。为了庆祝这一里程碑,让我们回顾一下 Dart 和 JS 互操作性的十年之旅。
互操作性从 Dart 诞生之初就是一个核心关注点。当 Dart 在 2011 年首次发布时,它被设计成可嵌入和多平台的。它运行在一个独立的虚拟机上,在浏览器中嵌入,并编译为 JavaScript。当 Flutter 在 2015 年出现时,我们也准备好在那里嵌入它。现在,我们也很高兴地将目标定为 WasmGC 运行时。
起初,我们迅速努力地暴露 Dart 嵌入的每个平台的能力。这就是我们的 SDK 平台特定库出现的原因:[dart:io](https://api.dart.dev/stable/dart-io/dart-io-library.html)
暴露了 VM 上的文件系统,[dart:html](https://api.dart.dev/stable/dart-html/dart-html-library.html)
暴露了 web 上的浏览器 API,等等。这些库看起来和感觉像普通的 Dart 库,但在幕后隐藏了一些复杂的低级原语来使它们工作。这是我们发明的第一种互操作形式。它很表达力强,但仅限于 SDK 库。
在 web 上,开发人员需要访问的不仅仅是浏览器 API。因此,我们开始寻找覆盖更多目标的互操作性的方法。作为一个起点,我们在 2013 年引入了 [dart:js](https://codereview.chromium.org//15782009)
来实现对 JavaScript 库的访问。
// 用于说明 Dart/JS 互操作性的简短 JavaScript 代码示例
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;
// 这个调用因为 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](https://pub.dev/documentation/js/latest/)
,我们终于有了一个高效且用户友好的开放 API。你可以在抽象类上撒些注解,voila,你就可以访问 JavaScript API 了。这一切都像魔术一样运行,直到它失败了。在 package:js
中有很多你无法做的事情:直接访问浏览器 API、重命名成员、转换、附加 Dart 逻辑,以及更多。为了补偿,我们还发布了 [dart:js_util](https://codereview.chromium.org/2150313003/)
—— 一个轻量级且高效的低级 API,类似于 dart:js
,作为备用方案。package:js
中的所有限制都让我们很烦恼,但我们束手无策。我们需要 Dart 语言做得更好。
那时,我们已经在进行我们所做过的最大的语言变更 —— 我们正在让 Dart 变得类型安全。讽刺的是,当我们在 2018 年发布了 Dart 2.0 的新类型系统时,互操作性变得更糟了!除了那些早期的限制之外,使 package:js
特殊的魔法也有一个阴暗面 —— 它无法检查类型的有效性。这意味着我们的互操作性是我们否则完全类型安全的语言中的不安全因素。
然后,我们的旅程转向了专注于改进 Dart 和 JS-interop 作为一个协调一致的努力。在明确的原则(符合习惯、表达力强、组合、准确、易接近、实用、非魔术、完整)的指导下,我们朝着一个以类型和静态分派为核心的设计前进,并挑战了 Dart 语言。接下来是一个并行的演变。
- 在 2019 年,Dart 2.7 添加了静态扩展方法。你可以将自定义的 Dart 逻辑附加到一个 JS-interop 类上,并进行值转换,比如将 JS 的
Promise
转换为 Dart 的Future
,而无需使用包装器。 - 在 2021 年,我们发布了带有
package:js
v0.6.4 的@staticInterop
。最终,JS-interop 足够表达力强了 —— 你可以暴露以前只由 SDK 库(如dart:html
)管理的浏览器 API。 - 在 2023 年,当我们在 Dart 3.0 中放弃了不安全的空安全性时,我们终于看到了我们取得的进步,我们的设计和
@staticInterop
的工作清楚地表明,我们已经准备好解决长期以来存在的不安全性差距。
那一年,我们引入了对 WasmGC 的编译,并利用了 JS-interop 来在其上运行丰富的框架,比如Flutter web。这引发了对 JS 类型 的工作,以清晰地定义 Dart 和 JS 在编程模型中的边界,并找到一种一致的方式来在 Wasm 和 JS 编译目标中使用 JS。我们还开始了 扩展类型语言实验 —— 这是在 Dart 3.3 中推出的一个功能,用于弥合 Dart 语言和 JS-interop 之间的差距。多年来,JS-interop 有一些行为,比如类型擦除,与 Dart 中的其他任何东西都不匹配。有了扩展类型,JS-interop 终于可以变得符合习惯,并且在 Dart 开发工具中得到应有的支持。
尽管路途中有许多转变和转折,但在整个十年中有一件事始终如一:我们 Dart 社区的积极参与。社区成员们早早地开始测试和贡献 dart:js
,然后影响了 package:js
的设计。他们编写了工具来解决功能差距([package:js_wrapping](https://github.com/a14n/dart-js-wrapping)
),并尝试了通过自动生成 Dart API([package:js_facade_gen](https://github.com/dart-archive/js_facade_gen)
、[package:js_bindings](https://pub.dev/packages/js_bindings)
、[package:typings](https://pub.dev/packages/typings)
)来提高生产力的方法。每一次贡献都帮助改善 Dart 的互操作设计。对于在座的每一个人,感谢你们让这成为如此令人兴奋的冒险!
最后,我们来到了 2024 年。我们在 Dart 3.3 中发布了 [dart:js_interop](https://dart.dev/interop/js-interop)
,以及 [package:web](https://dart.dev/interop/js-interop/package-web)
,这是 Dart 中的 JS 互操作的最新解决方案,使将 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;
// 最后,访问是安全的 - 当从 method2 返回时,此行将导致类型错误
object.method2().substring(1);
}
dart:js_interop
是一种静态、类型安全、符合习惯、表达力强、一致的互操作形式,基于扩展类型,能够暴露任何 JavaScript 或浏览器 API。package:web
使用dart:js_interop
执行了 13 年前dart:html
曾经执行的操作,但以一种在 JavaScript 和 WasmGC 中都受支持的方式。
今天,我们很高兴地庆祝一种新形式的 Dart/JS 互操作性以及它所带来的未来。了解我们的过去,我们确信这不是旅程的终点,而是我们历史上的一个激动人心的时刻。
我们迫不及待地想看看你将用它构建出什么!