內容

JS 互通提供從 Dart 與 JavaScript API 互動的機制。它允許您呼叫這些 API,並使用明確且慣用的語法與從中取得的值進行互動。

一般來說,透過在 全域 JS 範圍 內的某個位置提供 JavaScript API,即可存取該 API。要呼叫此 API 並接收其 JS 值,請使用 external 互操作成員。若要建構和提供 JS 值的類型,請使用和宣告 互操作類型,其中也包含互操作成員。若要將像 ListFunction 等 Dart 值傳遞給互操作成員,或將 JS 值轉換為 Dart 值,請使用 轉換函式,除非互操作成員 包含基本類型

互通型別

#

與 JS 值互動時,您需要提供 Dart 類型。您可以使用或宣告互操作類型來執行此動作。互操作類型可能是 Dart 提供的 "JS 類型",或包裝互操作類型的 擴充類型

互操作類型讓您可以提供 JS 值的介面,並讓您宣告其成員的互操作 API。它們也會用於其他互操作 API 的簽章中。

dart
extension type Window(JSObject _) implements JSObject {}

Window 是任意 JSObject 的互操作類型。沒有 執行時期保證 可確保 Window 實際上是 JS Window。對於針對相同值定義的任何其他互操作介面,也不會發生衝突。如果您想要檢查 Window 是否實際上是 JS Window,您可以 透過互操作檢查 JS 值的類型

您也可以透過包裝 Dart 提供的 JS 類型來宣告自己的互操作類型

dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
  external Array();
}

在大部分情況下,您可能會使用 JSObject 作為 表示類型 來宣告互操作類型,因為您可能會與 Dart 沒有提供互操作類型的 JS 物件互動。

互操作類型通常也應該 實作 其表示類型,以便可以在預期表示類型的地方使用它們,例如 package:web 中的許多 API。

互通成員

#

external 互操作成員提供 JS 成員的慣用語法。它們讓您可以為其引數和傳回值撰寫 Dart 類型簽章。可以在這些成員的簽章中撰寫的類型具有 限制。互操作成員對應的 JS API 是由宣告位置、名稱、Dart 成員類型以及任何 重新命名 組合決定的。

頂層互通成員

#

針對下列 JS 成員

js
globalThis.name = 'global';
globalThis.isNameEmpty = function() {
  return globalThis.name.length == 0;
}

您可以為它們撰寫互操作成員,如下所示

dart
@JS()
external String get name;

@JS()
external set name(String value);

@JS()
external bool isNameEmpty();

在此,存在屬性 name 和函式 isNameEmpty,它們在全域範圍內公開。若要存取它們,請使用頂層互操作成員。若要取得和設定 name,請宣告和使用具有相同名稱的互操作 getter 和 setter。若要使用 isNameEmpty,請宣告和呼叫具有相同名稱的互操作函式。您可以宣告頂層互操作 getter、setter、方法和欄位。互操作欄位等於 getter 和 setter 配對。

頂層互操作成員必須使用 @JS() 註解宣告,以將它們與其他 external 頂層成員區分開來,例如可以使用 dart:ffi 編寫的成員。

互通型別成員

#

針對以下類似的 JS 介面

js
class Time {
  constructor(hours, minutes) {
    this._hours = Math.abs(hours) % 24;
    this._minutes = arguments.length == 1 ? 0 : Math.abs(minutes) % 60;
  }

  static dinnerTime = new Time(18, 0);

  static getTimeDifference(t1, t2) {
    return new Time(t1.hours - t2.hours, t1.minutes - t2.minutes);
  }

  get hours() {
    return this._hours;
  }

  set hours(value) {
    this._hours = Math.abs(value) % 24;
  }

  get minutes() {
    return this._minutes;
  }

  set minutes(value) {
    this._minutes = Math.abs(value) % 60;
  }

  isDinnerTime() {
    return this.hours == Time.dinnerTime.hours && this.minutes == Time.dinnerTime.minutes;
  }
}
// Need to expose the type to the global scope.
globalThis.Time = Time;

您可以為它撰寫一個互操作介面,如下所示

dart
extension type Time._(JSObject _) implements JSObject {
  external Time(int hours, int minutes);
  external factory Time.onlyHours(int hours);

  external static Time dinnerTime;
  external static Time getTimeDifference(Time t1, Time t2);

  external int hours;
  external int minutes;
  external bool isDinnerTime();

  bool isMidnight() => hours == 0 && minutes == 0;
}

在互操作類型中,您可以宣告數種不同類型的 external 互操作成員

  • 建構函式。當呼叫時,僅具有位置參數的建構函式會使用 new 建立一個新的 JS 物件,其建構函式由擴充類型名稱定義。例如,在 Dart 中呼叫 Time(0, 0) 會產生一個看起來像 new Time(0, 0) 的 JS 呼叫。同樣地,呼叫 Time.onlyHours(0) 會產生一個看起來像 new Time(0) 的 JS 呼叫。請注意,無論是給予 Dart 名稱還是工廠,這兩個建構函式的 JS 呼叫都遵循相同的語意。

    • 物件文字建構函式。有時建立一個 JS 物件文字 很有用,它僅包含許多屬性和其值。為此,請宣告一個僅具有命名參數的建構函式,其中參數的名稱將是屬性名稱

      dart
      extension type Options._(JSObject o) implements JSObject {
        external Options({int a, int b});
        external int get a;
        external int get b;
      }

      呼叫 Options(a: 0, b: 1) 將會建立 JS 物件 {a: 0, b: 1}。此物件由呼叫引數定義,因此呼叫 Options(a: 0) 會產生 {a: 0}。您可以透過 external 執行個體成員取得或設定物件的屬性。

  • static 成員。與建構函式一樣,這些成員使用擴充類型名稱來產生 JS 程式碼。例如,呼叫 Time.getTimeDifference(t1, t2) 會產生一個看起來像 Time.getTimeDifference(t1, t2) 的 JS 呼叫。類似地,呼叫 Time.dinnerTime 會產生一個看起來像 Time.dinnerTime 的 JS 呼叫。與頂層一樣,您可以宣告 static 方法、取得器、設定器和欄位。

  • 實例成員。與其他 Dart 類型一樣,這些成員需要實例才能使用。這些成員取得、設定或呼叫實例的屬性。例如

    dart
      final time = Time(0, 0);
      print(time.isDinnerTime()); // false
      final dinnerTime = Time.dinnerTime;
      time.hours = dinnerTime.hours;
      time.minutes = dinnerTime.minutes;
      print(time.isDinnerTime()); // true

    呼叫 dinnerTime.hours 會取得 dinnerTimehours 屬性值。類似地,呼叫 time.minutes= 會設定 timeminutes 屬性值。呼叫 time.isDinnerTime() 會呼叫 timeisDinnerTime 屬性中的函式並傳回值。與頂層和 static 成員一樣,您可以宣告實例方法、取得器、設定器和欄位。

  • 運算子。在互通類型中只允許兩個 external 互通運算子:[][]=。這些是與 JS 的 屬性存取器 語意相符的實例成員。例如,您可以宣告它們如下

    dart
    extension type Array(JSArray<JSNumber> _) implements JSArray<JSNumber> {
      external JSNumber operator [](int index);
      external void operator []=(int index, JSNumber value);
    }

    呼叫 array[i] 會取得 array 中第 i 個槽位的數值,而 array[i] = i.toJS 會將該槽位的數值設定為 i.toJS。其他 JS 運算子會透過 dart:js_interop 中的 工具函式 公開。

最後,與其他擴充類型一樣,您可以在互通類型中宣告任何 external 成員isMidnight 就是一個這樣的範例。

互通型別的擴充成員

#

您也可以在互通類型的 擴充 中撰寫 external 成員。例如

dart
extension on Array {
  external int push(JSAny? any);
}

呼叫 push 的語意與它在 Array 定義中時完全相同。擴充可以有 external 實例成員和運算子,但不能有 external static 成員或建構函式。與互通類型一樣,您可以在擴充中撰寫任何非 external 成員。當互通類型沒有公開您需要的 external 成員,而您又不想建立新的互通類型時,這些擴充會很有用。

參數

#

external 互通方法只能包含位置引數和選用引數。這是因為 JS 成員只接受位置引數。唯一例外是物件文字建構函式,它們只能包含命名引數。

與非 external 方法不同,選用引數不會被替換為其預設值,而是會被省略。例如

dart
external int push(JSAny? any, [JSAny? any2]);

在 Dart 中呼叫 array.push(0.toJS) 會產生 array.push(0.toJS) 的 JS 呼叫,而不是 array.push(0.toJS, null)。這讓使用者不必為相同的 JS API 撰寫多個互通成員來避免傳入 null。如果您宣告一個具有明確預設值的參數,您會收到一個警告,表示該值將會被忽略。

@JS()

#

有時,使用與撰寫名稱不同的 JS 屬性會很有用。例如,如果你想撰寫兩個指向相同 JS 屬性的external API,你需要為其中至少一個撰寫不同的名稱。類似地,如果你想定義多個參考相同 JS 介面的互通類型,你需要重新命名其中至少一個。另一個範例是,如果 JS 名稱無法以 Dart 撰寫,例如 $a

為此,你可以使用具有常數字串值的 @JS() 註解。例如

dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
  external int push(JSNumber number);
  @JS('push')
  external int pushString(JSString string);
}

呼叫 pushpushString 都會產生使用 push 的 JS 程式碼。

你也可以重新命名互通類型

dart
@JS('Date')
extension type JSDate._(JSObject _) implements JSObject {
  external JSDate();

  external static int now();
}

呼叫 JSDate() 會產生 new Date() 的 JS 呼叫。類似地,呼叫 JSDate.now() 會產生 Date.now() 的 JS 呼叫。

此外,你可以命名空間整個函式庫,這會將前置詞新增到所有互通頂層成員、互通類型和這些類型中的static互通成員。如果你想避免將太多成員新增到全域 JS 範圍,這會很有用。

dart
@JS('library1')
library;

import 'dart:js_interop';

@JS()
external void method();

extension type JSType._(JSObject _) implements JSObject {
  external JSType();

  external static int get staticMember;
}

呼叫 method() 會產生 library1.method() 的 JS 呼叫,呼叫 JSType() 會產生 new library1.JSType() 的 JS 呼叫,而呼叫 JSType.staticMember 會產生 library1.JSType.staticMember 的 JS 呼叫。

與互通成員和互通類型不同,只有當你在函式庫的 @JS() 註解中提供非空值時,Dart 才會在 JS 呼叫中新增函式庫名稱。它不會使用函式庫的 Dart 名稱作為預設值。

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

呼叫 method() 會產生 method() 的 JS 呼叫,而不是 interop_library.method()

你也可以為函式庫、頂層成員和互通類型撰寫以 . 分隔的多個命名空間

dart
@JS('library1.library2')
library;

import 'dart:js_interop';

@JS('library3.method')
external void method();

@JS('library3.JSType')
extension type JSType._(JSObject _) implements JSObject {
  external JSType();
}

呼叫 method() 會產生 library1.library2.library3.method() 的 JS 呼叫,呼叫 JSType() 會產生 new library1.library2.library3.JSType() 的 JS 呼叫,依此類推。

不過,你無法在互通類型成員或互通類型延伸成員的值中使用 @JS() 註解和 .

如果未提供值給 @JS() 或值為空,則不會進行重新命名。

@JS() 也會告訴編譯器,成員或類型應視為 JS 互通成員或類型。對於所有頂層成員來說,這(有或沒有值)是必需的,以將它們與其他 external 頂層成員區分開來,但通常可以在互通類型和延伸成員中和其上省略,因為編譯器可以從表示類型和類型上判斷它是一個 JS 互通類型。

dart:js_interopdart:js_interop_unsafe

#

dart:js_interop 包含你應該需要的所有必要成員,包括 @JS、JS 類型、轉換函式和各種實用程式函式。實用程式函式包括

  • globalContext,代表編譯器用來尋找互操作成員和類型的全域範圍。
  • 用於檢查 JS 值類型的輔助函式
  • JS 算子
  • dartifyjsify,用於檢查特定 JS 值的類型,並將它們轉換為 Dart 值,反之亦然。當您知道 JS 值的類型時,建議使用特定轉換,因為額外的類型檢查可能會很耗費資源。
  • importModule,讓您可以動態匯入模組為 JSObject

未來可能會新增更多輔助函式至這個函式庫。

dart:js_interop_unsafe 包含讓您動態查詢屬性的成員。例如

dart
JSFunction f = console['log'];

我們沒有宣告一個名為 log 的互操作成員,而是使用字串來表示屬性。dart:js_interop_unsafe 提供動態取得、設定和呼叫屬性的功能。