目录

用法

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 实际上是JSWindow。它也不会与为相同值定义的任何其他互操作接口冲突。如果您想检查 Window 实际上是否是JS Window ,您可以[通过互操作检查JS值的类型]。

您还可以通过包装Dart提供的JS类型来声明您自己的JS类型的互操作类型:

dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
  external Array();
}

在大多数情况下,您可能会使用 JSObject 作为[表示类型]来声明互操作类型,因为您很可能正在与JS对象交互,而这些对象没有Dart提供的互操作类型。

互操作类型通常也应该[实现]它们的表示类型,以便它们可以在需要表示类型的任何地方使用,例如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;
  }
}
// 需要将类型暴露给全局作用域。
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= 的调用设置 timeminutes 属性的值。对 time.isDinnerTime() 的调用调用 timeisDinnerTime 属性中的函数并返回该值。与顶级成员和 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] 获取 array 中第 i 个槽中的值,而 array[i] = i.toJS 将该槽中的值设置为 i.toJS 。其他JS运算符通过 dart:js_interop 中的[实用函数]公开。

最后,与任何其他扩展类型一样,您允许在互操作类型中声明任何[非 external 成员]。 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) 将导致对 array.push(0.toJS) 的JS调用, 而不是 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() 将导致对 new Date() 的JS调用。类似地,调用 JSDate.now() 将导致对 Date.now() 的JS调用。

此外,您可以为整个库命名空间,这将为这些类型中的所有互操作顶级成员、互操作类型和 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() 将导致对 library1.method() 的JS调用,调用 JSType() 将导致对 new library1.JSType() 的JS调用,调用 JSType.staticMember 将导致对 library1.JSType.staticMember 的JS调用。 与互操作成员和互操作类型不同,只有当您在库上的 @JS() 注解中提供非空值时,Dart才会在JS调用中添加库名称。它不使用库的Dart名称作为默认值。

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

调用 method() 将导致对 method() 的JS调用,而不是 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() 将导致对 library1.library2.library3.method() 的JS调用,调用 JSType() 将导致对 new library1.library2.library3.JSType() 的JS调用,依此类推。

但是,您不能在互操作类型成员或互操作类型的扩展成员上的值中使用带有 .@JS() 注解。

如果未向 @JS() 提供值或值为empty,则不会发生重命名。

@JS() 还会告诉编译器成员或类型旨在被视为JS互操作成员或类型。对于所有顶级成员,都需要(无论是否有值)将其与其他 external 顶级成员区分开来,但在互操作类型内部和扩展成员上,通常可以省略,因为编译器可以从表示类型和类型推断出它是JS互操作类型。

将Dart函数和对象导出到JS

#

以上部分显示了如何从Dart调用JS成员。将Dart代码 导出 以便可以在JS中使用它也很有用。要将Dart函数导出到JS,首先使用Function.toJS将其转换,这会用JS函数包装Dart函数。然后,通过互操作成员将包装后的函数传递给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值),请参阅[模拟教程]。

dart:js_interopdart:js_interop_unsafe

#

dart:js_interop包含您可能需要的所有必要成员,包括 @JS 、JS类型、转换函数和各种实用函数。实用函数包括:

  • globalContext,它表示编译器用于查找互操作成员和类型的全局作用域。
  • [检查JS值类型的帮助器]
  • JS运算符
  • dartifyjsify,它们检查某些JS值的类型并将其转换为Dart值,反之亦然。当您知道JS值的类型时,最好使用特定的转换,因为额外的类型检查可能很昂贵。
  • importModule,它允许您动态地将模块导入为 JSObject

将来可能会向该库添加更多实用程序。

dart:js_interop_unsafe包含允许您动态查找属性的成员。例如:

dart
JSFunction f = console['log'];

我们没有声明名为 log 的互操作成员,而是使用字符串来表示该属性。 dart:js_interop_unsafe 提供了动态获取、设置和调用属性的功能。