メインコンテンツにスキップ

使用法

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

通常、JavaScript APIには、グローバルJSスコープのどこかにアクセスできるようにしてアクセスします。このAPIからJS値を呼び出したり受け取ったりするには、external相互運用メンバーを使用します。JS値の型を構築および提供するには、相互運用型を使用および宣言します。これらには相互運用メンバーも含まれます。Dart値を`List`や`Function`のような相互運用メンバーに渡したり、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型に対して、それらをラップすることで独自の相互運用型を宣言することもできます。

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相互運用メンバーを宣言できます。

  • コンストラクタ。呼び出されたとき、位置引数のみを持つコンストラクタは、拡張型の名前によって定義される新しいJSオブジェクトをnewを使用して作成します。たとえば、DartでTime(0, 0)を呼び出すと、new Time(0, 0)のように見えるJS呼び出しが生成されます。同様に、Time.onlyHours(0)を呼び出すと、new Time(0)のように見えるJS呼び出しが生成されます。2つのコンストラクタの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メソッド、ゲッター、セッター、フィールドを宣言できます。

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

  • 演算子。相互運用型で許可される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ユーティリティ関数によって公開されます。

最後に、他の拡張型と同様に、相互運用型で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)を呼び出すと、JS呼び出しはarray.push(0.toJS)になり、array.push(0.toJS, null)にはなりません。これにより、ユーザーは同じJS APIに対して複数の相互運用メンバーを記述してnullの渡しを避ける必要がなくなります。明示的なデフォルト値を持つパラメータを宣言すると、その値は無視されるという警告が表示されます。

@JS()

#

JSプロパティを、記述されている名前とは異なる名前で参照すると便利な場合があります。たとえば、同じJSプロパティを指す2つのexternal APIを記述したい場合、少なくとも一方に異なる名前を記述する必要があります。同様に、同じJSインターフェースを参照する複数の相互運用型を定義したい場合は、少なくとも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コードが生成されます。

相互運用型をリネームすることもできます。

dart
@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スコープに多くのメンバーを追加することを避けたい場合に便利です。

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()を呼び出すと、JS呼び出しlibrary1.method()が生成され、JSType()を呼び出すと、JS呼び出しnew library1.JSType()が生成され、JSType.staticMemberを呼び出すと、JS呼び出しlibrary1.JSType.staticMemberが生成されます。

相互運用メンバーおよび相互運用型とは異なり、ライブラリの@JS()アノテーションに空でない値を提供した場合にのみ、DartはJS呼び出しにライブラリ名を追加します。ライブラリのDart名はデフォルトとして使用されません。

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

method()を呼び出すと、JS呼び出しmethod()が生成され、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()を呼び出すと、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で呼び出します。

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');

このようにエクスポートされた関数は、相互運用メンバーと同様の型制限を持ちます。

JSがDartインターフェース全体と対話できるように、Dartインターフェース全体をエクスポートすると便利な場合があります。これを行うには、Dartクラスをエクスポート可能としてマークするために@JSExportを使用し、そのクラスのインスタンスをcreateJSInteropWrapperでラップします。この手法の詳細については、JS値のモック方法など、JavaScript相互運用オブジェクトのモック方法を参照してください。

dart:js_interopおよびdart:js_interop_unsafe

#

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

  • globalContext。これは、コンパイラが相互運用メンバーと型を見つけるために使用するグローバルスコープを表します。
  • JS値の型を検査するためのヘルパー
  • JS演算子
  • dartifyおよびjsify。これらは特定のJS値の型をチェックし、Dart値に変換したり、その逆を行ったりします。JS値の型がわかっている場合は、特定の変換を優先してください。追加の型チェックはコストがかかる可能性があるためです。
  • importModule。これにより、モジュールをJSObjectとして動的にインポートできます。
  • isA。これにより、JS相互運用値が型引数で指定されたJS型のインスタンスであるかどうかをチェックできます。

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

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

dart
JSFunction f = console['log'];

logという名前の相互運用メンバーを宣言する代わりに、文字列を使用してプロパティにアクセスできます。dart:js_interop_unsafeは、プロパティを動的にチェック、取得、設定、および呼び出すための関数も提供します。