flutter_js icon indicating copy to clipboard operation
flutter_js copied to clipboard

Support for Web

Open andreiborisov opened this issue 4 years ago • 12 comments

Thank you for such a wonderful package!

Do you have plans to support Web target? Obviously, you can run JS natively there, but it would be much easier from a developer's point of view to use the same API across all platforms.

andreiborisov avatar Jun 13 '21 13:06 andreiborisov

Unfortunately, I can't even compile the project to run on web. Is there a workaround? Screenshot 2021-06-14 at 23 18 30

KyryloZ avatar Jun 14 '21 20:06 KyryloZ

@kyryloz Where you trying run your code? Can you share the repo with your project? Maybe I can help you.

polotto avatar Aug 18 '21 22:08 polotto

About to run the the Web, that would be the cherry on my cake, let me explain why. I've been working on a project called SciDart (https://scidart.org/). This week I was wondering how can I implement distributed computing with it. Some how came something in my mind: What if I implement some Flutter that can download from some local server the code and execute it locally? With this app, I could take all devices that I have laying around, install this app and create a small cluster!!! I can have good computer power associate with recycling of my old devices. Better than that is the possibility to run the same app in the browser without install anything at all with the plus to run more than one tab.

I have the library to do the numerical stuff (SciDart) but I don't have an interpreter to get the code from the server and execute it. The flutter_js can easily fill that void!

polotto avatar Aug 18 '21 23:08 polotto

Yeah web support would be really nice - I'm voting up for this 👍

crisperit avatar Sep 01 '21 16:09 crisperit

for run it on web, i would need to create a intermediate layer (maybe a base package) where i would share the upfront interface of the flutter_js, but without the Dart FFI parts (like interop, DynamicLibrary). I would create a flutter_js_web which implements the browser part and flutter_js will continue to implement the Desktop and Mobile Layer. I need some time to come out with a mature implementation, which is really hard at this exact moment. But i will think about it.

abner avatar Sep 02 '21 19:09 abner

As a workaround:

Create the file js_web.dart

@JS()
library js;

import 'package:js/js.dart';

@JS('window.eval')
external dynamic eval(dynamic arg);

String evaluate(String jsCode) {
  return eval(jsCode).toString();
}

Create the file js.dart

import 'package:flutter_js/flutter_js.dart';

var flutterJs = getJavascriptRuntime();

String evaluate(String jsCode) {
  return flutterJs.evaluate(jsCode).stringResult;
}

Use this at the code:

import 'js.dart' as js if (dart.library.js)  'js_web.dart';

...
void main() {
  js.evaluate('console.log("hello world!")');
}

crisperit avatar Sep 06 '21 17:09 crisperit

As a workaround:

Create the file js_web.dart

@JS()
library js;

import 'package:js/js.dart';

@JS('window.eval')
external dynamic eval(dynamic arg);

String evaluate(String jsCode) {
  return eval(jsCode).toString();
}

Create the file js.dart

import 'package:flutter_js/flutter_js.dart';

var flutterJs = getJavascriptRuntime();

String evaluate(String jsCode) {
  return flutterJs.evaluate(jsCode).stringResult;
}

Use this at the code:

import 'js.dart' as js if (dart.library.js)  'js_web.dart';

...
void main() {
  js.evaluate('console.log("hello world!")');
}

Thanks for this workaround! For me to get it running on web, I had to move "as js" to the end of the line: import 'js/js.dart' if (dart.library.js) 'js/js_web.dart' as js;

Also maybe to obvious, but you need to add the js Dart Package to dependencies js: ^0.6.3

rainerlonau avatar Nov 11 '21 13:11 rainerlonau

If you guys @crisperit or @rainerlonau would send a MR for this it will be very welcome.

abner avatar Nov 11 '21 14:11 abner

If you guys @crisperit or @rainerlonau would send a MR for this it will be very welcome.

Hi, I´m not really sure how to do that (never did contribute yet). I´m also just getting started with flutter_js. And the workaround "gets rid" of the JsEvalResult because it returns only the stringResult. So not sure how "bad" this is in general.

rainerlonau avatar Nov 12 '21 09:11 rainerlonau

We found this https://github.com/abner/flutter_js/issues/43#issuecomment-913792427 to work very well. In fact running the js code in the browser itself performs much better than using either of the available engines when on IOS or Android.

We're using the solution below where we lazily load the esbuild bundle through eval both on web and mobile.

import 'package:flutter/foundation.dart';

// ignore: always_use_package_imports
import 'js.dart' if (dart.library.js) 'js_web.dart' as js;

// Dart imports:
import 'dart:async';

// Flutter imports:
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';

class JsEngineService {
  static void register() => GetIt.instance
      .registerLazySingleton<JsEngineService>(() => JsEngineService());

  JsEngineService() {
    print('Constructed js engine');
  }

  Completer<void>? _completer;

  Future<void> _loadJsCodeOnceMobile() async {
    if (_completer == null) {
      _completer = Completer();
      final Stopwatch stopwatch = Stopwatch()..start();
      final String jsCode = await rootBundle.loadString('assets/js/index.js');
      await js.evaluateAsync('''
      try { $jsCode } 
      catch (e) { 
        console.log('ERROR IN `assets/js/index.js`!!');
        console.log(e + '');
        console.log(e.stack + '');
      }
      ''');
      print(
          'javascript loaded in ${stopwatch.elapsed.inMilliseconds} miliseconds');
      _completer!.complete();
    }
    return _completer!.future;
  }

  Future<void> _loadJsCodeOnceWeb() async {
    if (_completer == null) {
      _completer = Completer();
      await js.evaluateAsync('''
      (function () {
        return new Promise((resolve, reject) => {
          var s = document.createElement('script');
          s.type = 'text/javascript';
          s.async = false;
          s.src = 'assets/assets/js/index.js';
          s.onload = function () {
            resolve();
          }
          document.body.append(s);
        });
      })()
      ''');
      _completer!.complete();
    }
    return _completer!.future;
  }

  Future<dynamic> eval(String code) {
    return Future.sync(() async {
      if (kIsWeb != true) {
        await _loadJsCodeOnceMobile();
      } else {
        await _loadJsCodeOnceWeb();
      }
      return js.evaluateAsync(code);
    });
  }

  Future<dynamic> handlePromise(dynamic value, {Duration? timeout}) {
    return js.handlePromise(value, timeout: timeout);
  }
}

And this is js_web.dart

@JS()
library js;

import 'package:js/js.dart';
import 'package:js/js_util.dart' as js_util;
import 'dart:js_util';

@JS('window.eval')
external dynamic eval(dynamic arg);

/// Returns a list of keys in a JavaScript [object].
///
/// This function binds to JavaScript `Object.keys()`.
@JS('Object.keys')
external List<String> objectKeys(object);

Future<dynamic> evaluateAsync(String jsCode) {
  return Future.sync(() async {
    final jsPromise = eval(jsCode);
    final jsValue = await promiseToFuture(jsPromise);
    return dartify(jsValue);
  });
}

dynamic handlePromise(dynamic promise, {Duration? timeout}) {
  return promise;
}

/// Returns Dart representation from JS Object.
///
/// Basic types (`num`, `bool`, `String`) are returned as-is. JS arrays
/// are converted into `List` instances. JS objects are converted into
/// `Map` instances. Both arrays and objects are traversed recursively
/// converting nested values.
///
/// Converting JS objects always results in a `Map<String, dynamic>` meaning
/// even if original object had an integer key set, it will be converted into
/// a `String`. This is different from JS semantics where you are allowed to
/// access a key by passing its int value, e.g. `obj[1]` would work in JS,
/// but fail in Dart.
///
/// See also:
/// - [jsify]
T dartify<T>(dynamic jsObject) {
  if (_isBasicType(jsObject)) {
    return jsObject as T;
  }

  if (jsObject is List) {
    return jsObject.map(dartify).toList() as T;
  }

  final keys = objectKeys(jsObject);
  final result = <String, dynamic>{};
  for (var key in keys) {
    result[key] = dartify(js_util.getProperty(jsObject, key));
  }

  return result as T;
}

/// Returns `true` if the [value] is a very basic built-in type - e.g.
/// [null], [num], [bool] or [String]. It returns `false` in the other case.
bool _isBasicType(value) {
  if (value == null || value is num || value is bool || value is String) {
    return true;
  }
  return false;
}

dmdeklerk avatar Feb 15 '22 09:02 dmdeklerk

@abner I can try to add web support - but I think JavascriptRuntime api is a bit not fit for interop JS.

Here are the methods:

JsEvalResult callFunction(Pointer<NativeType> fn, Pointer<NativeType> obj)
T? convertValue<T>(JsEvalResult jsValue)
void dispose()
JsEvalResult evaluate(String code)
Future<JsEvalResult> evaluateAsync(String code)
int executePendingJob()
String getEngineInstanceId()
void initChannelFunctions()
String jsonStringify(JsEvalResult jsValue)
bool setupBridge(String channelName, void Function(dynamic args) fn)

I think that for JS in web only these makes sense:

T? convertValue<T>(JsEvalResult jsValue)
JsEvalResult evaluate(String code)
Future<JsEvalResult> evaluateAsync(String code)
String jsonStringify(JsEvalResult jsValue)

Maybe it is worth to create more abstract interface like JavascriptEvaluator and hide JavascriptRuntime underneath it + implement web support?

crisperit avatar May 29 '23 07:05 crisperit