【Flutter Web】Javascriptを呼び出す (packages:js)

webの開発の為に多種多様なjavascriptのライブラリが存在しますが、flutter web開発時に使いたくてもdart・flutterでそれらを扱うパッケージが存在しないこともあると思います

そんな場合にはdart側からJavascirptの処理を直接呼び出すサポートをしてくれるのが`js`パッケージです

https://pub.dev/packages/js

その基本的な使い方を見ていきます

# 前提
今回は「 **`Flutter web`にて外部のjsライブラリもしくは自身で作成したjsの処理を実行する** 」事を想定しており、純粋にDartからjs処理を呼び出す事についてはあまり言及しません


# 概要
全体の流れとしては、以下の通りとなります
1. `web`フォルダ配下にjavascriptの処理を定義したファイルを作成
2. `web/index.html`内でそのファイルをインポート
3. Dart(Flutter)側でインポートしたjsファイルの処理を呼び出すメソッドを定義

## ファイル構成

```bash
.
├── lib
│   ├── my_javascript.dart # このファイルにjs処理を書いていく
│   └── main.dart
│
└── web
    ├── my_javascript.js # このファイルにjs処理を書いていく
    ├── index.html
    └── manifest.json

```

# 基本の流れ
0. `js`パッケージをインポートしておく

```shell
$ flutter pub add js
```

1. jsのfunctionを定義したファイルをwebディレクトリに作成

```js: web/my_javascript.js
function basicFunction() {
    console.log("function executed!")
}
```

2. `web/index.html`にて定義したjsファイルをインポート

```html: web/index.html
  <script src="my_javascript.js"></script>
```

3. js処理を呼び出すDart側のメソッドを定義

```dart: lib/js/my_javascript.dart
@JS()
library my_javascript;

import 'package:js/js.dart';

@JS('basicFunction')
external void basicFunction();
```

- 前提としてjsファイルの呼び出し部分の前には`@JS()`アノテーションを付ける
- `library`としてjsの処理を定義したファイルを指定
- 定義したjsの処理を割り当てるdartメソッドの前に`@JS(<割り当てるfunction名>)`を付ける
- dartメソッドの頭に`external`句を付けて引数、返り値をjsの処理と同様にする

4. Dart側のメソッドを実行する
あとはDart側で定義したメソッドを実行するだけです

```dart: lib/main.dart
onPressed: ()=> basicFunction(),
```

# 外部jsライブラリの使い方
上記では自分で定義したjavascriptの処理を呼び出しましたが、今度は公開されている外部のjsライブラリを使ってみましょう

## 扱い方は2通り
外部ライブラリを使う場合は、「自分で定義したjavascriptの処理で使うやり方」と「dart側で直接外部ライブラリの処理を呼び出すやり方」と２通りが存在します

## 自分で定義したjsの処理で使う
1. 外部ライブラリを`web/index.html`でインポートしておく
```html: web/index.html
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
```
2. 自分で定義したjavascriptの処理内で呼び出す
`web/index.html`でインポートしておけば、自身のjsファイル内で改めてimportする事なく外部ライブラリの処理を呼ぶ事ができます

```js: web/my_javascript.js
// function using library
function functionUsingLibrary(list){
    // using max method from lodash
    console.log(`biggest number is ${_.max(list)}`)
}
```

## Dart側から直接呼び出す
1. 外部ライブラリを`web/index.html`でインポートしておく
```html: web/index.html
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
```


2. Dart側で外部ライブラリの処理を直接呼ぶ
Dart側で直接呼び出す際は`library`としてインポートします
  
```dart: lib/js/lodash.dart
@JS('_')
library lodash;

import 'package:js/js.dart';

@JS('uniq')
external List<int> uniq(List<int> list);
```

## どちらが良いのか？
呼び出したいjsの処理がそのライブラリの処理だけであれば、Dart側から直接呼んでも良いですが、複数あったり、今後増える可能性もあるのであれば **自分で定義したjsファイルを用意し、そこに処理を集約させる** 方が個人的には分かりやすく、責務もスッキリすると考えています。あくまでも個人的な意見ですが。

# jsの処理に引数を渡す
引数を受け取るjs処理があると思います。`String`や`bool`などプリミティブな型はそのまま渡せますが、オブジェクトを引数として渡す場合は、そのオブジェクトのクラスをjsパッケージを使って定義する必要があります

```dart:lib/js/my_javascript.dart
@JS('functionWithObjectArg')
external void functionWithObjectArg(Person person);

@JS('')
@anonymous
class Person {
  external String get name;
  external int get age;
  external String get message;
  external factory Person({String name, int age, String message});
```
- `js`に用意されている`@anonymous`アノテーションを付ける
- `@anonymous`アノテーションが付いたクラスでは`factory`コンストラクタを定義する必要がある
- 通常、名前付き引数は使えないので、上記の通りクラスとして定義し、そのコンストラクタを名前付きにする事で実現させる

```dart:lib/main.dart
  onPressed: () => functionWithObjectArg(
    Person(
      name: 'John',
      age: 52,
      message: "Hi, I'm John!!",
    ),
  ),
```

# jsの処理に関数を引数として渡す
関数を引数で渡したい場合は、渡す関数を`allowInterop`メソッドでラップする。onSuccessやonFailureのコールバックメソッドを使う際に有効です。

```js: web/my_javascript.js
// example with passing function
function functionWithFunctionArg(func) {
    const result = func()
    console.log(`function result was ${result}`)
}
```

```dart: lib/js/my_javascript.dart
@JS('functionWithFunctionArg')
external void functionWithFunctionArg(Function func);
```

```dart:lib/main.dart
  onPressed: () {
    functionWithFunctionArg(allowInterop(() {
      return 125 + 25;
    }));
  },
```

# jsのPromiseをdart側で実行する
Promiseのjs処理を呼びたい場合、Dart側でそのまま`Future`型に変換する事はできません。

以下二つの対応が必要です
1. `js_utilライブラリ`(jsパッケージに内包されている)の`promiseToFuture`関数でFutureに変換する
2. `promiseToFuture`メソッドは引数として`Object`を受け取る為、dart側のインターフェースでは **Objectを返すメソッドとして定義する必要がある**

```html:web/my_javascript.js
// example with Promise function
async function functionWithPromise() {
    console.log("execution started")
    await new Promise(resolve => {
        setTimeout(resolve, 3000) // wait 3 seconds
      });
      console.log("call waited 3 seconds")
}
```

```dart: lib/js/my_javascript.dart
@JS('functionWithPromise')
external Object functionWithPromise();
```

```dart:lib/main.dart
// 呼び出し
  onPressed: () async {
    await promiseToFuture(functionWithPromise());
    print('execution finished');
  },
```

# おわり

コード：

https://github.com/heyhey1028/flutter_samples/tree/main/samples/js_sample

以上の知識があれば、大体のjsライブラリは活用する事が出来るかと思います。Web開発では豊富なjsライブラリを使いたいシーンも、そのライブラリに対応したFlutterパッケージが存在しない事も多々有ります。`packages:js`はそんな制約を一気に取っ払ってくれる強力な助っ人です。


### 参考
https://flutter-square.com/js-call-dart/ (ja)
https://liewjuntung.medium.com/ use-javascript-in-flutter-web-a6eed3efb9a0 (en)
https://qiita.com/tfandkusu/items/b8b498d60ddff3db4d50 (ja)
https://codeburst.io/ how-to-use-javascript-libraries-in-your-dart-applications-e44668b8595d (en)
https://www.youtube.com/watch?v=zoN1_5tYzOM&t=44s (en)