开发插件

一个简单的闹钟 API

查看项目源代码:https://github.com/openkraken/kraken_plugin_examples/tree/main/plugins/my_kraken_plugin开发插件 - 图1

接下来通过一个简单的例子来演示如何给 Kraken 的 JS 环境中添加一个自定义的 API。

我们的目标是通过创建一个插件,然后新增一个 alarmClock 的全局对象,并支持在 JS 层注册一个回调用于处理闹钟响了,然后在 Dart 层去实现定时器的功能。

准备工作

初始化项目工程

通过 flutter create 命令来创建一个 flutter plugin 脚手架。然后在这个 plugin 脚手架上添加我们的代码,现在我们需要添加一些配置,让构建的时候,将所有的依赖都打包进 App 中。

  1. flutter create --template=plugin --ios-language objc --android-language java --platforms ios,android,macos ./my_kraken_plugin

安装 kraken-npbt开发插件 - 图2

不管是使用 JavaScript 还是 C/C++ 编写的插件,都需要使用 kraken-npbt 工具进行构建

  1. npm install kraken-npbt -g

在安装完成之后,使用下面的命令在插件目录内进行项目初始化,它会在 bridge 目录下生成必要的编译工程文件。

  1. kraken-npbt configure

添加 JavaScript 层的实现

在 bridge 目录下创建一个名为 my_plugin.js 的文件,然后放入以下代码:

my_plugin.js

  1. kraken.addKrakenModuleListener(function(moduleName, event, data) { if (moduleName == 'AlarmClock') { if (alarmClock.onTimeListener != null) { alarmClock.onTimeListener(event, data); } }});
  2. const alarmClock = { onTimeListener: null, setTime(time) { kraken.invokeModule('AlarmClock', 'setTime', time, (e, ret) => { if (e) { throw new Error(e); } console.log(ret); }); },
  3. onTime(fn) { this.onTimeListener = fn; },};
  4. Object.defineProperty(globalThis, 'alarmClock', { value: alarmClock, enumerable: true, writable: false, configurable: false,});

通过上面的代码实现了一个 AlarmClock 对象,通过在 JS 插件内调用 kraken.invokeModulekraken.addKrakenModuleListener 方法来实现和 Dart 层的通讯。

Kraken 在 JS 全局环境中注入了名为 kraken 的全局变量,提供插件实现所必要的功能:

  1. // 调用 Dart 函数kraken.invokeModule: (module: string, method: string, params?: Object | null, fn?: (err: Error, data: any) => void) => string;
  2. // 监听 Dart 触发的 Module 事件kraken.addKrakenModuleListener: (fn: (moduleName: string, event: Event, extra: string) => void) => void;

Dart 层逻辑的实现

上面的 JavaScript 实现将调用转发到了 Dart 层,接下来就是要在 Dart 层实现闹钟的业务逻辑。

alarm_clock_module.dart

  1. import 'package:kraken/module.dart';import 'package:kraken/dom.dart';import 'dart:async';
  2. class AlarmClockModule extends BaseModule { AlarmClockModule(ModuleManager moduleManager) : super(moduleManager);
  3. @override String get name => 'AlarmClock';
  4. @override void dispose() {}
  5. @override String invoke(String method, dynamic params, InvokeModuleCallback callback) { try { if (method == 'setTime') { dynamic time = params; Timer(Duration(seconds: time.toInt()), () { Event alarmEvent = Event('alarm'); moduleManager.emitModuleEvent(name, event: alarmEvent, data: 'Wake Up!'); callback(data: 'success'); }); } return null; } catch (e, stack) { String errmsg = '$e\n$stack'; callback(errmsg: errmsg); }
  6. return null; }}

Kraken 提供了基础的 BaseModule 抽象类,实现 BaseModule 所定义的方法就可以实现一个 Kraken 的 Module。

Kraken 在设计上使用 Module 来处理来自 JavaScript API 的调用。因此对于 AlarmClock 这个 JS API,这个 Module 命名是 AlarmClockModule 。

在 Module 内向 JavaScript 返回数据有 2 种方式,第一种是通过 InvokeModuleCallback callback 来进行返回。只要 JavaScript 的代码在调用的时候,在最后了一个参数传入了一个函数作为回调的话,就可以在 Dart 层调用 InvokeModuleCallback callback 来直接进行回调。回调参数可以传递 errmsgdata,用于处理异常和正常的两种情况。

第二种方式是在 Module 内的任何函数内调用 moduleManager.emitModuleEvent(name, event: alarmEvent, data: 'Wake Up!'); 来触发一个 Module 事件。通过在 JavaScript 上调用 kraken.addKrakenModuleListener 就可以监听到这个事件。不过值得注意的是,任何一个 Module 所触发的事件都会执行 kraken.addKrakenModuleListener 所注册的回调,因此还需要判断回调执行时调用的 Module 名称。

完成插件的注册

现在我们已经完成了大部分功能的实现,接下来只需把代码注册到 Kraken 中,就大功告成了。

构建 bridge

kraken-npbt 工具可以把 bridge 目录下的 C++ 和 JavaScript 文件都构建成一个动态链接库产物。只需要使用下面的命令就可以一键构建 macOS / iOS / Android 平台的产物。

  1. kraken-npbt build

构建的产物也会自动按照不同的平台,放置在插件项目中的不同目录下:

  • macOS: your_kraken_plugin/macos/libmy_kraken_plugin_jsc.dylib
  • iOS: your_kraken_plugin/ios/libmy_kraken_plugin_jsc.dylib
  • android:
    • your_kraken_plugin/android/jniLibs/arm64_v8a/libmy_kraken_plugin_jsc.so
    • your_kraken_plugin/android/jniLibs/armeabi_v7a/libmy_kraken_plugin_jsc.so

将 bridge 构建产物注册到插件

macOS: my_kraken_plugin/macos/my_kraken_plugin.pubspec开发插件 - 图3

  1. s.vendored_libraries = 'libmy_kraken_plugin_jsc.dylib'

iOS: my_kraken_plugin/ios/my_kraken_plugin.pubspec开发插件 - 图4

  1. s.vendored_libraries = 'libmy_kraken_plugin_jsc.dylib'

Android: my_kraken_plugin/android/build.gradle开发插件 - 图5

  1. android { sourceSets { main { jniLibs.srcDirs = ['jniLibs'] } }}

在插件初始化阶段初始化 bridge

platform.dart开发插件 - 图6

  1. // ignore_for_file: unused_import, undefined_function
  2. import 'dart:ffi';import 'dart:io' show Platform;import 'dart:typed_data';
  3. /// Search dynamic lib from env.KRAKEN_LIBRARY_PATH or /usr/libconst String KRAKEN_JS_ENGINE = 'KRAKEN_JS_ENGINE';final String kkJsEngine = Platform.environment[KRAKEN_JS_ENGINE] ?? ((Platform.isIOS || Platform.isMacOS || Platform.isAndroid) ? 'jsc' : 'quickjs');final String libName = 'libmy_kraken_plugin_$kkJsEngine';final String nativeDynamicLibraryName = (Platform.isMacOS || Platform.isIOS) ? '$libName.dylib' : Platform.isWindows ? '$libName.dll' : '$libName.so';DynamicLibrary nativeDynamicLibrary = DynamicLibrary.open(nativeDynamicLibraryName);

my_kraken_plugin.dart开发插件 - 图7

  1. import 'platform.dart';import 'dart:ffi';
  2. typedef Native_InitBridge = Void Function();typedef Dart_InitBridge = void Function();
  3. final Dart_InitBridge _initBridge =nativeDynamicLibrary.lookup<NativeFunction<Native_InitBridge>>('initBridge').asFunction();
  4. void initBridge() { _initBridge();}
  5. class MyKrakenPlugin { static void initialize() { initBridge(); ModuleManager.defineModule((moduleNamager) => AlarmClockModule(moduleNamager)); }}

之后只需要在应用中的 main 函数内进行插件的初始化,就可以直接使用 AlarmClock 这个 API 了。

  1. void main() { MyKrakenPlugin.initialize(); runApp(MyApp());}