A quickjs engine for flutter
flutter_qjs
This plugin is a simple js engine for flutter using the quickjs project with dart:ffi. Plugin currently supports all the platforms except web!
Getting Started
Basic usage
Firstly, create a FlutterQjs
object, then call dispatch
to dispatch event loop:
final engine = FlutterQjs()
engine.dispatch();
Use evaluate
method to run js script, now you can use it synchronously, or use await to resolve Promise
:
try {
print(engine.evaluate(code ?? ''));
} catch (e) {
print(e.toString());
}
Method close
can destroy quickjs runtime that can be recreated again if you call evaluate
. Parameter port
should be close to stop dispatch
loop when you do not need it.
engine.port.close(); // stop dispatch loop
engine.close(); // close engine
engine = null;
Data conversion between dart and js are implemented as follow:
dart | js |
---|---|
Bool | boolean |
Int | number |
Double | number |
String | string |
Uint8List | ArrayBuffer |
List | Array |
Map | Object |
JSFunction(...args) IsolateJSFunction(...args) |
function(....args) |
Future | Promise |
notice: function
can only be sent from js to dart.
Invoke dart function
A global JavaScript function channel
is presented to invoke dart function.
In constructor, pass handler function to manage JavaScript call. For example, you can use Dio
to implement http in JavaScript:
final engine = FlutterQjs(
methodHandler: (String method, List arg) {
switch (method) {
case "http":
return Dio().get(arg[0]).then((response) => response.data);
default:
throw Exception("No such method");
}
},
);
then, in java script you can use channel function to invoke methodHandler
, make sure the second parameter is a list:
channel("http", ["http://example.com/"]);
Use modules
ES6 module with import
function is supported and can be managed in dart with moduleHandler
:
final engine = FlutterQjs(
moduleHandler: (String module) {
if(module == "hello")
return "export default (name) => `hello \${name}!`;";
throw Exception("Module Not found");
},
);
then in JavaScript, import
function is used to get modules:
import("hello").then(({default: greet}) => greet("world"));
notice: Module handler should be called only once for each module name. To reset the module cache, call FlutterQjs.close
then evaluate
again.
To use async function in module handler, try Run on isolate thread
Run on isolate thread
Create a IsolateQjs
object, pass handlers to implement js-dart interaction and resolving modules. The methodHandler
is used in isolate, so the handler function must be a top-level function or a static method. Async function such as rootBundle.loadString
can be used now to get modules:
dynamic methodHandler(String method, List arg) {
switch (method) {
case "http":
return Dio().get(arg[0]).then((response) => response.data);
default:
throw Exception("No such method");
}
}
final engine = IsolateQjs(
methodHandler: methodHandler,
moduleHandler: (String module) async {
return await rootBundle.loadString(
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
},
);
// not need engine.dispatch();
Same as run on main thread, use evaluate
to run js script. In this way, Promise
return by evaluate
will be automatically tracked and return the resolved data:
try {
print(await engine.evaluate(code ?? '', "<eval>"));
} catch (e) {
print(e.toString());
}
Method close
can destroy quickjs runtime that can be recreated again if you call evaluate
.
This example contains a complete demonstration on how to use this plugin.
For macOS & iOS developer
I am new to Xcode and iOS developing, and I cannot find a better way to support both simulators and real devices without combining the binary frameworks. To reduce build size, change the s.vendored_frameworks
in ios/flutter_qjs.podspec
to the specific framework.
For simulator, use:
s.vendored_frameworks = `build/Debug-iphonesimulator/ffiquickjs.framework`
For real device, use:
s.vendored_frameworks = `build/Debug-iphoneos/ffiquickjs.framework`
Two additional notes:
-
quickjs built with
release
config has bug in resolvingPromise
. Please let me know if you know the solution. -
ios/make.sh
limits the build architectures to avoid combining conflicts. Change themake.sh
to support another architectures.