一个高性能、在 Flutter 中开箱即用的 JavaScript 引擎
- 最新的
QuickJS支持 - 默认使用高性能的编译策略
- 默认开启
big number等特性 - 自动处理类型转换,支持互调用
- 全平台支持,包括
Web和OHOS端
import 'package:jsf/jsf.dart';
final js = JsRuntime();
print(js.eval('40 + 2')); // 42final js = JsRuntime(
options: const JsRuntimeOptions(
memoryLimitBytes: 64 * 1024 * 1024,
maxStackSizeBytes: 1024 * 1024,
timeout: Duration(seconds: 2),
),
);timeout 由 QuickJS interrupt handler 执行,可以随时修改或清除:
js.clearTimeout();
js.setTimeout(const Duration(milliseconds: 500));只需要 Dart 值时使用 eval()。eval() 会执行 JS,拿到结果后立即转换成 Dart 类型,并释放临时 JS handle:
final data = js.eval('({id: 1n, tags: ["a", "b"]})');
// {'id': BigInt.one, 'tags': ['a', 'b']}自动转换规则:
| JavaScript | Dart |
|---|---|
undefined |
jsUndefined |
null |
null |
boolean |
bool |
| integer number | int |
| floating-point number | double |
bigint |
BigInt |
string |
String |
Array |
List<dynamic> |
| plain object | Map<String, dynamic> |
Date |
DateTime |
Map / Set |
Map<Object?, Object?> / Set<Object?> |
RegExp / Error |
JsRegExp / JsErrorDetails |
ArrayBuffer / TypedArray |
Uint8List / JsTypedArray |
NaN / Infinity / -Infinity |
Dart double 特殊值 |
对象和数组会递归转换,所以上面的 id: 1n 会变成 Dart BigInt.one,tags 会变成 List<String> 风格的 Dart list。稀疏数组空洞会变成 jsArrayHole。Dart 传入 JS 时也支持 null、jsUndefined、bool、int、double、String、BigInt、DateTime、Uint8List、JsRegExp、JsErrorDetails、JsTypedArray、Set、List 和 Map<String, Object?>。
eval() 适合一次性拿结果,不保留 JS 对象身份。下面这些情况应该使用 evalValue() 保留 JsValue handle:
- 需要调用 JS 函数或对象方法。
- 需要读写对象属性或数组下标。
- 需要等待已有 Promise。
- 需要处理循环引用、类实例、DOM/宿主对象、TypedArray/ArrayBuffer 等不能可靠转成普通 Dart Map/List 的值。
- 需要让同一个 JS 对象在多次调用之间保持 identity。
需要保留 JS 对象/函数身份时使用 evalValue():
final object = js.evalValue('({count: 2, items: [3, 4]})');
try {
final count = object.getPropertyValue('count');
try {
print(count.toDart()); // 2
} finally {
count.dispose();
}
} finally {
object.dispose();
}JsValue.toDart() 和 eval() 使用同一套转换规则。循环对象不能自动转换:
final circular = js.evalValue('const v = {}; v.self = v; v');
try {
circular.toDart(); // throws JsException
} finally {
circular.dispose();
}拥有所有权的 JsValue 必须手动 dispose()。registerHandleFunction 里收到的是 borrowed handle,只在回调期间有效;需要长期保存时调用 duplicate()。
final add = js.evalValue('(function(a, b) { return a + b; })');
try {
final result = js.callValue(add, [20, 22]);
try {
print(result.toDart()); // 42
} finally {
result.dispose();
}
} finally {
add.dispose();
}简单调用可以直接使用:
js.execInitScript('function join(prefix, values) { return prefix + values.join(","); }');
print(js.call('join', ['v:', [1, 2, 3]])); // v:1,2,3使用 registerFunction() 可以把 Dart 函数注册到 JS 全局对象。JS 侧调用方式和普通 JavaScript 函数一致,参数会自动转换成 Dart 值,返回值也会自动转换回 JS:
js.registerFunction('dartSum', (args) {
return args.cast<num>().reduce((a, b) => a + b);
});
print(js.eval('dartSum(4, 5, 6)')); // 15回调可以接收多个参数,也可以返回 Map、List、BigInt、DateTime 等可转换值:
js.registerFunction('receiveMessage', (args) {
final name = args[0] as String;
final payload = args[1] as Map;
return {
'ok': true,
'message': '$name:${payload['count']}',
};
});
print(js.eval('receiveMessage("counter", {count: 3}).message')); // counter:3Dart 回调可以返回 Future,JS 侧会收到 Promise,所以可以在 JS 中直接 await 或 .then():
js.registerFunction('loadUser', (args) async {
return {'id': 1, 'name': 'Ada'};
});
final user = await js.evalAsync('loadUser().then((user) => user.name)');如果 JS 侧使用 async/await:
final name = await js.evalAsync('''
(async () => {
const user = await loadUser();
return user.name;
})()
''');
print(name); // Ada使用 registerFunction() 时,JS 对象会按转换规则变成 Dart snapshot。如果需要保留 JS 对象身份、访问函数、类实例、循环对象或宿主对象,使用 registerHandleFunction()。它会把参数作为 JsValue handle 传给 Dart:
js.registerHandleFunction('readModel', (args) {
final model = args.first;
final count = model.getPropertyValue('count');
try {
return count.toDart();
} finally {
count.dispose();
}
});registerHandleFunction() 中收到的参数是 borrowed handle,只在回调期间有效。需要保存到回调外时,调用 duplicate() 获取 owned handle,并在使用完后 dispose()。
JS 返回 Promise 时,使用 evalAsync() 可以直接得到 Dart Future 的结果:
final value = await js.evalAsync('Promise.resolve({ok: true})');
print(value); // {'ok': true}evalAsync() 也适合调用 async function:
final result = await js.evalAsync('''
async function compute() {
const value = await Promise.resolve(21);
return value * 2;
}
compute()
''');
print(result); // 42也可以等待已有句柄:
final promise = js.evalValue('Promise.resolve(42)');
try {
print(await js.awaitValue(promise));
} finally {
promise.dispose();
}Dart Future 返回给 JS 时会变成 Promise。这个能力适用于 registerFunction() 和 registerHandleFunction():
js.registerFunction('readConfig', (args) async {
return {'theme': 'dark'};
});
final theme = await js.evalAsync('''
readConfig().then((config) => config.theme)
''');
print(theme); // dark注册内存模块:
js.registerModules({
'math': 'export const answer = 42; export function inc(v) { return v + 1; }',
'consumer': 'import { answer, inc } from "math"; export const result = inc(answer);',
'pkg/relative': 'import { answer } from "../math"; export const result = answer;',
});
js.registerImportMap({'@math': 'math'});
js.eval(
'import { result } from "consumer"; globalThis.result = result;',
filename: 'main',
module: true,
);
print(js.eval('result')); // 43
print(await js.evalAsync('import("@math").then((m) => m.inc(m.answer))')); // 43把 Flutter asset 注册为模块:
await js.registerModuleFromAsset('app/config', 'assets/config.js');Native 端由 QuickJS 模块加载器执行。Web 端内置 JSF registry loader,使用同一套 Dart API,支持内存模块、import map、相对路径解析、模块缓存、静态 import、命名/默认/namespace export、re-export,以及字面量形式的动态 import("module")。Web 的 evalAsync(..., module: true) 和动态 import() 会使用浏览器原生 ES Module/Blob loader,因此支持浏览器实现的完整 ESM 语法,包括 export * from、export { x } from、export * as ns from 和 top-level await。内存模块面向应用内脚本和 Flutter asset 模块,不会访问网络模块地址。
JavaScript 异常会转换成 JsException:
try {
js.eval('throw new Error("boom")');
} on JsException catch (error) {
print(error.message);
}- 一个
JsRuntime拥有一个 QuickJS runtime 和一个 context。 - Runtime 应在创建它的 Dart isolate 中使用。
- Runtime 使用完必须
dispose()。 - Owned
JsValue使用完必须dispose()。 - Runtime 释放后不能继续使用其句柄。
- Runtime 释放时会主动释放仍登记在该 runtime 下的 owned
JsValue,但建议业务代码仍显式dispose(),方便控制对象生命周期和内存峰值。
集成测试位于 example/integration_test:
cd example
flutter drive --driver=integration_test/driver.dart --target=integration_test/js_runtime_test.dart -d macos测试覆盖基础类型、BigInt、对象/数组、handle 调用、Dart callback、Promise、模块加载、异常、超时、Unicode、TypedArray、循环对象和多 runtime。
