跳到主要內容

用法

JS 互操作提供了從 Dart 與 JavaScript API 互動的機制。它允許你使用顯式、慣用的語法呼叫這些 API 並與其返回的值進行互動。

通常,透過使其在全域性 JS 作用域內可用,你可以訪問 JavaScript API。要呼叫此 API 並接收其中的 JS 值,你可以使用external 互操作成員。要構造 JS 值併為其提供型別,你可以使用並宣告互操作型別,互操作型別也包含互操作成員。要將 Dart 值(如 ListFunction)傳遞給互操作成員或將 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 互操作成員

  • 建構函式。僅包含位置引數的建構函式在呼叫時會建立一個新的 JS 物件,其建構函式由擴充套件型別的名稱使用 new 定義。例如,在 Dart 中呼叫 Time(0, 0) 會生成看起來像 new Time(0, 0) 的 JS 呼叫。類似地,呼叫 Time.onlyHours(0) 會生成看起來像 new Time(0) 的 JS 呼叫。請注意,這兩個建構函式的 JS 呼叫遵循相同的語義,無論它們是否具有 Dart 名稱或者它們是否是工廠建構函式。

    • 物件字面量建構函式。有時建立僅包含多個屬性及其值的 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 方法、getter、setter 和欄位。

  • 例項成員。與其他 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= 設定 time 的 minutes 屬性值。呼叫 time.isDinnerTime() 呼叫 time 的 isDinnerTime 屬性中的函式並返回值。與頂層成員和 static 成員類似,你可以宣告例項方法、getter、setter 和欄位。

  • 運算子。互操作型別中只允許使用兩個 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] 獲取 arrayi 個槽位中的值,而 array[i] = i.toJS 將該槽位中的值設定為 i.toJS。其他 JS 運算子由 dart:js_interop 中的實用函式公開。

最後,與任何其他擴充套件型別一樣,你可以在互操作型別中宣告任何external 成員。使用互操作值的布林 getter 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) 會導致 JS 呼叫 array.push(0.toJS),而不是 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() 會導致 JS 呼叫 new Date()。類似地,呼叫 JSDate.now() 會導致 JS 呼叫 Date.now()

此外,你可以為整個庫設定名稱空間,為該庫中所有互操作頂層成員、互操作型別以及這些型別內的 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() 會導致 JS 呼叫 library1.method(),呼叫 JSType() 會導致 JS 呼叫 new library1.JSType(),呼叫 JSType.staticMember 會導致 JS 呼叫 library1.JSType.staticMember

與互操作成員和互操作型別不同,Dart 只有在庫的 @JS() 註解中提供了非空值時,才會將庫名稱新增到 JS 呼叫中。它不會將 Dart 庫的名稱作為預設值。

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

呼叫 method() 會導致 JS 呼叫 method(),而不是 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() 會導致 JS 呼叫 library1.library2.library3.method(),呼叫 JSType() 會導致 JS 呼叫 new library1.library2.library3.JSType(),依此類推。

但是,你不能在互操作型別成員或互操作型別擴充套件成員的值中使用帶有 .@JS() 註解。

如果未向 @JS() 提供值或值為空,則不會發生重新命名。

@JS() 也告訴編譯器一個成員或型別旨在被視為 JS 互操作成員或型別。對於所有頂層成員來說,它是必需的(無論是否有值),以區分它們與其他 external 頂層成員,但由於編譯器可以從表示型別和 on-type 判斷出它是 JS 互操作型別,因此通常可以在互操作型別上和內部以及擴充套件成員上省略它。

將 Dart 函式和物件匯出到 JS

#

前面的章節展示瞭如何在 Dart 中呼叫 JS 成員。將 Dart 程式碼匯出以便在 JS 中使用也很有用。要將 Dart 函式匯出到 JS,首先使用 Function.toJS 進行轉換,它將 Dart 函式封裝到 JS 函式中。然後,透過互操作成員將封裝後的函式傳遞給 JS。此時,它就可以被其他 JS 程式碼呼叫了。

例如,此程式碼轉換 Dart 函式並使用互操作將其設定為全域性屬性,然後該屬性在 JS 中被呼叫

dart
import 'dart:js_interop';

@JS()
external set exportedFunction(JSFunction value);

void printString(JSString string) {
  print(string.toDart);
}

void main() {
  exportedFunction = printString.toJS;
}
js
globalThis.exportedFunction('hello world');

以這種方式匯出的函式具有與互操作成員類似的型別限制

有時匯出整個 Dart 介面以便 JS 可以與 Dart 物件互動很有用。為此,使用 @JSExport 將 Dart 類標記為可匯出,並使用 createJSInteropWrapper 封裝該類的例項。有關此技術的更詳細說明,包括如何模擬 JS 值,請檢視如何模擬 JavaScript 互操作物件

dart:js_interop 和 dart:js_interop_unsafe

#

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

  • globalContext,它表示編譯器用於查詢互操作成員和型別的全域性作用域。
  • 用於檢查 JS 值型別的輔助函式
  • JS 運算子
  • dartifyjsify,它們檢查某些 JS 值的型別並將其轉換為 Dart 值,反之亦然。當你知道 JS 值的型別時,優先使用特定的轉換,因為額外的型別檢查可能會很耗時。
  • importModule,它允許你將模組動態匯入為 JSObject
  • isA,它允許你檢查 JS 互操作值是否是型別引數指定的 JS 型別的例項。

將來可能會在此庫中新增更多實用函式。

dart:js_interop_unsafe 包含允許你動態查詢屬性的成員。例如

dart
JSFunction f = console['log'];

你可以使用字串而不是宣告名為 log 的互操作成員來訪問屬性。dart:js_interop_unsafe 還提供了動態檢查、獲取、設定和呼叫屬性的函式。