用法
JS 互操作提供了从 Dart 与 JavaScript API 交互的机制。它允许你调用这些 API 并使用显式、惯用的语法与你从它们获取的值进行交互。
通常,你可以通过将 JavaScript API 放在 全局 JS 范围 中的某个位置来访问它。要从这个 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
。
与互操作成员和互操作类型不同,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
顶层成员区分开来,但在互操作类型内部以及扩展成员上通常可以省略,因为编译器可以从表示类型和类型上判断它是 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 值,请参见 模拟教程。
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.5.3。页面最后更新于 2024-08-06。 查看源代码 或 报告问题.