JSのライブラリを簡単にDartから使う方法

こんにちは。Dart初心者の岸野(@kishisuke)です。

BaaS@rakuzaのFlutter SDK開発中! – PEOPLE Engineering Blog
にて、BaaS@rakuzaも将来的にFlutter Webに対応したいと書きましたが、どうやってWebに対応すれば良いのか調べてみました。

方針としては、BaaS@rakuza JavaScript SDKをDartのコードから呼び出して、再利用します!

DartからJSのコードにアクセスするには

Dartではpkg/jsを使ってJSのコードを呼び出すことができます。(JS interop)
例えば以下の様に、externalを付けた関数を定義して、@JS("JS側の関数名")を付けます。

// Dart
@JS("JSON.stringify")
external String stringify(obj);

 
JS interopを使ってDartライブラリを提供している例としては、chartjsfirebaseがあります。

JS interopを自動生成

JS interopのコードを手動で作成するのは大変そうなので、dart_js_facade_genという、TypeScriptの型定義ファイルからJS interopのコードを自動生成するツールを使います。
このdart_js_facade_gen、3年間ぐらい更新されてなくて、最新のDartに追従できていなかったのですが、つい3週間前ぐらいに久しぶりに更新されました。

JavaScript SDKはTypeScriptで開発しており、型定義ファイルも配布しているので、それを使います。

自動生成は以下のコマンドで行います。自動生成されたコードは標準出力に吐き出されるので、リダイレクトしてファイルに書き込んでます。(ディレクトリ指定など、細かいオプションも指定できます)

dart_js_facade_gen index.d.ts > baasatrakuza_interop.dart

 
と、早速エラーになりました。どうやら、export { }の様な空のexportはサポートされていない模様。コメントアウトして、再度実行します。

index.d.ts:3426:1: re-exports must have a module URL (export x from "./y").
index.d.ts:3426:8: empty export list

 
するとファイルが生成されました。
ただ、このままだとうまく呼び出すことができなかったため、いくつか手動で修正を行いました。

library

libraryディレクティブは自動生成のコマンドを実行したディレクトリ名が書き込まれる様です。正しいlibraryに変更します。

// Dart
library dist;

// Dart
library baasatrakuza_dart;

 

グローバル値のマッピング

BaaS@rakuzaのJavaScript SDKの各オブジェクトにアクセスする際は、RKZというグローバルオブジェクトを経由します。(ES Module/CommonJS除く)
例えば以下の様な感じです。

// TypeScript
await RKZ.User.registerPushDeviceToken(userAccessToken, RKZ.DeviceType.IOS);

 
この時、RKZがグローバルオブジェクトなのかどうかはdart_js_facade_genには分からないため補う必要があります。

// Dart
@JS("RKZ") // "RKZ"を補完
external Rakuza
    get JS$_default;

 
ちなみに、型定義ファイルは以下の様になっているのですが、Dartは先頭が_で始まるとprivate扱いにする仕様のため、JS$_defaultという様に変換されてしまいます・・

// TypeScript
declare const _default: Rakuza;
export default _default;

 
これもわかりやすく、以下の様に修正します。

// Dart
@JS("RKZ")
external Rakuza
    get RKZ;

 
これで、RKZ.User.registerPushDeviceTokenにアクセスできる様になりましたが、RKZ.DeviceType.IOSにはアクセスできません。RKZ.DeviceTypeは型定義ファイルだとenumであり、dart_js_facade_genによりclassに変換されています。
Dartではインスタンス(オブジェクト)がクラス(コンストラクタ関数)を持つことはできないため、コンパイルエラーになります。そこで、JS側の実体であるRKZ.DeviceTypeを、Dart側のDeviceTypeクラスと紐づけてやります。

// Dart
@JS("RKZ.DeviceType")
class DeviceType {
  external static String get Android;
  external static String get IOS;
}

 
これで、DartでもJavaScript SDKの各オブジェクトにアクセスできるようになりました!

// Dart
await RKZ.User.registerPushDeviceToken(userAccessToken, DeviceType.IOS);

 

enumの扱い

TypeScriptのenumは数値以外も列挙することができます。例えば、先ほどマッピングしたRKZ.DeviceTypeは文字列のenumです。
対してDartのenumは数値のみ列挙できます。そのアンマッチもあってか、型定義ファイルのenum型は定数を持つクラスに変換されます。

型定義ファイル

// TypeScript
export declare enum DeviceType {
    Android = "0001",
    IOS = "0002"
}

 
変換後のDartファイル

// Dart
@JS("RKZ.DeviceType")
class DeviceType {
  external static num get Android;
  external static num get IOS;
}

 
ただ、Dart側はenum(class)の各値がnum型になっています・・
おそらく、型定義ファイルのenumの値が何型なのか分からないからでしょう。仕方ないので、手動でString型に変換します。

PromiseとFuture

JSは非同期処理をPromiseで扱いますが、DartはFutureです。
PromiseからFutureに変換する必要がありますが、これは自動生成されたコードがよしなにやってくれます。
ちなみに変換処理はDart 2.6から導入されたextensionを活用していました。

// Dart
@JS()
class DataClient extends ClientBase {
  //...
}
//...
extension DataClientExtensions on DataClient {
  Future<Data> get(String objectId, String code) {
    final Object t = this;
    final _DataClient tt = t;
    return promiseToFuture(tt.get(objectId, code));
  }
  //...
}

 

さいごに

これらの調整を行うことで、ある程度実用的なレベルでJavaScript SDKのコードを再利用できることが確認できました。
iOS/AndroidのFlutterプラグインが完成して、Flutter Webが軌道に乗ってきたら、本対応したいと思います!

コメントを残す

%d人のブロガーが「いいね」をつけました。