Flutter 里跑 JavaScript,这需求听起来就挺怪的。
但实际上,JS 引擎在移动应用里用处挺大的。
比如你需要从网页抓取内容——网站的图片链接加密了一层,得用 JS 解密。你用 Dart 去硬写那些加密逻辑,一是麻烦,二是一旦网站换了算法,你就得重新发版。如果用 JS 呢?直接把网站的 JS 拿过来跑就完事了。
再比如规则引擎、数据清洗、过滤逻辑这些经常变动的业务,写成 JS 脚本动态加载,改规则不用发版。甚至可以通过 JavaScript引擎 实现某种程度的“热更新”——经常变动的业务逻辑用 JS 写,从服务器下发脚本就行。
这个 JS 引擎我几年前就实现了,一直在自己项目里用着。
去年给开源了,想着需要有个“真实项目”的示例,最近把“蜜柑计划”的 HTML 解析改造了一下 —— 从 isolate + Dart 手动解析,换成了 FJS + JS 在 Rust 线程执行。换用 fjs 实现之后,各种 HTML 操作和正则写起来顺手,Rust 又保证了内存安全和性能,算是顺便做了个优化,有兴趣的可以看一下怎么实现和bundle的:https://github.com/iota9star/mikan_flutter/tree/main/api
为什么不用现成的?
我调研了 pub.dev 上的几个方案。
flutter_js 是最火的,352 likes,25 万下载。但问题很明显——Android 用 QuickJS,iOS 用 JavaScriptCore,两个引擎行为还不一样。而且 QuickJS 的版本太老了,很多新特性都没有。
flutter_qjs 的 API 更现代一些,但用起来很繁琐——得手动调用 dispatch() 建立事件循环,引用管理也要手动 free()/dup(),稍微不注意就内存泄漏。
jsf 开箱即用,支持 Web,作者写得不错。但它只支持同步执行,作者也明确说了不打算加异步功能。这对需要网络请求的场景来说基本没法用。
想了想,算了,自己造一个吧。
怎么造的
底层用 QuickJS,这个没跑——轻量、快速,就是给嵌入式场景用的。但直接用 FFI 调 C API 太痛苦了,内存管理、生命周期、异步回调,全是坑。
所以我用了 Rust + FRB。
Rust 有个 rquickjs 库,是 QuickJS 的 bindings,有了别人封装好的东西,现在用起来舒服多了。再加上 flutter_rust_bridge 自动生成 FFI 代码,最后加上直接 AWS LLRT 封装好的一大堆 Node.js 兼容的模块。 就这么搞定了一个高性能、异步、ES6模块支持的 JS 引擎。实际代码量不多,但能做到什么程度,全看用户想象力。
实际上去年我用 fjs 实现了几种不同实现机制的热更框架,可以写 TSX + 编译器完成 Flutter 组件和业务的热更,算是一个比较有趣的尝试。
用起来怎么样
先说最重要的:异步是原生支持的。JS 的 Promise 会自动转成 Dart 的 Future,async/await 开箱即用。
然后它内置了一堆 Node.js 的模块——console、fetch、crypto、timers、buffer、url、path、fs 等等,基本上你需要的都有了。
Dart 端的 API 是类型安全的,用了 sealed class,编译期就能检查类型。ES6 的模块支持也很完整,import/export 随便用。
内存控制也暴露出来了,你可以设置内存上限、GC 阈值,手动触发 GC。双向桥接也有,Dart 调 JS、JS 调 Dart 都可以。
怎么用
基本的使用就这样:
final runtime = await JsAsyncRuntime.withOptions(
builtin: JsBuiltinOptions(
console: true,
fetch: true,
timers: true,
),
);
final context = await JsAsyncContext.from(runtime);
final engine = JsEngine(context);
await engine.initWithoutBridge();
final result = await engine.eval(source: JsCode.code('''
console.log('Hello from FJS!');
1 + 2
'''));
print(result.value); // 3
如果你需要用模块,可以这样:
await engine.declareNewModule(
module: JsModule.code(module: 'math', code: '''
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
'''),
);
await engine.eval(source: JsCode.code('''
const { add, multiply } = await import('math');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
'''));
双向桥接也支持,Dart 可以调 JS,JS 也能调 Dart:
await engine.init(bridge: (jsValue) async {
final data = jsValue.value;
if (data is Map && data['action'] == 'fetchUser') {
final user = await fetchUser(data['id']);
return JsResult.ok(JsValue.from(user));
}
return JsResult.ok(JsValue.none());
});
await engine.eval(source: JsCode.code('''
const user = await fjs.bridge_call({ action: 'fetchUser', id: 123 });
console.log(user);
'''));
更多的实现可以参考 GitHub 上的示例应用,示例应用也做的比较完整,有各种 API 的使用示例。下面是一些贴图:




性能如何
在“蜜柑计划”里面,执行一个复杂的 HTML 解析脚本(大约 100 行 JS,包含正则、DOM 解析、网络请求),大概只需要 100-200ms。内存占用级别可以忽略不计,一个 runtime 大概 2-3MB。
如果你在 Flutter 里也有执行 JS 的需求,可以试试 FJS。代码完全开源,欢迎在 云栈社区 与大家分享你的实践经验或提出问题。
项目地址:https://github.com/fluttercandies/fjs