随着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库。
在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互操作性终于