0%

【文章翻譯】History of JS interop in Dart

【文章內容使用 Gemini 1.5 Pro 自動產生】

Dart 中 JS 互操作的歷史

Wasm 支援剛剛在現有的 Flutter beta 版本中推出,這得益於 Dart 3.3 中達成的令人興奮的 JavaScript 互操作里程碑。為了慶祝,我們回顧了 Dart 和 JS 互操作長達十年的發展歷程。

AI Image generated by Gemini

互操作性一直是 Dart 從一開始就關注的核心議題。當 Dart 在 2011 年首次發佈時,它被設計為 可嵌入跨平台 的。它運行在獨立的虛擬機上,嵌入在瀏覽器中,並編譯為 JavaScript。當 Flutter 在 2015 年出現時,我們已經準備好將它嵌入到這裡。現在,我們也很興奮地 將目標定位到 WasmGC 執行時

起初,我們迅速努力暴露 Dart 嵌入的每個平台的功能。這就是我們的 SDK 平台特定函式庫出現的方式:dart:io 暴露了 VM 上的文件系統,dart:html 暴露了 Web 上的瀏覽器 API,等等。這些函式庫看起來和感覺起來像普通的 Dart 函式庫,但在幕後隱藏了一些複雜的低階原生元素,使它們能夠運作。這是我們發明的第一種互操作形式。它表達力強,但僅限於 SDK 函式庫。

在 Web 上,開發人員需要的遠不止瀏覽器 API。因此,我們開始尋找將互操作性擴展到更多目標的方法。作為起點,我們在 2013 年引入了 dart:js,以啟用存取 JavaScript 函式庫。

1
2
3
4
5
6
7
// Short example JavaScript code to illustrate Dart/JS interop
window.myTopLevel = {
field1: 0,
method2() {
return this.field1;
}
}
1
2
3
4
5
6
7
8
9
10
11
// Access via `dart:js` (2013)
import 'dart:js' as js;

void main() {
// This line has a typo! oops :(
var object = js.context['myTopLevl'];
object['field1'] = 1;
// This call fails with a noSuchMethod because method2
// returns an int, oops
object.callMethod('method2', []).substr(1);
}

我們當時就知道 dart:js 不是我們想要的程式設計模型。您必須使用字串來存取 JavaScript 中的名稱 - 忘記編譯時找到問題,更別提代碼補全了!實作起來也很昂貴。它在大多数操作中高度依赖盒子和深度复制。因此,我們在 2014 年和 2015 年繼續 草擬 想法,直到發佈了 package:js v0.6。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Access via `package:js` (2015)
import 'package:js/js.dart';

// Magic annotations allow us to declare API signatures:
@JS()
class MyObject {
external int get field1;
external void set field1(int value);
external String method2();
}

@JS()
external MyObject get myTopLevel;

void main() {
// Access to code is less error prone: analyzer can check that
// these symbols match a declaration, and we get code-completion too!
var object = myTopLevel;
object.field1 = 1;
// But types are not checked, this unsoundly invokes substring on an int
object.method2().substring(1);
}

有了 package:js,我們終於擁有一個開放的 API,它既高效又使用者友善。您可以在抽象類別上添加一些註解,然後 voila,您就可以存取 JavaScript API。它就像魔法一樣運作,直到它失效。有很多事情您無法使用 package:js 做到:直接存取瀏覽器 API、重新命名成員、轉換、附加 Dart 邏輯,以及 更多。為了彌補,我們還發佈了 dart:js_util - 一個類似於 dart:js 的輕量級高效低階 API,作為後備方案。package:js 中的所有限制確實困擾著我們,但我們束手無策。我們需要從 Dart 語言中獲得更多幫助才能做得更好。

在那段時間裡,我們已經在為語言做最大的變更,我們從未做過 - 我們正在使 Dart 健全。具有諷刺意味的是,當我們在 2018 年使用 Dart 2.0 發佈新的類型系統時,互操作性變得 更差 了!除了早期的限制之外,使 package:js 特殊的魔法也有陰暗面 - 它無法檢查類型的有效性。這意味著我們的互操作性是我們其他健全語言中不健全的來源。

然後,我們的旅程轉變為專注於改善 Dart 和 JS 互操作,作為一項協同努力。我們秉持著明確的原則(要慣用、表達力強、可組合、精確、易於使用、務實、非魔法且完整),朝著一個以類型和靜態分派為基礎的設計前進,並挑戰 Dart 語言。此後,語言同步演進。

  • 在 2019 年,Dart 2.7 添加了靜態擴展方法。您可以將自訂 Dart 邏輯附加到 JS 互操作類別,並轉換值,例如將 JS Promise 轉換為 Dart Future,而無需使用包裝器。
  • 在 2021 年,我們發佈了 package:js v0.6.4 中的 @staticInterop。最終,JS 互操作變得足夠表達力強 - 您可以暴露以前專屬於 SDK 函式庫(如 dart:html)管理的瀏覽器 API。
  • 在 2023 年,當我們在 Dart 3.0 中放棄不健全的空安全時,我們終於看到了取得的進展,我們的設計和 @staticInterop 的工作使我們清楚地知道,我們已經準備好解決長期以來存在的健全性差距。

那一年,我們引入了編譯到 WasmGC,並利用 JS 互操作在它上面運行豐富的框架,例如 Flutter Web。這激發了對 JS 類型 的工作,以便在程式設計模型中明確定義 Dart 和 JS 的界限,並找到在 Wasm 和 JS 編譯目標中與 JS 相處的統一方法。我們還開始了 擴展類型 語言實驗 - 一項在 Dart 3.3 中推出的功能,它彌合了 Dart 語言和 JS 互操作之間的差距。多年來,JS 互操作一直存在一些行為,例如類型擦除,這些行為與 Dart 中的其他任何東西都不匹配。有了擴展類型,JS 互操作終於可以變得慣用,並在 Dart 開發工具中獲得它應有的支援。

儘管一路上經歷了許多變化和轉折,但有一件事始終保持一致:我們 Dart 社群的積極參與。社群成員率先對 dart:js 進行測試和貢獻,然後又影響了 package:js 的設計。他們編寫了工具來解決功能差距(package:js_wrapping),並嘗試透過自動生成 Dart API 來提高生產力(package:js_facade_genpackage:js_bindingspackage:typings)。每一次貢獻都幫助 Dart 的互操作設計變得更好。對於在這裡的每個人,感謝您使這段旅程如此令人興奮!

最後,我們來到了 2024 年。我們在 Dart 3.3 中發佈了 dart:js_interop,以及 package:web,這是 Dart 中最新的 JS 互操作解決方案,使 將 Flutter 編譯到 Wasm 成为可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Access via `dart:js_interop` (2024)
import 'dart:js_interop';

// Declarations use extension types, which are very similar to package:js
// declarations. The main difference: they are statically dispatched.
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 來完成 dart:html 13 年前做的事情,但以一種在 JavaScript 和 WasmGC 中都支援的方式。

今天,我們很高興地慶祝 Dart/JS 互操作的新形式,以及它帶來的未來。了解我們的過去,我们確信這不是旅程的終點,而是我們歷史上一個令人興奮的時刻。

我們迫不及待地想看看您將用它建立什麼!


Dart 中 JS 互操作的歷史 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

http://creativecommons.org/licenses/by/4.0/