Dart 中 JS 互操作的歷史,你知道嗎?
由于在 Dart 3.3 中達(dá)到了令人興奮的 JavaScript 互操作里程碑,Wasm 的支持剛剛登陸當(dāng)前的 Flutter 測(cè)試版。為了慶祝這一里程碑,我們回顧了 Dart 和 JavaScript 互操作性長(zhǎng)達(dá)十年的歷程。
從 Dart 誕生之初,互操作性就是一個(gè)核心重點(diǎn)。2011 年 Dart 首次發(fā)布時(shí),它被設(shè)計(jì)為可嵌入和多平臺(tái)的。它可以運(yùn)行在獨(dú)立的虛擬機(jī)上,嵌入到瀏覽器中,并編譯為 JavaScript。2015 年 Flutter 出現(xiàn)時(shí),我們也準(zhǔn)備將其嵌入其中?,F(xiàn)在,我們也很高興能將 WasmGC 運(yùn)行時(shí)作為目標(biāo)。
起初,我們很快就暴露了嵌入 Dart 的各個(gè)平臺(tái)的功能。這就是我們的 SDK 平臺(tái)特定庫(kù)出現(xiàn)的方式:dart:io 暴露了虛擬機(jī)上的文件系統(tǒng),dart:html 暴露了 Web 上的瀏覽器 API,等等。這些庫(kù)在外觀和感覺(jué)上都與普通的 Dart 庫(kù)無(wú)異,但其背后卻隱藏著一些復(fù)雜的底層本地原語(yǔ),以使它們能正常工作。這是我們發(fā)明的第一種互操作形式。它具有很強(qiáng)的表現(xiàn)力,但僅限于 SDK 庫(kù)。
在 Web 上,開(kāi)發(fā)人員需要訪問(wèn)的不僅僅是瀏覽器 API。因此,我們開(kāi)始研究如何開(kāi)放互操作性,以覆蓋更多目標(biāo)。作為起點(diǎn),我們?cè)?2013 年推出了 dart:js,以實(shí)現(xiàn)對(duì) JavaScript 庫(kù)的訪問(wèn)。
// 用于說(shuō)明 Dart/JS 互操作的簡(jiǎn)短 JavaScript 代碼示例
window.myTopLevel = {
field1: 0,
method2() {
return this.field1;
}
}
// 通過(guò)“dart:js”訪問(wèn)(2013)
import 'dart:js' as js;
void main() {
// 這一行有一個(gè)錯(cuò)字!哎呀 :(
var object = js.context['myTopLevl'];
object['field1'] = 1;
// 此調(diào)用因 noSuchMethod 失敗,因?yàn)?method2 返回一個(gè) int,哎呀
object.callMethod('method2', []).substr(1);
}
我們當(dāng)時(shí)就知道,dart:js 并不是我們想要的編程模型。你必須使用字符串來(lái)訪問(wèn) JavaScript 中的名稱(chēng)--別提在編譯時(shí)發(fā)現(xiàn)問(wèn)題了,也別提代碼自動(dòng)補(bǔ)全了!實(shí)現(xiàn)成本也很高。大多數(shù)操作都嚴(yán)重依賴(lài)盒和深度拷貝。因此,我們?cè)?2014 年和 2015 年繼續(xù)起草各種想法,直到 package:js 的 v0.6 版本發(fā)布。
// 通過(guò) `package:js` 訪問(wèn) (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() {
// 訪問(wèn)代碼不容易出錯(cuò):分析器可以檢查
// 這些符號(hào)與聲明相匹配,而且我們還能獲得代碼補(bǔ)全!
var object = myTopLevel;
object.field1 = 1;
// 但是沒(méi)有檢查類(lèi)型,這就在一個(gè) int 上調(diào)用了子串,這是不正確的。
object.method2().substring(1);
}
有了 package:js,我們終于有了高效、用戶(hù)友好的開(kāi)放式 API。你可以在抽象類(lèi)上添加一些注解,然后就可以訪問(wèn) JavaScript API 了。這一切就像魔法一樣神奇,直到它失效。使用 package:js 有很多無(wú)法實(shí)現(xiàn)的功能:直接訪問(wèn)瀏覽器 API、重命名成員、轉(zhuǎn)換、附加 Dart 邏輯等等。為了彌補(bǔ)這些不足,我們還提供了 dart:js_util--一個(gè)類(lèi)似于 dart:js 的輕量級(jí)、高效的底層 API 作為備用。package:js 中的所有限制確實(shí)困擾著我們,但我們束手無(wú)策。我們需要更多的 Dart 語(yǔ)言來(lái)做得更好。
大約在那個(gè)時(shí)候,我們已經(jīng)在致力于對(duì)語(yǔ)言進(jìn)行有史以來(lái)最大的改變——我們讓 Dart 聽(tīng)起來(lái)更有趣。諷刺的是,當(dāng)我們?cè)?2018 年發(fā)布帶有 Dart 2.0 的新類(lèi)型系統(tǒng)時(shí),互操作性變得更糟!除了這些早期的限制之外,使 package:js 變得特別的魔法也有一個(gè)黑暗的一面——它無(wú)法檢查類(lèi)型的有效性。這意味著我們的互操作性是我們?cè)窘∪恼Z(yǔ)言中不健全的根源。
之后,我們的工作重心發(fā)生了變化,轉(zhuǎn)而集中精力改進(jìn) Dart 和 JS-interop。我們遵循明確的原則(習(xí)慣化、表現(xiàn)力強(qiáng)、組合性強(qiáng)、精確、平易近人、務(wù)實(shí)、非神化和完整),轉(zhuǎn)向以類(lèi)型和靜態(tài)分派為基礎(chǔ)的設(shè)計(jì),并對(duì) Dart 語(yǔ)言提出了挑戰(zhàn)。接下來(lái)的發(fā)展是并行的。
- 2019 年,Dart 2.7 添加了靜態(tài)擴(kuò)展方法。您可以將自定義 Dart 邏輯附加到 JS 互操作類(lèi)并轉(zhuǎn)換值,例如將 JS Promise 轉(zhuǎn)換為 Dart Future ,而無(wú)需使用包裝器。
- 2021 年,我們發(fā)布了 @staticInterop 和 package:js v0.6.4。最后,JS 互操作具有足夠的表現(xiàn)力 - 您可以公開(kāi)以前由 dart:html 等 SDK 庫(kù)專(zhuān)門(mén)管理的瀏覽器 API。
- 2023 年,當(dāng)我們?cè)?Dart 3.0 中放棄了不健全的空安全性時(shí),我們終于看到了我們所取得的進(jìn)步,我們的設(shè)計(jì)和 @staticInterop 的工作清楚地表明,我們已經(jīng)準(zhǔn)備好解決長(zhǎng)期存在的健全性差距。
那一年,我們?yōu)?WasmGC 引入了編譯功能,并利用 JS 互操作在其上運(yùn)行 Flutter web 等豐富的框架。這引發(fā)了 JS Types 的工作,以在編程模型中明確定義 Dart 和 JS 的邊界,并找到在 Wasm 和 JS 編譯目標(biāo)中使用 JS 的一致方法。我們還開(kāi)始了擴(kuò)展類(lèi)型語(yǔ)言實(shí)驗(yàn)--這是 Dart 3.3 中推出的一項(xiàng)功能,它在 Dart 語(yǔ)言和 JS 互操作之間架起了一座橋梁。多年來(lái),JS 互操作的行為(如類(lèi)型擦除)與 Dart 中的任何其他行為都不匹配。有了擴(kuò)展類(lèi)型,JS 互操作終于可以習(xí)以為常,并在 Dart 開(kāi)發(fā)工具中獲得應(yīng)有的支持。
盡管一路走來(lái)經(jīng)歷了許多轉(zhuǎn)變和轉(zhuǎn)折,但有一件事在整個(gè)十年中始終如一:我們的 Dart 社區(qū)的積極參與。社區(qū)成員采取了早期步驟測(cè)試并為 dart:js 做出貢獻(xiàn),然后影響 package:js 的設(shè)計(jì)。他們編寫(xiě)了工具來(lái)解決功能差距 (package:js_wrapping[1]),并嘗試通過(guò)自動(dòng)生成 Dart API 來(lái)提高生產(chǎn)力的方法 (package:js_facade_gen[2] 、 package:js_bindings[3] 、 package:typings[4])
最后,我們已經(jīng)到了 2024 年了。我們?cè)?Dart 3.3 中發(fā)布了 dart:js_interop 以及 package:web ,這是 Dart 中 JS 互操作的最新解決方案,使將 Flutter 編譯為 Wasm 成為可能。
// 通過(guò) `dart:js_interop` 訪問(wèn) (2024)
import 'dart:js_interop';
// 聲明使用擴(kuò)展類(lèi)型,這與 package:js
// 聲明非常相似。主要區(qū)別在于:它們是靜態(tài)調(diào)度的。
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 是一種靜態(tài)、健全、慣用、富有表現(xiàn)力且一致的互操作形式,基于能夠公開(kāi)任何 JavaScript 或?yàn)g覽器 API 的擴(kuò)展類(lèi)型。
- package:web 使用 dart:js_interop 完成 13 年前 dart:html 曾經(jīng)做過(guò)的事情,但是 JavaScript 和 WasmGC 都支持這種方式。
今天,我們很高興慶祝 Dart/JS 互操作的新形式及其所帶來(lái)的未來(lái)。了解我們的過(guò)去,我們確信這不是旅程的終點(diǎn),而是我們歷史上令人興奮的時(shí)刻。
我們迫不及待地想看看您將用它構(gòu)建什么!
原文: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