用法
JS 互通提供從 Dart 與 JavaScript API 互動的機制。它允許您呼叫這些 API 並使用明確的慣用語法與從它們取得的值互動。
通常,您可以將 JavaScript API 設為在全域 JS 範圍內的某個位置可用,以存取 JavaScript API。若要從此 API 呼叫並接收 JS 值,您可以使用external
互通成員。為了為 JS 值建構和提供類型,您可以使用和宣告互通類型,其中也包含互通成員。若要將 Dart 值(例如 List
或 Function
)傳遞至互通成員,或從 JS 值轉換為 Dart 值,您可以使用轉換函式,除非互通成員包含原始類型。
互通類型
#與 JS 值互動時,您需要為它提供 Dart 類型。您可以透過使用或宣告互通類型來執行此操作。互通類型是 Dart 提供的「JS 類型」,或是包裝互通類型的擴充類型。
互通類型允許您為 JS 值提供介面,並讓您為其成員宣告互通 API。它們也用於其他互通 API 的簽名中。
extension type Window(JSObject _) implements JSObject {}
Window
是任意 JSObject
的互通類型。無法保證執行階段 Window
實際上是 JS Window
。與為相同值定義的任何其他互通介面也沒有衝突。如果您想要檢查 Window
是否實際上是 JS Window
,您可以透過互通檢查 JS 值的類型。
您也可以藉由包裝 JS 類型來宣告自己的 JS 類型互通類型
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
external Array();
}
在大多數情況下,您可能會使用 JSObject
作為表示類型來宣告互通類型,因為您可能與沒有 Dart 提供互通類型的 JS 物件互動。
互通類型通常也應該實作其表示類型,以便它們可以在預期使用表示類型的地方使用,例如在package:web
中的許多 API 中。
互通成員
#external
互通成員為 JS 成員提供慣用語法。它們允許您為其引數和傳回值撰寫 Dart 類型簽名。可以在這些成員的簽名中撰寫的類型有限制。互通成員對應的 JS API 取決於宣告位置、名稱、它是何種 Dart 成員以及任何重新命名的組合。
最上層互通成員
#假設以下 JS 成員
globalThis.name = 'global';
globalThis.isNameEmpty = function() {
return globalThis.name.length == 0;
}
您可以像這樣為它們撰寫互通成員
@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 介面
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;
您可以像這樣為它撰寫互通介面
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 物件常值是很有用的。為了執行此操作,您可以宣告一個僅具有具名參數的建構函式,其中參數名稱將為屬性名稱
dartextension 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 類型一樣,這些成員需要一個執行個體才能使用。這些成員會取得、設定或呼叫執行個體上的屬性。例如
dartfinal 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
會取得dinnerTime
的hours
屬性的值。同樣地,呼叫time.minutes=
會設定time
的minutes
屬性的值。呼叫time.isDinnerTime()
會呼叫time
的isDinnerTime
屬性中的函式並傳回值。與最上層和static
成員一樣,您可以宣告執行個體方法、getter、setter 和欄位。運算子。在互通類型中只允許兩個
external
互通運算子:[]
和[]=
。這些是符合 JS 的屬性存取器語意的執行個體成員。例如,您可以像這樣宣告它們dartextension 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
成員。例如
extension on Array {
external int push(JSAny? any);
}
呼叫 push
的語意與它在 Array
定義中的語意相同。擴展可以擁有 external
實例成員和運算符,但不能擁有 external
static
成員或建構函式。如同 interop 類型,您可以在擴展中撰寫任何非 external
成員。當 interop 類型沒有公開您需要的 external
成員,且您不想建立新的 interop 類型時,這些擴展會很有用。
參數
#external
interop 方法只能包含位置和可選參數。這是因為 JS 成員僅接受位置參數。唯一的例外是物件字面值建構函式,它們只能包含具名參數。
與非 external
方法不同,可選參數不會被其預設值取代,而是會被省略。例如
external int push(JSAny? any, [JSAny? any2]);
在 Dart 中呼叫 array.push(0.toJS)
將導致 JS 呼叫 array.push(0.toJS)
,而不是 array.push(0.toJS, null)
。這讓使用者不必為同一個 JS API 撰寫多個 interop 成員來避免傳入 null
。如果您宣告帶有明確預設值的參數,您會收到警告,表示該值將被忽略。
@JS()
#有時,使用與撰寫時不同的名稱來參考 JS 屬性會很有用。例如,如果您想撰寫兩個指向同一個 JS 屬性的 external
API,您至少需要為其中一個撰寫不同的名稱。同樣地,如果您想定義多個參考同一個 JS 介面的 interop 類型,您至少需要重新命名其中一個。另一個例子是 JS 名稱無法在 Dart 中撰寫,例如 $a
。
為了做到這一點,您可以使用帶有常數字串值的 @JS()
註解。例如
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
external int push(JSNumber number);
@JS('push')
external int pushString(JSString string);
}
呼叫 push
或 pushString
都會產生使用 push
的 JS 程式碼。
您也可以重新命名 interop 類型
@JS('Date')
extension type JSDate._(JSObject _) implements JSObject {
external JSDate();
external static int now();
}
呼叫 JSDate()
將導致 JS 呼叫 new Date()
。同樣地,呼叫 JSDate.now()
將導致 JS 呼叫 Date.now()
。
此外,您可以對整個函式庫命名空間,這將在這些類型中為所有 interop 頂層成員、interop 類型和 static
interop 成員加上前綴。如果您想避免在全域 JS 範圍中加入太多成員,這會很有用。
@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
。
與 interop 成員和 interop 類型不同,只有當您在函式庫的 @JS()
註解中提供非空值時,Dart 才會在 JS 呼叫中加入函式庫名稱。它不會使用函式庫的 Dart 名稱作為預設值。
library interop_library;
import 'dart:js_interop';
@JS()
external void method();
呼叫 method()
將導致 JS 呼叫 method()
,而不是 interop_library.method()
。
您也可以為函式庫、頂層成員和 interop 類型撰寫多個以 .
分隔的命名空間
@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()
,依此類推。
但是,您不能在 interop 類型成員或 interop 類型的擴展成員上使用值中帶有 .
的 @JS()
註解。
如果沒有為 @JS()
提供值,或值為空,則不會進行重新命名。
@JS()
也會告知編譯器,成員或類型打算被視為 JS interop 成員或類型。所有頂層成員都必須使用(無論有無值)它,以將它們與其他 external
頂層成員區分開來,但通常可以省略 interop 類型及其內部以及擴展成員上的它,因為編譯器可以從表示類型和 on-type 判斷它是否為 JS interop 類型。
將 Dart 函式和物件匯出至 JS
#以上章節展示了如何從 Dart 呼叫 JS 成員。匯出 Dart 程式碼以便在 JS 中使用也很有用。若要將 Dart 函數匯出到 JS,請先使用 Function.toJS
將其轉換,這會使用 JS 函數包裝 Dart 函數。然後,透過 interop 成員將包裝的函數傳遞給 JS。在那時,它就可以被其他 JS 程式碼呼叫了。
例如,此程式碼會轉換 Dart 函數,並使用 interop 將其設定在全域屬性中,然後在 JS 中呼叫它
import 'dart:js_interop';
@JS()
external set exportedFunction(JSFunction value);
void printString(JSString string) {
print(string.toDart);
}
void main() {
exportedFunction = printString.toJS;
}
globalThis.exportedFunction('hello world');
以這種方式匯出的函數具有與 interop 成員相似的 類型限制。
有時,匯出整個 Dart 介面以便 JS 可以與 Dart 物件互動會很有用。為此,請使用 @JSExport
將 Dart 類別標記為可匯出,並使用 createJSInteropWrapper
包裝該類別的實例。如需此技術的更詳細說明,包括如何模擬 JS 值,請參閱模擬教學。
dart:js_interop
和 dart:js_interop_unsafe
#dart:js_interop
包含您應該需要的所有必要成員,包括 @JS
、JS 類型、轉換函數和各種實用程式函數。實用程式函數包括
globalContext
,它代表編譯器用來尋找 interop 成員和類型的全域範圍。- 用於檢查 JS 值類型的協助程式
- JS 運算符
dartify
和jsify
,它們會檢查某些 JS 值的類型,並將它們轉換為 Dart 值,反之亦然。當您知道 JS 值的類型時,最好使用特定的轉換,因為額外的類型檢查可能會很耗費效能。importModule
,它允許您將模組動態匯入為JSObject
。
未來可能會將更多實用程式新增至此函式庫。
dart:js_interop_unsafe
包含允許您動態查詢屬性的成員。例如
JSFunction f = console['log'];
我們沒有宣告名為 log
的 interop 成員,而是使用字串來表示屬性。dart:js_interop_unsafe
提供了動態取得、設定和呼叫屬性的功能。
除非另有說明,否則本網站上的文件反映的是 Dart 3.6.0。頁面最後更新時間為 2024-11-27。 檢視原始碼 或 回報問題。