从 Dart 诞生之初,互操作性就是一个核心重点。2011 年 Dart 首次发布时,它被设计为可嵌入和多平台的。它可以运行在独立的虚拟机上,嵌入到浏览器中,并编译为 JavaScript。2015 年 Flutter 出现时,我们也准备将其嵌入其中。现在,我们也很高兴能将 WasmGC 运行时作为目标。
起初,我们很快就暴露了嵌入 Dart 的各个平台的功能。这就是我们的 SDK 平台特定库出现的方式:dart:io 暴露了虚拟机上的文件系统,dart:html 暴露了 Web 上的浏览器 API,等等。这些库在外观和感觉上都与普通的 Dart 库无异,但其背后却隐藏着一些复杂的底层本地原语,以使它们能正常工作。这是我们发明的第一种互操作形式。它具有很强的表现力,但仅限于 SDK 库。
在 Web 上,开发人员需要访问的不仅仅是浏览器 API。因此,我们开始研究如何开放互操作性,以覆盖更多目标。作为起点,我们在 2013 年推出了 dart:js,以实现对 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;
// 此调用因 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';
// @JS 注解允许我们声明 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 上调用了子串,这是不正确的。
object.method2().substring(1);
}
有了 package:js,我们终于有了高效、用户友好的开放式 API。你可以在抽象类上添加一些注解,然后就可以访问 JavaScript API 了。这一切就像魔法一样神奇,直到它失效。使用 package:js 有很多无法实现的功能:直接访问浏览器 API、重命名成员、转换、附加 Dart 逻辑等等。为了弥补这些不足,我们还提供了 dart:js_util--一个类似于 dart:js 的轻量级、高效的底层 API 作为备用。package:js 中的所有限制确实困扰着我们,但我们束手无策。我们需要更多的 Dart 语言来做得更好。
大约在那个时候,我们已经在致力于对语言进行有史以来最大的改变——我们让 Dart 听起来更有趣。讽刺的是,当我们在 2018 年发布带有 Dart 2.0 的新类型系统时,互操作性变得更糟!除了这些早期的限制之外,使 package:js 变得特别的魔法也有一个黑暗的一面——它无法检查类型的有效性。这意味着我们的互操作性是我们原本健全的语言中不健全的根源。
之后,我们的工作重心发生了变化,转而集中精力改进 Dart 和 JS-interop。我们遵循明确的原则(习惯化、表现力强、组合性强、精确、平易近人、务实、非神化和完整),转向以类型和静态分派为基础的设计,并对 Dart 语言提出了挑战。接下来的发展是并行的。
- 2019 年,Dart 2.7 添加了静态扩展方法。您可以将自定义 Dart 逻辑附加到 JS 互操作类并转换值,例如将 JS Promise 转换为 Dart Future ,而无需使用包装器。
- 2021 年,我们发布了 @staticInterop 和 package:js v0.6.4。最后,JS 互操作具有足够的表现力 - 您可以公开以前由 dart:html 等 SDK 库专门管理的浏览器 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:js 做出贡献,然后影响 package:js 的设计。他们编写了工具来解决功能差距 (package:js_wrapping[1]),并尝试通过自动生成 Dart API 来提高生产力的方法 (package:js_facade_gen[2] 、 package:js_bindings[3] 、 package:typings[4])
最后,我们已经到了 2024 年了。我们在 Dart 3.3 中发布了 dart: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;
// At last, access is sound - this line fails with a type error
// when returning from method2.
object.method2().substring(1);
}
- dart:js_interop 是一种静态、健全、惯用、富有表现力且一致的互操作形式,基于能够公开任何 JavaScript 或浏览器 API 的扩展类型。
- package:web 使用 dart:js_interop 完成 13 年前 dart:html 曾经做过的事情,但是 JavaScript 和 WasmGC 都支持这种方式。
今天,我们很高兴庆祝 Dart/JS 互操作的新形式及其所带来的未来。了解我们的过去,我们确信这不是旅程的终点,而是我们历史上令人兴奋的时刻。
我们迫不及待地想看看您将用它构建什么!
原文:https://medium.com/dartlang/history-of-js-interop-in-dart-98b06991158f
参考资料
[1]package:js_wrapping: https://github.com/a14n/dart-js-wrapping
[2]package:js_facade_gen: https://github.com/dart-archive/js_facade_gen
[3]package:js_bindings: https://pub.dev/packages/js_bindings
[4]package:typings: https://pub.dev/packages/typings