uniapp原生插件开发从入门到填坑离线打包JSI调用iOS/Android双端实战一、问题概述uniapp的生态虽然丰富但总有一些场景是JS SDK无法覆盖的调用蓝牙打印机、对接三方人脸识别SDK、访问系统级API、使用C编写的音视频编解码库等。这时就需要开发原生插件将AndroidJava/Kotlin和iOSObjective-C/Swift的原生能力暴露给uniapp的JS层调用。然而uniapp原生插件开发涉及的知识面很广Android需要了解Gradle构建、JSI机制、Weex扩展iOS需要熟悉CocoaPods、WXModuleProtocol协议两端都要配置package.json和离线打包参数。任何一个环节出错插件都无法正常加载。小瑾团队需要对接一个第三方蓝牙热敏打印机SDK该SDK仅提供Android AAR包和iOS Framework没有JS版本。她花了整整一周才完成插件开发和调试踩遍了从环境配置到方法调用的各种坑。本文是这次踩坑经验的系统化总结。二、原生插件架构原理2.1 uniapp插件体系uniapp原生插件的核心是JS → Native Bridge通信┌──────────────────────────────────────┐ │ uni.requireNativePlugin(PluginID) │ ← JS层调用 ├──────────────────────────────────────┤ │ JSI Bridge / Weex Module │ ← 中间桥接层 ├──────────────────────────────────────┤ │ Android: WXModule / Component │ │ iOS: WXModuleProtocol / Component │ ← 原生实现层 └──────────────────────────────────────┘插件分为两种类型Module模块纯功能插件无UI如调用系统API、蓝牙通信等。Component组件带UI的原生组件如自定义地图、图表等。2.2 关键概念概念AndroidiOS模块协议继承WXModule遵循WXModuleProtocol组件协议继承WXComponent遵循WXComponentProtocol方法暴露JSMethod注解WX_EXPORT_METHOD宏回调机制JSCallbackWXModuleCallback线程模型JS线程 UI线程JS线程 主线程三、Android端插件开发3.1 插件目录结构native-plugins/ └── my-bluetooth-printer/ ├── package.json # 插件元信息 ├── android/ │ ├── build.gradle # Android构建配置 │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── example/ │ └── printer/ │ └── BluetoothPrinterModule.java └── ios/ ├── BluetoothPrinterModule.h ├── BluetoothPrinterModule.m └── ThirdPartySDK/ # 第三方SDK文件3.2 package.json 配置{ name: my-bluetooth-printer, id: my-bluetooth-printer, version: 1.0.0, description: 蓝牙热敏打印机插件, _dp_type: nativeplugin, _dp_nativeplugin: { android: { plugins: [ { type: module, name: my-bluetooth-printer, class: com.example.printer.BluetoothPrinterModule } ], integrateType: aar, minSdkVersion: 21, dependencies: [ androidx.core:core:1.9.0 ], abis: [armeabi-v7a, arm64-v8a] }, ios: { plugins: [ { type: module, name: my-bluetooth-printer, class: BluetoothPrinterModule } ], integrateType: framework, deploymentTarget: 11.0, frameworks: [ CoreBluetooth.framework ] } } }关键字段说明id插件唯一标识JS层通过此ID引用。class原生实现类的完整类名Android含包名。integrateTypeAndroid用aariOS用framework。abisAndroid需要指定支持的CPU架构减少包体积。3.3 Android Module 实现// BluetoothPrinterModule.java package com.example.printer; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import com.taobao.weex.annotation.JSMethod; import com.taobao.weex.bridge.JSCallback; import com.taobao.weex.common.WXModule; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.UUID; public class BluetoothPrinterModule extends WXModule { private BluetoothAdapter bluetoothAdapter; private BluetoothSocket bluetoothSocket; private OutputStream outputStream; /** * 初始化蓝牙适配器 * 使用 JSMethod 注解暴露给JS层 */ JSMethod(uiThread false) public void init(JSCallback callback) { try { bluetoothAdapter BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter null) { callback.invoke(createErrorResult(设备不支持蓝牙)); return; } if (!bluetoothAdapter.isEnabled()) { callback.invoke(createErrorResult(请先开启蓝牙)); return; } callback.invoke(createSuccessResult(蓝牙初始化成功)); } catch (Exception e) { callback.invoke(createErrorResult(初始化失败: e.getMessage())); } } /** * 搜索附近的蓝牙设备 */ JSMethod(uiThread false) public void scanDevices(JSCallback callback) { if (bluetoothAdapter null) { callback.invoke(createErrorResult(蓝牙未初始化)); return; } SetBluetoothDevice bondedDevices bluetoothAdapter.getBondedDevices(); ArrayListMapString, Object deviceList new ArrayList(); for (BluetoothDevice device : bondedDevices) { MapString, Object deviceMap new HashMap(); deviceMap.put(name, device.getName()); deviceMap.put(address, device.getAddress()); deviceList.add(deviceMap); } MapString, Object result new HashMap(); result.put(code, 0); result.put(data, deviceList); callback.invoke(result); } /** * 连接指定设备 */ JSMethod(uiThread false) public void connect(String address, JSCallback callback) { try { BluetoothDevice device bluetoothAdapter.getRemoteDevice(address); UUID uuid UUID.fromString(00001101-0000-1000-8000-00805F9B34FB); bluetoothSocket device.createRfcommSocketToServiceRecord(uuid); // 连接操作必须在子线程 new Thread(() - { try { bluetoothAdapter.cancelDiscovery(); bluetoothSocket.connect(); outputStream bluetoothSocket.getOutputStream(); // 回调JS层需回到主线程 MapString, Object result createSuccessResult(设备连接成功); callback.invoke(result); } catch (IOException e) { MapString, Object result createErrorResult(连接失败: e.getMessage()); callback.invoke(result); } }).start(); } catch (IOException e) { callback.invoke(createErrorResult(连接异常: e.getMessage())); } } /** * 打印文本内容 */ JSMethod(uiThread false) public void printText(String content, JSCallback callback) { if (outputStream null) { callback.invoke(createErrorResult(设备未连接)); return; } try { // ESC/POS 指令文本内容 byte[] textBytes content.getBytes(GBK); outputStream.write(textBytes); outputStream.write(new byte[]{0x0A}); // 换行 // 走纸3行 outputStream.write(new byte[]{0x1B, 0x64, 0x03}); // 切纸 outputStream.write(new byte[]{0x1D, 0x56, 0x00}); outputStream.flush(); callback.invoke(createSuccessResult(打印完成)); } catch (Exception e) { callback.invoke(createErrorResult(打印失败: e.getMessage())); } } /** * 断开连接 */ JSMethod(uiThread false) public void disconnect() { try { if (outputStream ! null) { outputStream.close(); outputStream null; } if (bluetoothSocket ! null) { bluetoothSocket.close(); bluetoothSocket null; } } catch (IOException e) { e.printStackTrace(); } } // 工具方法 private MapString, Object createSuccessResult(String message) { MapString, Object result new HashMap(); result.put(code, 0); result.put(message, message); return result; } private MapString, Object createErrorResult(String message) { MapString, Object result new HashMap(); result.put(code, -1); result.put(message, message); return result; } /** * 模块销毁时释放资源 */ Override public void onActivityDestroy() { super.onActivityDestroy(); disconnect(); } }3.4 JSMethod注解详解JSMethod(uiThread false) // uiThread true → 回调在主线程执行适合UI操作 // uiThread false → 回调在JS线程执行适合耗时操作耗时操作网络请求、蓝牙通信、文件IO必须设置uiThread false否则会阻塞UI线程导致ANR。如果需要回调中更新UI应在runOnUiThread中操作。3.5 Android build.gradle// android/build.gradle apply plugin: com.android.library android { compileSdkVersion 33 defaultConfig { minSdkVersion 21 targetSdkVersion 33 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } // 关键指定aar输出 libraryVariants.all { variant - variant.outputs.all { outputFileName my-bluetooth-printer-${variant.name}.aar } } } dependencies { // uni-app基础依赖 compileOnly com.alibaba:fastjson:1.2.83 compileOnly files(libs/weex_sdk.jar) // 第三方SDK implementation files(libs/printer-sdk.aar) // AndroidX implementation androidx.core:core:1.9.0 }四、iOS端插件开发4.1 iOS Module 头文件// BluetoothPrinterModule.h #import Foundation/Foundation.h #import WeexSDK/WXModuleProtocol.h interface BluetoothPrinterModule : NSObject WXModuleProtocol end4.2 iOS Module 实现// BluetoothPrinterModule.m #import BluetoothPrinterModule.h #import CoreBluetooth/CoreBluetooth.h interface BluetoothPrinterModule () CBCentralManagerDelegate, CBPeripheralDelegate property (nonatomic, strong) CBCentralManager *centralManager; property (nonatomic, strong) CBPeripheral *connectedPeripheral; property (nonatomic, strong) CBCharacteristic *writeCharacteristic; property (nonatomic, copy) WXModuleCallback scanCallback; end implementation BluetoothPrinterModule // 导出给JS的方法 —— 使用 WX_EXPORT_METHOD 宏 WX_EXPORT_METHOD(selector(init::)) WX_EXPORT_METHOD(selector(scanDevices:)) WX_EXPORT_METHOD(selector(connectDevice::)) WX_EXPORT_METHOD(selector(printText::)) WX_EXPORT_METHOD(selector(disconnect)) // 暴露给JS的方法 - (void)init:(WXModuleCallback)callback { self.centralManager [[CBCentralManager alloc] initWithDelegate:self queue:nil]; if (callback) { callback({code: 0, message: 蓝牙初始化成功}); } } - (void)scanDevices:(WXModuleCallback)callback { self.scanCallback callback; // 扫描所有蓝牙设备不指定serviceUUID [self.centralManager scanForPeripheralsWithServices:nil options:nil]; // 10秒后停止扫描 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [self.centralManager stopScan]; }); } - (void)connectDevice:(NSDictionary *)params callback:(WXModuleCallback)callback { NSString *uuidString params[uuid]; NSUUID *uuid [[NSUUID alloc] initWithUUIDString:uuidString]; NSArray *peripherals [self.centralManager retrievePeripheralsWithIdentifiers:[uuid]]; if (peripherals.count 0) { self.connectedPeripheral peripherals.firstObject; self.connectedPeripheral.delegate self; [self.centralManager connectPeripheral:self.connectedPeripheral options:nil]; if (callback) { callback({code: 0, message: 正在连接...}); } } else { if (callback) { callback({code: (-1), message: 未找到指定设备}); } } } - (void)printText:(NSDictionary *)params callback:(WXModuleCallback)callback { NSString *content params[content]; if (!self.writeCharacteristic) { if (callback) { callback({code: (-1), message: 设备未连接}); } return; } // ESC/POS 指令构建 NSMutableData *printData [NSMutableData data]; // 初始化打印机 [printData appendBytes:\x1B\x40 length:2]; // ESC // 文本内容GBK编码 NSStringEncoding gbkEncoding CFStringConvertEncodingToNSStringEncoding( kCFStringEncodingGB_18030_2000); [printData appendData:[content dataUsingEncoding:gbkEncoding]]; [printData appendBytes:\x0A length:1]; // 换行 // 走纸 切纸 [printData appendBytes:\x1B\x64\x03 length:3]; // 走纸3行 [printData appendBytes:\x1D\x56\x00 length:3]; // 切纸 // 写入注意蓝牙每次最多写入20字节 [self writeDataInChunks:printData completion:^(BOOL success) { if (callback) { callback({code: success ? 0 : (-1), message: success ? 打印完成 : 打印失败}); } }]; } - (void)disconnect { if (self.connectedPeripheral) { [self.centralManager cancelPeripheralConnection:self.connectedPeripheral]; self.connectedPeripheral nil; self.writeCharacteristic nil; } } // CoreBluetooth 委托方法 - (void)centralManagerDidUpdateState:(CBCentralManager *)central { if (central.state ! CBManagerStatePoweredOn) { NSLog(蓝牙不可用); } } - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { if (self.scanCallback peripheral.name) { NSDictionary *deviceInfo { name: peripheral.name ?: 未知设备, uuid: peripheral.identifier.UUIDString, rssi: RSSI }; // 可以多次回调JS层接收设备列表 self.scanCallback(deviceInfo); } } - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { // 连接成功后发现服务 [peripheral discoverServices:nil]; } - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { for (CBService *service in peripheral.services) { [peripheral discoverCharacteristics:nil forService:service]; } } - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { for (CBCharacteristic *characteristic in service.characteristics) { if (characteristic.properties CBCharacteristicPropertyWrite) { self.writeCharacteristic characteristic; NSLog(找到可写特征: %, characteristic.UUID); } } } // 分包写入 - (void)writeDataInChunks:(NSData *)data completion:(void(^)(BOOL))completion { static const NSUInteger kMaxChunkSize 20; NSUInteger offset 0; __block void (^writeNextChunk)(void) nil; writeNextChunk ^{ if (offset data.length) { if (completion) completion(YES); return; } NSUInteger chunkSize MIN(kMaxChunkSize, data.length - offset); NSData *chunk [data subdataWithRange:NSMakeRange(offset, chunkSize)]; [self.connectedPeripheral writeValue:chunk forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithResponse]; offset chunkSize; // 延迟写入下一包 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.02 * NSEC_PER_SEC), dispatch_get_main_queue(), writeNextChunk); }; writeNextChunk(); } end4.3 iOS插件Podspec# my-bluetooth-printer.podspec Pod::Spec.new do |s| s.name my-bluetooth-printer s.version 1.0.0 s.summary 蓝牙打印机插件 s.homepage https://example.com s.license MIT s.author example s.ios.deployment_target 11.0 s.source { :path . } s.source_files ios/*.{h,m} s.vendored_frameworks ios/ThirdPartySDK/PrinterSDK.framework s.frameworks CoreBluetooth s.dependency WeexSDK end五、JS层调用插件开发完成后在uniapp项目中使用// 引入原生插件 const printerModule uni.requireNativePlugin(my-bluetooth-printer) // 封装为业务层方法 export class BluetoothPrinter { // 初始化蓝牙 static async init() { return new Promise((resolve, reject) { printerModule.init((result) { if (result.code 0) { resolve(result) } else { reject(result) } }) }) } // 扫描设备 static async scanDevices() { return new Promise((resolve) { const devices [] printerModule.scanDevices((result) { if (result.code 0) { devices.push(...result.data) } else { // iOS中单个设备回调 if (result.name) { devices.push(result) } } }) // 延迟返回等待扫描完成 setTimeout(() resolve(devices), 10000) }) } // 连接设备 static async connect(address) { return new Promise((resolve, reject) { printerModule.connect(address, (result) { result.code 0 ? resolve(result) : reject(result) }) }) } // 打印文本 static async printText(content) { return new Promise((resolve, reject) { printerModule.printText(content, (result) { result.code 0 ? resolve(result) : reject(result) }) }) } // 断开连接 static disconnect() { printerModule.disconnect() } } // 页面中使用 export default { methods: { async doPrint() { try { await BluetoothPrinter.init() const devices await BluetoothPrinter.scanDevices() if (devices.length 0) { uni.showToast({ title: 未发现蓝牙设备, icon: none }) return } // 连接第一个设备实际项目中让用户选择 await BluetoothPrinter.connect(devices[0].address) await BluetoothPrinter.printText(商品名称示例商品\n单价¥99.00\n数量1\n合计¥99.00) uni.showToast({ title: 打印完成, icon: success }) } catch (error) { uni.showToast({ title: error.message || 操作失败, icon: none }) } finally { BluetoothPrinter.disconnect() } } } }六、调试技巧6.1 Android调试Android Studio调试在插件项目中打断点以离线打包方式运行uni-app项目即可在Android Studio中调试原生代码。Logcat查看日志import android.util.Log; Log.d(PrinterPlugin, 调试信息: message);adb logcat -s PrinterPlugin过滤插件日志。6.2 iOS调试Xcode调试使用离线打包工程在插件代码中打断点。NSLog输出NSLog([PrinterPlugin] 调试信息: %, message);在Xcode的Console中过滤PrinterPlugin。6.3 常见问题排查问题可能原因排查方法uni.requireNativePlugin返回null插件未正确注册检查package.json中id和class是否正确方法调用无响应方法未用JSMethod/WX_EXPORT_METHOD检查注解和宏是否正确使用Android闪退ABI不匹配或so缺失检查abis配置和aar中包含的soiOS编译报错Framework未链接检查Podspec中vendored_frameworks6.4 离线打包验证插件开发完成后建议先通过离线打包验证而非直接上传插件市场确保原生代码正确无误Android使用Android Studio打开uni-app离线打包工程将插件aar放入libs目录。iOS使用Xcode打开离线打包工程在Podfile中添加本地插件依赖。验证通过后再打包上传到插件市场。七、总结uniapp原生插件开发虽然有一定门槛但掌握核心流程后并不复杂理解双向通信模型JS通过uni.requireNativePlugin获取模块引用原生通过JSCallback/WXModuleCallback回传结果。注意线程模型Android中JSMethod(uiThread)的选择直接影响性能和稳定性。统一错误处理原生端返回统一格式{code, message, data}JS层做Promise封装。先离线验证再发布离线打包是最可靠的调试手段先跑通再考虑上传插件市场。原生插件是uniapp能力边界的重要延伸掌握这一技能后理论上可以实现任何App端功能。内容由AI生成仅供参考