Flutter JavaScript执行插件flutter_js的使用
Flutter JavaScript执行插件flutter_js的使用
摘要
flutter_js 插件是一个用于在 Flutter 应用中执行 JavaScript 的引擎。它现在在 Android 上使用 QuickJS,并通过 Dart FFI 运行;在 iOS 上使用 JavaScriptCore。JavaScript 运行时通过 Dart FFI 同步运行,因此你可以在 Flutter 应用(包括移动设备和桌面平台)中直接运行 JavaScript 代码。
功能
- 在 Android 上使用 QuickJS
- 在 iOS 上使用 JavaScriptCore
- 支持 XMLHttpRequest (xhr) 和 Fetch HTTP 调用
- 支持 Promises
- 可以在 Flutter 应用中调用 JavaScript 代码,反之亦然
安装
在 pubspec.yaml 文件中添加以下依赖:
dependencies:
flutter_js: 0.1.0+0
iOS
由于 flutter_js 使用原生 JavaScriptCore,因此无需任何额外操作。
Android
更改 android/app/build.gradle 文件中的最小 Android SDK 版本为 21 或更高版本:
minSdkVersion 21
发布部署
Android
设置 ProGuard 规则,以优化发布构建:
#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class de.prosiebensat1digital.** { *; }
在 android/app/build.gradle 文件中添加以下内容:
minifyEnabled true
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
示例
以下是一个简单的 Flutter 应用示例,展示如何在 Flutter 应用中评估 JavaScript 代码:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_js/flutter_js.dart';
import 'package:flutter_js_example/ajv_example.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: FlutterJsHomeScreen(),
);
}
}
class FlutterJsHomeScreen extends StatefulWidget {
const FlutterJsHomeScreen({super.key});
@override
_FlutterJsHomeScreenState createState() => _FlutterJsHomeScreenState();
}
class _FlutterJsHomeScreenState extends State<FlutterJsHomeScreen> {
String _jsResult = '';
final JavascriptRuntime javascriptRuntime =
getJavascriptRuntime(forceJavascriptCoreOnAndroid: false);
String? _quickjsVersion;
Future<String> evalJS() async {
JsEvalResult jsResult = await javascriptRuntime.evaluateAsync(
"""
if (typeof MyClass == 'undefined') {
var MyClass = class {
constructor(id) {
this.id = id;
}
getId() {
return this.id;
}
}
}
async function test() {
var obj = new MyClass(1);
var jsonStringified = JSON.stringify(obj);
var value = Math.trunc(Math.random() * 100).toString();
var asyncResult = await sendMessage("getDataAsync", JSON.stringify({"count": Math.trunc(Math.random() * 10)}));
var err;
try {
await sendMessage("asyncWithError", "{}");
} catch(e) {
err = e.message || e;
}
return {"object": jsonStringified, "expression": value, "asyncResult": asyncResult, "expectedError": err};
}
test();
""",
sourceUrl: 'script.js',
);
javascriptRuntime.executePendingJob();
JsEvalResult asyncResult = await javascriptRuntime.handlePromise(jsResult);
return asyncResult.stringResult;
}
@override
void initState() {
super.initState();
javascriptRuntime.setInspectable(true);
javascriptRuntime.onMessage('getDataAsync', (args) async {
await Future.delayed(const Duration(seconds: 1));
final int count = args['count'];
Random rnd = Random();
final result = <Map<String, int>>[];
for (int i = 0; i < count; i++) {
result.add({'key$i': rnd.nextInt(100)});
}
return result;
});
javascriptRuntime.onMessage('asyncWithError', (_) async {
await Future.delayed(const Duration(milliseconds: 100));
return Future.error('Some error');
});
}
@override
void dispose() {
javascriptRuntime.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FlutterJS Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'JS Evaluate Result:\n\n$_jsResult\n',
textAlign: TextAlign.center,
),
const SizedBox(
height: 20,
),
const Padding(
padding: EdgeInsets.all(10),
child: Text(
'Click on the big JS Yellow Button to evaluate the expression bellow using the flutter_js plugin'),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Math.trunc(Math.random() * 100).toString();",
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold),
),
),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => AjvExample(
//widget.javascriptRuntime,
javascriptRuntime),
),
),
child: const Text('See Ajv Example'),
),
SizedBox.fromSize(size: const Size(double.maxFinite, 20)),
ElevatedButton(
child: const Text('Fetch Remote Data'),
onPressed: () async {
var asyncResult = await javascriptRuntime.evaluateAsync("""
fetch('https://raw.githubusercontent.com/abner/flutter_js/master/FIXED_RESOURCE.txt').then(response => response.text());
""");
javascriptRuntime.executePendingJob();
final promiseResolved =
await javascriptRuntime.handlePromise(asyncResult);
var result = promiseResolved.stringResult;
setState(() => _quickjsVersion = result);
},
),
Text(
'QuickJS Version\n${_quickjsVersion ?? '<NULL>'}',
textAlign: TextAlign.center,
)
],
),
),
floatingActionButton: FloatingActionButton(
//backgroundColor: Colors.transparent,
child: Image.asset('assets/js.ico'),
onPressed: () async {
final result = await evalJS();
if (!mounted) return;
setState(() {
_jsResult = result;
});
},
),
);
}
}
替代方案及为什么我们认为我们的库更好
flutter_liquidcore
基于 LiquidCore,使用 V8 引擎,所以可执行文件较大(约 20MB),导致最终应用较大。
interactive_webview
允许在隐藏的 WebView 中评估 JavaScript。不会增加应用大小,但整个浏览器会在内存中运行以评估 JavaScript 代码。我们认为嵌入式引擎是更好的解决方案。
jsengine
基于 JerryScript,比 QuickJS 慢,并且不支持 iOS。
flutter_jscore
使用 JavaScriptCore 在 Android 和 iOS 上。我们从这个出色的包中获取了 JavaScriptCore 绑定。默认情况下,我们在 Android 上提供 QuickJS 作为 JavaScript 运行时,因为它提供了更小的体积。我们的库还增加了对 ConsoleLog、SetTimeout、Xhr、Fetch 和 Promises 的支持,使您的 Flutter 应用能够通过 onMessage 函数向 JavaScript 代码提供 Dart 函数。
flutter_qjs
这是一个出色的包,通过 Dart FFI 实现了 QuickJS JavaScript 引擎。唯一的不同是我们仅在 iOS 设备上使用 QuickJS,这可能会导致 Apple Store 审核问题。在 flutter_js 0.4.0 版本中,我们添加了对桌面的支持并改进了 Dart/Js 集成,我们从 flutter_qjs 源代码中借用了 C 函数绑定和 Dart/JS 转换和集成。我们只是对其进行了一些调整以支持 xhr、fetch 并保持与 flutter_js 提供的相同接口。
小 APK 大小
一个简单的 Hello World Flutter 应用大约为 4.2 MB 或 4.6 MB。以下是使用 flutter_js 生成的示例应用的 APK 大小:
|master ✓| → flutter build apk --split-per-abi
✓ Built build/app/outputs/apk/release/app-armeabi-v7a-release.apk (5.4MB).
✓ Built build/app/outputs/apk/release/app-arm64-v8a-release.apk (5.9MB).
✓ Built build/app/outputs/apk/release/app-x86_64-release.apk (6.1MB).
Ajv 示例
我们刚刚添加了一个使用神奇的 js 库 Ajv 的示例,它允许将最先进的 JSON 模式验证功能引入 Flutter 世界。我们可以在以下链接中查看 Ajv 示例:
https://github.com/abner/flutter_js/blob/master/example/lib/ajv_example.dart
macOS
在 macOS 上解决 Command Line Tool - Error - xcrun: error: unable to find utility “xcodebuild”, not a developer tool or in PATH 错误的方法:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
为了启用 HTTP 调用,在你的文件中添加以下内容:
DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
Windows 和 Linux
C 包装库托管在 GitHub 仓库中:https://github.com/abner/quickjs-c-bridge
我们只是分离了代码以允许构建它,并且在这个仓库中我们只有已发布的共享库,因此每个使用 flutter_js 的应用程序不需要每次都重新编译它。
QuickJS Android 共享库
包装库(包括 QuickJS 和 JavaScriptCore)也在单独的仓库中编译:https://github.com/fast-development/android-js-runtimes
库编译并发布到 JitPack 后,通过 flutter_js 使用的包装库的应用程序不需要使用 Android NDK 编译共享库。
JavaScript 评估单元测试
我们可以使用桌面平台(Windows、Linux 和 macOS)来单元测试 flutter_js 中的 JavaScript 评估。
对于 Windows 和 Linux,你需要首先构建你的应用桌面可执行文件:flutter build -d windows 或 flutter build -d linux。
在 Windows 上,第一次构建你的应用后,至少需要添加路径 build\windows\runner\Debug(绝对路径)到你的环境变量中。
在 PowerShell 中,只需运行 $env:path += ";${pwd}\build\windows\runner\Debug"。现在你可以在添加了 \build\windows\runner\Debug 到路径的命令行会话中运行测试。
对于 Linux,你需要导出一个名为 LIBQUICKJSC_TEST_PATH 的环境变量,指向 build/linux/debug/bundle/lib/libquickjs_c_bridge_plugin.so。例如:export LIBQUICKJSC_TEST_PATH="$PWD/build/linux/debug/bundle/lib/libquickjs_c_bridge_plugin.so"
要在 Visual Studio Code 中集成运行测试,你需要在 .vscode/launch.json 文件中设置一个启动器,以便在 Windows 中填充 PATH,在 Linux 中填充 LIBQUICKJSC_TEST_PATH:
{
"version": "0.2.0",
"configurations": [
{
"name": "test-with-flutterjs",
"type": "dart",
"program": "test/flutter_js_test.dart",
"windows": {
"env": {
"PATH": "${env:Path};${workspaceFolder}\\example\\build\\windows\\runner\\Debug"
}
},
"linux": {
"env": {
"LIBQUICKJSC_TEST_PATH": "${workspaceFolder}/example/build/linux/debug/bundle/lib/libquickjs_c_bridge_plugin.so"
}
},
"request": "launch"
}
]
}
更多关于Flutter JavaScript执行插件flutter_js的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter JavaScript执行插件flutter_js的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,关于在Flutter中使用flutter_js插件来执行JavaScript代码,以下是一个简单的代码示例,展示了如何集成和使用该插件。
首先,确保你的Flutter项目中已经添加了flutter_js依赖。你可以在pubspec.yaml文件中添加以下依赖:
dependencies:
flutter:
sdk: flutter
flutter_js: ^3.0.0 # 请检查最新版本号
然后,运行flutter pub get来安装依赖。
接下来,在你的Flutter应用中,你可以按照以下步骤使用flutter_js插件来执行JavaScript代码。
示例代码
- 创建一个Flutter应用(如果还没有的话):
flutter create my_flutter_app
cd my_flutter_app
-
在
pubspec.yaml中添加flutter_js依赖(如上所示)。 -
编辑
main.dart文件:
import 'package:flutter/material.dart';
import 'package:flutter_js/flutter_js.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter JS Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final FlutterJs _flutterJs = FlutterJs();
String _result = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter JS Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
decoration: InputDecoration(
labelText: 'Enter JavaScript Code',
border: OutlineInputBorder(),
),
onChanged: (value) async {
try {
var result = await _flutterJs.evaluate(value);
setState(() {
_result = result.toString();
});
} catch (e) {
setState(() {
_result = 'Error: $e';
});
}
},
),
SizedBox(height: 20),
Text(
'Result: $_result',
style: TextStyle(fontSize: 18),
),
],
),
),
);
}
@override
void dispose() {
_flutterJs.dispose();
super.dispose();
}
}
解释
- FlutterJs实例:我们创建了一个
FlutterJs实例,这个实例将用于执行JavaScript代码。 - TextField:用于输入JavaScript代码。每当文本改变时,我们调用
_flutterJs.evaluate方法来执行输入的JavaScript代码。 - 结果展示:执行结果会显示在下方的
Text组件中。 - 错误处理:如果执行JavaScript代码时出现错误,我们会捕获异常并在结果中显示错误信息。
- 资源释放:在
dispose方法中,我们调用_flutterJs.dispose()来释放资源,这是一个良好的实践,尤其是在涉及原生代码或资源密集型操作时。
这个示例展示了如何在Flutter应用中集成和使用flutter_js插件来执行JavaScript代码。你可以根据需要扩展这个示例,比如添加更多的JavaScript功能、优化用户体验等。

