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 windowsflutter 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

1 回复

更多关于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代码。

示例代码

  1. 创建一个Flutter应用(如果还没有的话):
flutter create my_flutter_app
cd my_flutter_app
  1. pubspec.yaml中添加flutter_js依赖(如上所示)。

  2. 编辑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功能、优化用户体验等。

回到顶部