跳到主内容

如何模拟 JavaScript 互操作对象

在本教程中,你将学习如何模拟 JS 对象,以便在不使用真实实现的情况下测试互操作实例成员。

背景与动机

#

在 Dart 中模拟类通常通过覆盖实例成员来实现。然而,由于使用扩展类型来声明互操作类型,所有扩展类型成员都是静态分派的,因此不能使用覆盖。此限制也适用于扩展成员,因此无法模拟实例扩展类型或扩展成员。

虽然这适用于任何非external扩展类型成员,但external互操作成员是特殊的,因为它们在 JS 值上调用成员。

dart
extension type Date(JSObject _) implements JSObject {
  external int getDay();
}

用法部分所述,调用getDay()将导致在 JS 对象上调用getDay()。因此,通过使用不同的JSObject,可以调用getDay的不同实现

为此,应该有一种机制来创建一个具有getDay属性的 JS 对象,该属性在调用时会调用 Dart 函数。一种简单的方法是创建一个 JS 对象并将getDay属性设置为转换后的回调,例如。

dart
final date = Date(JSObject());
date['getDay'] = (() => 0).toJS;

虽然这可行,但这容易出错,并且在使用许多互操作成员时扩展性不佳。它也无法正确处理 getter 或 setter。相反,你应该结合使用createJSInteropWrapper@JSExport来声明一个类型,该类型为所有external实例成员提供实现。

模拟示例

#
dart
import 'dart:js_interop';

import 'package:expect/minitest.dart';

// The Dart class must have `@JSExport` on it or at least one of its instance
// members.
@JSExport()
class FakeCounter {
  int value = 0;
  @JSExport('increment')
  void renamedIncrement() {
    value++;
  }
  void decrement() {
    value--;
  }
}

extension type Counter(JSObject _) implements JSObject {
  external int value;
  external void increment();
  void decrement() {
    value -= 2;
  }
}

void main() {
  var fakeCounter = FakeCounter();
  // Returns a JS object whose properties call the relevant instance members in
  // `fakeCounter`.
  var counter = createJSInteropWrapper<FakeCounter>(fakeCounter) as Counter;
  // Calls `FakeCounter.value`.
  expect(counter.value, 0);
  // `FakeCounter.renamedIncrement` is renamed to `increment`, so it gets
  // called.
  counter.increment();
  expect(counter.value, 1);
  expect(fakeCounter.value, 1);
   // Changes in the fake affect the wrapper and vice-versa.
  fakeCounter.value = 0;
  expect(counter.value, 0);
  counter.decrement();
  // Because `Counter.decrement` is non-`external`, we never called
  // `FakeCounter.decrement`.
  expect(counter.value, -2);
}

@JSExport 允许你声明一个可在createJSInteropWrapper中使用的类。createJSInteropWrapper 将创建一个对象字面量,它将类的每个实例成员名称(或重命名)映射到一个 JS 回调,该回调使用Function.toJS创建。当被调用时,JS 回调将反过来调用实例成员。在上面的示例中,获取和设置counter.value会获取和设置fakeCounter.value

你可以通过从类中省略注解而只注解特定成员来指定类中的某些成员进行导出。你可以在@JSExport的文档中查看关于更专业化导出(包括继承)的更多细节。

请注意,此机制并非仅限于测试。你可以使用它为任意 Dart 对象提供一个 JS 接口,从而让你能够实质上将 Dart 对象导出到 JS,并带有一个预定义的接口。