Flutter与JS的优雅共舞:在Flutter应用中无缝集成ArcBlock JS SDK
在移动应用开发的世界里,Flutter 以其卓越的跨平台能力和惊艳的 UI 表现力,赢得了无数开发者的青睐。而 ArcBlock 作为领先的区块链开发平台,其强大的 JS SDK 为开发者构建去中心化应用提供了坚实的基础。当这两种优秀的技术相遇,一个现实的问题摆在了我们面前:如何在 Flutter 项目中,优雅地调用和集成功能丰富的 ArcBlock JS SDK?
官方并未提供 Dart 版本的 SDK,我们是否需要从零开始用 Dart 重写所有加密和通信逻辑?答案是否定的。本文将带你走上一条更高效、更稳妥的探索之路,我们将利用前端生态成熟的工具链,搭建一座连接 Flutter 和 JavaScript 的坚固桥梁。
Chapter 1: 方案选型 - 为何以及如何选择?
在探索之初,我们面临几个选择:
- AI 代码翻译:我们能否用 AI 将整个 JS SDK 自动翻译成 Dart?这是一个诱人的想法,但在当前阶段,由于语言范式、加密算法的精确性要求以及复杂的依赖关系,AI 翻译一个完整的、生产级的 SDK 风险极高,极易引入难以察觉的安全漏洞。因此,我们果断放弃了这个方案。
- 原生交互方案对比:两条主流的技术路径进入了我们的视野。
| 方案 | 工作原理 | 优点 | 缺点 |
|---|---|---|---|
| JS 运行时<br>(如 flutter_js) | 将一个轻量级的 JS 引擎(如 V8)直接打包进 App 中执行 JS 代码。 | 性能好,无 WebView 依赖,更轻量。 | 环境不完整。浏览器/Node.js 的核心 API(如 fetch, crypto)缺失,需要手动在 Dart 中实现并注入(Polyfill),工作量巨大。 |
| 无头 WebView<br>(如 flutter_inappwebview) | 在后台启动一个隐藏的 WebView 来执行 JS,利用其完整的浏览器环境。 | 环境完整,JS SDK 开箱即用,兼容性最好。 | 资源消耗略高,但对于非密集计算的场景,完全可以接受。 |
考虑到 ArcBlock SDK 可能会依赖网络 (fetch) 和加密 (crypto) 等浏览器环境 API,“无头 WebView” 方案无疑是最稳妥、最省力的选择。
然而,为了探索 Flutter 与 JS 交互的边界,并挑战更“原生”的集成方式,本文将选择 flutter_js 方案进行深度实践。我们将直面它最大的挑战——依赖管理和环境 Polyfill,并给出一套完整的解决方案。
Chapter 2: 核心思路 - 创建 JS “桥接器”
flutter_js 需要的是一个单一的 JS 脚本文件,但 ArcBlock SDK 是由多个互相依赖的 NPM 包组成的。我们不可能手动将它们合并。怎么办?
答案是:在 Flutter 项目中内嵌一个迷你的、独立的 JavaScript 项目。
这个 JS 项目的唯一使命就是:利用前端世界无比成熟的打包工具(Webpack),将所有零散的 ArcBlock SDK 模块和它们的依赖,打包(Bundle)成一个 Flutter 可以直接使用的、单一的 arc-sdk.bundle.js 文件。
Chapter 3: 实战演练 - 从零到一构建打包流程
现在,让我们卷起袖子,一步步搭建这个 JS 打包流水线。
3.1 创建 js_bundler 项目
在你的 Flutter 项目 didmail_app 的根目录下,创建一个新文件夹 js_bundler。它将是我们独立的 JS 世界。
3.2 初始化 package.json
在 js_bundler 目录下创建 package.json 文件。它负责管理我们的 JS 依赖和构建脚本。
{
"name": "didmail-js-bundler",
"version": "1.0.0",
"description": "Bundles ArcBlock JS SDK for Flutter app",
"private": true,
"scripts": {
"build": "webpack --mode=production"
},
"dependencies": {
"@arcblock/did-sdk": "^1.18.29",
"@arcblock/did-wallet": "^1.18.29"
},
"devDependencies": {
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4"
}
}> 提示:你需要将 dependencies 中的包名和版本替换为你实际需要用到的 ArcBlock SDK。
3.3 配置 webpack.config.js
这是最关键的一步。在 js_bundler 目录下创建 webpack.config.js,它会告诉 Webpack 如何打包,以及把成品放在哪里。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
// 魔法发生的地方:将打包好的 bundle 文件直接输出到 Flutter 的 assets 目录!
path: path.resolve(__dirname, '..', 'assets', 'js'),
filename: 'arc-sdk.bundle.js',
// 将我们导出的模块挂载到全局 ArcSDK 对象上,方便 Dart 调用
library: 'ArcSDK',
libraryTarget: 'umd',
globalObject: 'this'
},
};3.4 编写 JS 入口文件 index.js
在 js_bundler 内创建 src 文件夹,并在其中创建 index.js。这是我们提供给 Dart 的“API 接口”。
// 导入你需要的 ArcBlock SDK 功能
const { fromMnemonic, fromSecretKey } = require('@arcblock/did-wallet');
// 导出这些函数,以便 Dart 可以通过全局的 'ArcSDK' 对象访问
module.exports = {
generateWalletFromMnemonic: (mnemonic) => {
try {
const wallet = fromMnemonic(mnemonic);
// 返回一个可被序列化为 JSON 的普通对象
return {
address: wallet.address,
publicKey: wallet.publicKey,
privateKey: wallet.secretKey,
mnemonic: wallet.mnemonic,
};
} catch (e) {
throw new Error(`Failed to generate wallet: ${e.message}`);
}
},
// 在这里导出其他你需要的函数
};3.5 构建!
现在,打开你的系统终端,执行以下命令:
# 1. 进入 JS 项目目录
cd didmail_app/js_bundler
# 2. 安装所有 JS 依赖
npm install
# 3. 执行构建
npm run build完成后,一个新鲜出炉的 arc-sdk.bundle.js 文件就会自动出现在 didmail_app/assets/js/ 目录下!
Chapter 4: Flutter 集成 - 让 Dart 调用 JS
JS 的部分已经准备就绪,让我们回到 Dart 的世界。
4.1 添加 Flutter 依赖
flutter pub add flutter_js
flutter pub add get_it # 用于服务管理4.2 创建桥接服务 ArcBlockJsService.dart
这个类将封装所有与 flutter_js 的交互细节。
import 'package:flutter/services.dart';
import 'package:flutter_js/flutter_js.dart';
class ArcBlockJsService {
late final JavascriptRuntime _runtime;
ArcBlockJsService._(this._runtime);
static Future<ArcBlockJsService> init() async {
final runtime = getJavascriptRuntime();
// 注入 console.log,用于调试 JS 代码
runtime.onMessage('console', (args) {
print('[JS Console]: ${args.join(' ')}');
return '';
});
// 加载我们打包好的 SDK
final sdkScript = await rootBundle.loadString('assets/js/arc-sdk.bundle.js');
await runtime.evaluateAsync(sdkScript);
return ArcBlockJsService._(runtime);
}
Future<Map<String, dynamic>> generateWalletFromMnemonic(String mnemonic) async {
// 注意:JS 代码里调用的是 ArcSDK.generateWalletFromMnemonic
final jsCode = "ArcSDK.generateWalletFromMnemonic('$mnemonic')";
final result = await _runtime.evaluateAsync(jsCode);
if (result.isError) {
throw Exception('JS execution failed: ${result.stringResult}');
}
return Map<String, dynamic>.from(result.rawResult);
}
void dispose() {
_runtime.dispose();
}
}4.3 全局管理与调用
使用 get_it 在 main.dart 启动时初始化我们的服务,然后就可以在任何地方(如 AuthViewModel)方便地调用它了。
初始化 (main.dart):
// ...
import 'package:didmail_app/core/service_locator.dart';
void main() async {
// ...
await setupServiceLocator(); // 在这里初始化所有服务
runApp(const MyApp());
}调用 (AuthViewModel.dart):
// ...
import 'package:didmail_app/core/service_locator.dart';
import 'package:didmail_app/services/arcblock_js_service.dart';
class AuthViewModel extends ChangeNotifier {
Future<void> createAccount(...) async {
final arcblockService = sl<ArcBlockJsService>(); // 从服务定位器获取实例
final wallet = await arcblockService.generateWalletFromMnemonic(mnemonic);
// ... 后续逻辑
}
}Chapter 5: 挑战与展望 - 直面 Polyfill
我们选择的 flutter_js 方案最大的挑战现在浮出水面:如果你的 JS SDK 调用了 fetch 或 crypto.subtle 等浏览器/Node.js 才有的 API,flutter_js 会报错,因为它是一个纯净的 JS 引擎。
这时,你就必须手动为这些 API 提供 Polyfill。例如,要实现 fetch,你需要在 ArcBlockJsService.init() 中,利用 Dart 的 http 包,向 JS 环境中注入一个 fetch 函数。这是一个复杂但可行的过程,它要求你深入理解 Dart 与 JS 的消息传递机制。
结语
我们成功地走完了一条在 Flutter 中集成复杂 JS SDK 的完整路径。通过内嵌 JS 子项目和利用 Webpack 打包的策略,我们解决了 flutter_js 无法直接消费 NPM 生态的根本问题,搭建了一套专业且可维护的开发流程。
这个方案不仅适用于 ArcBlock SDK,更可以推广到任何你想在 Flutter 中使用的、功能强大的 JavaScript 库。它完美地诠释了 Flutter 开放和包容的生态理念——让我们站在巨人的肩膀上,将不同技术栈的优势融合,创造出更精彩的应用。
希望这篇文章能为 ArcBlock 社区的 Flutter 开发者们带来启发。Happy coding
3 条回复
我看不懂,但是我大受震撼😂
实际使用过程发现flutter_js的bug:
https://github.com/abner/flutter_js/issues/176
无法解决,打算转RN了