用法
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
成员。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,则至少需要为其中一个 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
。
与互操作成员和互操作类型不同,只有当您在库的 @JS()
注释中提供非空值时,Dart 才会在 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
顶层成员区分开来,但通常可以在互操作类型及其内部以及扩展成员上省略,因为编译器可以从表示类型和类型上判断它是 JS 互操作类型。
将 Dart 函数和对象导出到 JS
#以上各节介绍了如何从 Dart 调用 JS 成员。 导出 Dart 代码以便在 JS 中使用也很有用。要将 Dart 函数导出到 JS,首先使用 Function.toJS
将其转换,这将使用 JS 函数包装 Dart 函数。然后,通过互操作成员将包装的函数传递给 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 值),请参阅 模拟教程。
dart:js_interop
和 dart:js_interop_unsafe
#dart:js_interop
包含您应该需要的所有必要成员,包括 @JS
、JS 类型、转换函数和各种实用程序函数。实用程序函数包括
globalContext
,它表示编译器用于查找互操作成员和类型的全局作用域。- 用于检查 JS 值类型的助手
- JS 运算符
dartify
和jsify
,它们检查某些 JS 值的类型,并将其转换为 Dart 值,反之亦然。当您知道 JS 值的类型时,最好使用特定的转换,因为额外的类型检查可能很昂贵。importModule
,允许您以JSObject
的形式动态导入模块。
将来可能会向此库添加更多实用程序。
dart:js_interop_unsafe
包含允许您动态查找属性的成员。例如
JSFunction f = console['log'];
我们没有声明名为 log
的互操作成员,而是使用字符串来表示属性。 dart:js_interop_unsafe
提供了动态获取、设置和调用属性的功能。
除非另有说明,否则本网站上的文档反映的是 Dart 3.6.0。页面上次更新于 2024-11-27。 查看源代码 或 报告问题。