目次

使用方法

JS相互運用は、DartからJavaScript APIと対話するためのメカニズムを提供します。明示的で慣用的な構文を使用して、これらのAPIを呼び出し、そこから取得した値と対話できます。

通常、グローバルJSスコープ内のどこかでJavaScript APIを使用可能にすることでアクセスします。このAPIからJS値を呼び出して受信するには、external相互運用メンバーを使用します。JS値の型を作成して提供するには、相互運用型を使用および宣言します。これには相互運用メンバーも含まれます。ListFunctionなどのDart値を相互運用メンバーに渡したり、JS値からDart値に変換するには、相互運用メンバーがプリミティブ型を含む場合を除き、変換関数を使用します。

相互運用型

#

JS値と対話する際には、Dart型を提供する必要があります。これは、相互運用型を使用するか、宣言することで実行できます。相互運用型は、Dartによって提供される"JS型"か、相互運用型をラップする拡張型のいずれかです。

相互運用型を使用すると、JS値のインターフェースを提供し、そのメンバーの相互運用APIを宣言できます。これらは、他の相互運用APIのシグネチャにも使用されます。

dart
extension type Window(JSObject _) implements JSObject {}

Windowは、任意のJSObjectに対する相互運用型です。Windowが実際にJSのWindowであるというランタイム保証はありません。また、同じ値に対して定義されている他の相互運用インターフェースとの競合もありません。Windowが実際にJSのWindowであることを確認する場合は、相互運用を通じてJS値の型を確認できます。

Dartが提供するJS型をラップすることで、独自のJS型に対する相互運用型を宣言することもできます。

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

ほとんどの場合、Dartによって提供される相互運用型を持たないJSオブジェクトと対話している可能性が高いため、表現型としてJSObjectを使用して相互運用型を宣言する可能性が高いです。

相互運用型は、一般的に実装する必要があります。表現型が期待される場所(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を取得および設定するには、同じ名前の相互運用ゲッターとセッターを宣言して使用します。isNameEmptyを使用するには、同じ名前の相互運用関数を宣言して呼び出します。トップレベルの相互運用ゲッター、セッター、メソッド、フィールドを宣言できます。相互運用フィールドは、ゲッターとセッターのペアに相当します。

トップレベルの相互運用メンバーは、dart:ffiを使用して記述できるものなど、他のexternalトップレベルメンバーと区別するために、@JS()アノテーションで宣言する必要があります。

相互運用型メンバー

#

次のような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;
  }
}
// Need to expose the type to the global scope.
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相互運用メンバーを宣言できます。

  • コンストラクタ。呼び出されると、位置パラメータのみを持つコンストラクタは、newを使用して拡張型の名前で定義されたコンストラクタを持つ新しいJSオブジェクトを作成します。たとえば、DartでTime(0, 0)を呼び出すと、new Time(0, 0)のようなJS呼び出しが生成されます。同様に、Time.onlyHours(0)を呼び出すと、new Time(0)のようなJS呼び出しが生成されます。Dart名を与えられたか、ファクトリであるかにかかわらず、2つのコンストラクタのJS呼び出しは同じセマンティクスに従います。

    • オブジェクトリテラルコンストラクタ。いくつかのプロパティとその値を含む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メソッド、ゲッター、セッター、フィールドを宣言できます。

  • インスタンスメンバー。他の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メンバーと同様に、インスタンスメソッド、ゲッター、セッター、フィールドを宣言できます。

  • 演算子。Interop型では、許可されるexternalインタロップ演算子は[][]=の2つのみです。これらは、JSのプロパティアクセサのセマンティクスに一致するインスタンスメンバーです。例えば、次のように宣言できます。

    dart
    extension type Array(JSArray<JSNumber> _) implements JSArray<JSNumber> {
      external JSNumber operator [](int index);
      external void operator []=(int index, JSNumber value);
    }

    array[i]を呼び出すと、arrayi番目のスロットの値を取得し、array[i] = i.toJSはそのスロットの値をi.toJSに設定します。その他のJS演算子は、dart:js_interopユーティリティ関数を通じて公開されます。

最後に、他の拡張型と同様に、Interop型にはexternalではないメンバーを宣言できます。isMidnightはその一例です。

相互運用型に対する拡張メンバー

#

Interop型の拡張機能にもexternalメンバーを記述できます。例えば

dart
extension on Array {
  external int push(JSAny? any);
}

pushの呼び出しのセマンティクスは、Arrayの定義内にある場合と同じです。拡張機能はexternalインスタンスメンバーと演算子を持つことができますが、external staticメンバーやコンストラクタを持つことはできません。Interop型と同様に、拡張機能にはexternalではないメンバーを記述できます。これらの拡張機能は、必要なexternalメンバーがInterop型で公開されておらず、新しいInterop型を作成したくない場合に役立ちます。

パラメータ

#

externalインタロップメソッドには、位置引数と省略可能な引数のみを含めることができます。これは、JSメンバーが位置引数のみを受け取るためです。例外はオブジェクトリテラルコンストラクタで、名前付き引数のみを含めることができます。

externalではないメソッドとは異なり、省略可能な引数はデフォルト値で置き換えられるのではなく、省略されます。例えば

dart
external int push(JSAny? any, [JSAny? any2]);

Dartでarray.push(0.toJS)を呼び出すと、array.push(0.toJS, null)ではなく、array.push(0.toJS)というJS呼び出しが行われます。これにより、nullを渡すのを避けるために、同じJS APIに対して複数のインタロップメンバーを記述する必要がなくなります。明示的なデフォルト値を持つパラメータを宣言すると、その値が無視されるという警告が表示されます。

@JS()

#

記述されているものとは異なる名前でJSプロパティを参照することが便利な場合があります。例えば、同じJSプロパティを指す2つのexternal APIを記述する場合、少なくとも一方に異なる名前を付ける必要があります。同様に、同じJSインターフェースを参照する複数のInterop型を定義する場合、少なくとも1つを名前変更する必要があります。別の例としては、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);
}

pushまたはpushStringのいずれかを呼び出すと、pushを使用するJSコードが生成されます。

Interop型も名前変更できます。

dart
@JS('Date')
extension type JSDate._(JSObject _) implements JSObject {
  external JSDate();

  external static int now();
}

JSDate()を呼び出すと、new Date()というJS呼び出しが行われます。同様に、JSDate.now()を呼び出すと、Date.now()というJS呼び出しが行われます。

さらに、ライブラリ全体に名前空間を付けることができます。これにより、それらの型内のすべてのInteropトップレベルメンバー、Interop型、およびstatic Interopメンバーにプレフィックスが追加されます。これは、グローバルな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呼び出しが行われます。

InteropメンバーやInterop型とは異なり、Dartはライブラリの@JS()アノテーションに空でない値を指定した場合にのみ、JS呼び出しにライブラリ名を付加します。デフォルトでは、Dartのライブラリ名は使用しません。

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

method()を呼び出すと、interop_library.method()ではなくmethod()というJS呼び出しが行われます。

ライブラリ、トップレベルメンバー、およびInterop型に対して、`.`で区切られた複数の名前空間を記述することもできます。

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呼び出しが行われます。

ただし、Interop型メンバーまたはInterop型の拡張メンバーの値に`.`を含む@JS()アノテーションを使用することはできません。

@JS()に値が指定されていない場合、または値が空の場合、名前変更は行われません。

@JS()は、メンバーまたは型がJSインタロップメンバーまたは型として扱われることをコンパイラに指示します。他のexternalトップレベルメンバーと区別するために、すべてのトップレベルメンバーには(値の有無にかかわらず)必須ですが、コンパイラは表現型と型からJSインタロップ型であることを認識できるため、Interop型内および拡張メンバー上では多くの場合省略できます。

Dartの関数とオブジェクトをJSにエクスポートする

#

上記のセクションでは、DartからJSメンバーを呼び出す方法を示しています。JSで使用できるようにDartコードをエクスポートすることも役立ちます。Dart関数をJSにエクスポートするには、まずFunction.toJSを使用して変換します。これは、Dart関数をJS関数でラップします。次に、ラップされた関数をInteropメンバーを通じてJSに渡します。その時点で、他のJSコードから呼び出し準備が完了します。

例えば、このコードはDart関数を変換し、Interopを使用してグローバルプロパティに設定し、その後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');

このようにエクスポートされた関数は、Interopメンバーと同様の制限があります。

JSがDartオブジェクトと対話できるように、Dartインターフェース全体をエクスポートすることが便利な場合があります。これを行うには、@JSExportを使用してDartクラスをエクスポート可能としてマークし、createJSInteropWrapperを使用してそのクラスのインスタンスをラップします。JS値のモック方法を含むこのテクニックの詳細な説明については、モックに関するチュートリアルを参照してください。

dart:js_interopdart:js_interop_unsafe

#

dart:js_interopには、@JS、JS型、変換関数、およびさまざまなユーティリティ関数など、必要なメンバーがすべて含まれています。ユーティリティ関数には、

  • コンパイラがInteropメンバーと型を見つけるために使用するグローバルスコープを表すglobalContext
  • JS値の型を検査するためのヘルパー。
  • JS演算子。
  • 特定のJS値の型をチェックし、Dart値と相互に変換するdartifyjsify。JS値の型がわかっている場合は、特定の変換を使用することをお勧めします。追加の型チェックは高価になる可能性があります。
  • モジュールをJSObjectとして動的にインポートできるimportModule

このライブラリには、将来的にさらに多くのユーティリティが追加される可能性があります。

dart:js_interop_unsafeには、プロパティを動的に検索できるメンバーが含まれています。例えば

dart
JSFunction f = console['log'];

logという名前のInteropメンバーを宣言する代わりに、文字列を使用してプロパティを表しています。dart:js_interop_unsafeは、プロパティを動的に取得、設定、および呼び出す機能を提供します。