用法
JS 互操作提供了从 Dart 与 JavaScript API 交互的机制。它允许你使用显式、惯用的语法调用这些 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 值的类型。
你也可以通过封装 Dart 提供的 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
成员。使用互操作值的布尔 getter isMidnight
就是一个这样的例子。
互操作类型的扩展成员
#你也可以在互操作类型的扩展中编写 external
成员。例如
extension on Array {
external int push(JSAny? any);
}
调用 push
的语义与它在 Array
定义中时的语义完全相同。扩展可以有 external
实例成员和运算符,但不能有 external
static
成员或构造函数。与互操作类型一样,你可以在扩展中编写任何非 external
成员。当互操作类型没有公开你需要的 external
成员,并且你不想创建新的互操作类型时,这些扩展很有用。
参数
#external
互操作方法只能包含位置参数和可选参数。这是因为 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 编写多个互操作成员来避免传入 null
。如果你声明了一个带有显式默认值的参数,你会收到一个警告,表示该值将被忽略。
@JS()
#有时使用与书写名称不同的名称来引用 JS 属性很有用。例如,如果你想编写两个指向同一个 JS 属性的 external
API,你需要至少为其中一个编写不同的名称。类似地,如果你想定义多个引用同一 JS 接口的互操作类型,你需要重命名至少其中一个。另一个例子是如果 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 代码。
你也可以重命名互操作类型
@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 作用域添加太多成员,这会很有用。
@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 库的名称作为默认值。
library interop_library;
import 'dart:js_interop';
@JS()
external void method();
调用 method()
会导致 JS 调用 method()
,而不是 interop_library.method()
。
你还可以为库、顶层成员和互操作类型编写由 .
分隔的多个命名空间
@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 中被调用
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');
以这种方式导出的函数具有与互操作成员类似的类型限制。
有时导出整个 Dart 接口以便 JS 可以与 Dart 对象交互很有用。为此,使用 @JSExport
将 Dart 类标记为可导出,并使用 createJSInteropWrapper
封装该类的实例。有关此技术的更详细说明,包括如何模拟 JS 值,请查看如何模拟 JavaScript 互操作对象。
dart:js_interop 和 dart:js_interop_unsafe
#dart:js_interop
包含你可能需要的所有必要成员,包括 @JS
、JS 类型、转换函数和各种实用函数。实用函数包括
globalContext
,它表示编译器用于查找互操作成员和类型的全局作用域。- 用于检查 JS 值类型的辅助函数
- JS 运算符
dartify
和jsify
,它们检查某些 JS 值的类型并将其转换为 Dart 值,反之亦然。当你知道 JS 值的类型时,优先使用特定的转换,因为额外的类型检查可能会很耗时。importModule
,它允许你将模块动态导入为JSObject
。isA
,它允许你检查 JS 互操作值是否是类型参数指定的 JS 类型的实例。
将来可能会在此库中添加更多实用函数。
dart:js_interop_unsafe
包含允许你动态查找属性的成员。例如
JSFunction f = console['log'];
你可以使用字符串而不是声明名为 log
的互操作成员来访问属性。dart:js_interop_unsafe
还提供了动态检查、获取、设置和调用属性的函数。