从 0 到 1 用iOS 原生实现 UniApp 插件
本文介绍了UniApp框架下iOS原生插件开发的完整流程。首先解析了UniApp插件架构,包括JavaScript层、Weex/DCL Bridge和Native层的通信原理,对比了Module和Component两种插件类型。接着详细说明了开发环境搭建步骤,包括下载SDK、创建插件工程和Xcode配置。最后通过代码示例展示了Module插件的基础实现,包括同步方法(UNI_EXPORT_METH
·
前言
UniApp 是一个使用 Vue.js 开发跨平台应用的框架,但在实际项目中,我们经常需要调用原生能力来实现一些特殊功能,比如:
- 调用硬件设备(蓝牙、NFC、生物识别等)
- 集成第三方 SDK(支付、地图、推送等)
- 性能敏感的计算任务
- 访问系统级 API
本文将从零开始,详细讲解如何开发一个完整的 iOS 原生插件。
一、UniApp 插件架构解析
1.1 插件通信原理
┌─────────────────────────────────────────────────────────────────────┐
│ UniApp 插件通信架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ JavaScript 层 │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ const module = uni.requireNativePlugin('PluginName') │ │ │
│ │ │ module.methodName(params, callback) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ JSBridge │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Weex/DCL Bridge │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ • 参数序列化/反序列化 (JSON) │ │ │
│ │ │ • 方法路由分发 │ │ │
│ │ │ • 回调管理 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Native 层 (iOS) │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ Module 插件: 提供 API 方法 │ │ │
│ │ │ Component 插件: 提供原生 UI 组件 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 插件类型对比
| 类型 | 用途 | 基类 | 场景示例 |
|---|---|---|---|
| Module | 提供 API 能力 | DCUniModule |
网络请求、文件操作、设备信息 |
| Component | 提供 UI 组件 | DCUniComponent |
自定义播放器、地图、图表 |
二、开发环境搭建
2.1 准备工作
下载 UniApp 离线 SDK
# 1. 访问 DCloud 官网下载 iOS 离线 SDK
# https://nativesupport.dcloud.net.cn/AppDocs/download/ios
# 2. 解压后目录结构
SDK/
├── HBuilder-Hello/ # 示例工程
├── HBuilder-uniPluginDemo/ # 插件开发示例
├── SDK/
│ ├── Libs/ # 核心库文件
│ ├── inc/ # 头文件
│ └── Resources/ # 资源文件
└── Feature-iOS.xls # 功能配置表
创建插件工程
# 推荐的目录结构
MyPlugin/
├── ios/
│ ├── MyPlugin.xcodeproj
│ └── MyPlugin/
│ ├── MyPlugin.h
│ ├── MyPlugin.m
│ └── Info.plist
├── android/ # Android 插件(可选)
├── package.json # 插件配置
└── readme.md
2.2 Xcode 工程配置
创建 Framework 项目
- Xcode → File → New → Project
- 选择 Framework
- 命名为插件名称(如
DCTestModule)
配置编译设置
Build Settings:
├── Architectures
│ └── Build Active Architecture Only → No
├── Build Options
│ └── Enable Bitcode → No
├── Linking
│ └── Mach-O Type → Static Library (推荐) 或 Dynamic Library
└── Deployment
└── iOS Deployment Target → 12.0 (与主工程一致)
引入 SDK 头文件
// 在插件头文件中引入
#import "DCUniModule.h"
#import "DCUniComponent.h"
#import "DCUniConvert.h"
// 或使用模块导入
@import DCUniPlugin;
三、Module 插件开发
3.1 基础 Module 实现
// DCTestModule.h
#import <Foundation/Foundation.h>
#import "DCUniModule.h"
NS_ASSUME_NONNULL_BEGIN
@interface DCTestModule : DCUniModule
@end
NS_ASSUME_NONNULL_END
// DCTestModule.m
#import "DCTestModule.h"
@implementation DCTestModule
#pragma mark - 同步方法
// 使用 UNI_EXPORT_METHOD_SYNC 导出同步方法
UNI_EXPORT_METHOD_SYNC(@selector(syncMethod:))
- (NSString *)syncMethod:(NSDictionary *)options {
NSString *name = options[@"name"] ?: @"World";
return [NSString stringWithFormat:@"Hello, %@!", name];
}
#pragma mark - 异步方法
// 使用 UNI_EXPORT_METHOD 导出异步方法
UNI_EXPORT_METHOD(@selector(asyncMethod:callback:))
- (void)asyncMethod:(NSDictionary *)options
callback:(UniModuleKeepAliveCallback)callback {
NSString *message = options[@"message"] ?: @"default";
// 模拟异步操作
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
if (callback) {
// 第二个参数表示是否保持回调(持续回调用 YES)
callback(@{
@"code": @0,
@"message": @"success",
@"data": message
}, NO);
}
});
}
@end
3.2 高级功能实现
持续回调(事件监听)
// DCEventModule.m
#import "DCEventModule.h"
#import <CoreLocation/CoreLocation.h>
@interface DCEventModule () <CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, copy) UniModuleKeepAliveCallback locationCallback;
@end
@implementation DCEventModule
UNI_EXPORT_METHOD(@selector(startLocationUpdates:callback:))
- (void)startLocationUpdates:(NSDictionary *)options
callback:(UniModuleKeepAliveCallback)callback {
self.locationCallback = callback;
if (!self.locationManager) {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
}
[self.locationManager requestWhenInUseAuthorization];
[self.locationManager startUpdatingLocation];
}
UNI_EXPORT_METHOD(@selector(stopLocationUpdates))
- (void)stopLocationUpdates {
[self.locationManager stopUpdatingLocation];
// 最后一次回调,keepAlive = NO
if (self.locationCallback) {
self.locationCallback(@{
@"code": @0,
@"message": @"stopped"
}, NO);
self.locationCallback = nil;
}
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray<CLLocation *> *)locations {
CLLocation *location = locations.lastObject;
if (self.locationCallback) {
// keepAlive = YES 表示保持回调,后续还会继续调用
self.locationCallback(@{
@"code": @0,
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"timestamp": @(location.timestamp.timeIntervalSince1970 * 1000)
}, YES); // ⚠️ 重要:YES 保持回调
}
}
- (void)locationManager:(CLLocationManager *)manager
didFailWithError:(NSError *)error {
if (self.locationCallback) {
self.locationCallback(@{
@"code": @(-1),
@"message": error.localizedDescription
}, NO); // 错误时结束回调
self.locationCallback = nil;
}
}
@end
主动触发事件(globalEvent)
// DCEventEmitter.m
#import "DCEventEmitter.h"
#import "DCUniSDKInstance.h"
#import "WXSDKManager.h"
@implementation DCEventEmitter
UNI_EXPORT_METHOD(@selector(startListening))
- (void)startListening {
// 注册通知监听
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleAppNotification:)
name:@"CustomAppEvent"
object:nil];
}
- (void)handleAppNotification:(NSNotification *)notification {
// 向 JS 层发送全局事件
[self fireGlobalEventWithName:@"customEvent"
params:notification.userInfo ?: @{}];
}
// 发送全局事件的封装方法
- (void)fireGlobalEventWithName:(NSString *)eventName params:(NSDictionary *)params {
if (self.uniInstance) {
[self.uniInstance fireGlobalEvent:eventName params:params];
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
3.3 参数类型处理
// DCTypeModule.m - 各种参数类型的处理
@implementation DCTypeModule
UNI_EXPORT_METHOD(@selector(handleTypes:callback:))
- (void)handleTypes:(NSDictionary *)options
callback:(UniModuleKeepAliveCallback)callback {
// 1. 基本类型
NSString *stringValue = [DCUniConvert NSString:options[@"string"]];
NSInteger intValue = [DCUniConvert NSInteger:options[@"integer"]];
CGFloat floatValue = [DCUniConvert CGFloat:options[@"float"]];
BOOL boolValue = [DCUniConvert BOOL:options[@"bool"]];
// 2. 数组
NSArray *arrayValue = options[@"array"];
if ([arrayValue isKindOfClass:[NSArray class]]) {
for (id item in arrayValue) {
NSLog(@"Array item: %@", item);
}
}
// 3. 字典
NSDictionary *dictValue = options[@"dict"];
if ([dictValue isKindOfClass:[NSDictionary class]]) {
NSLog(@"Dict value: %@", dictValue);
}
// 4. Base64 数据
NSString *base64String = options[@"base64Data"];
if (base64String) {
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String
options:0];
NSLog(@"Data length: %lu", (unsigned long)data.length);
}
// 5. 日期处理
NSNumber *timestamp = options[@"timestamp"];
if (timestamp) {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp.doubleValue / 1000.0];
NSLog(@"Date: %@", date);
}
// 返回结果
callback(@{
@"code": @0,
@"result": @{
@"string": stringValue ?: @"",
@"int": @(intValue),
@"float": @(floatValue),
@"bool": @(boolValue)
}
}, NO);
}
@end
四、Component 插件开发
4.1 基础 Component 实现
// DCTestComponent.h
#import "DCUniComponent.h"
NS_ASSUME_NONNULL_BEGIN
@interface DCTestComponent : DCUniComponent
@end
NS_ASSUME_NONNULL_END
// DCTestComponent.m
#import "DCTestComponent.h"
@interface DCTestComponent ()
@property (nonatomic, strong) UILabel *contentLabel;
@property (nonatomic, strong) NSString *text;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, assign) CGFloat fontSize;
@end
@implementation DCTestComponent
#pragma mark - 生命周期
// 初始化方法,解析 JS 传入的属性
- (instancetype)initWithRef:(NSString *)ref
type:(NSString *)type
styles:(NSDictionary *)styles
attributes:(NSDictionary *)attributes
events:(NSArray *)events
uniInstance:(DCUniSDKInstance *)uniInstance {
self = [super initWithRef:ref
type:type
styles:styles
attributes:attributes
events:events
uniInstance:uniInstance];
if (self) {
// 解析属性
_text = attributes[@"text"] ?: @"";
_textColor = [DCUniConvert UIColor:styles[@"color"]] ?: [UIColor blackColor];
_fontSize = styles[@"fontSize"] ? [DCUniConvert CGFloat:styles[@"fontSize"]] : 16.0;
}
return self;
}
// 创建视图
- (UIView *)loadView {
UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor clearColor];
return containerView;
}
// 视图加载完成后的配置
- (void)viewDidLoad {
[super viewDidLoad];
self.contentLabel = [[UILabel alloc] init];
self.contentLabel.text = self.text;
self.contentLabel.textColor = self.textColor;
self.contentLabel.font = [UIFont systemFontOfSize:self.fontSize];
self.contentLabel.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:self.contentLabel];
}
// 布局
- (void)layoutDidFinish {
[super layoutDidFinish];
self.contentLabel.frame = self.view.bounds;
}
#pragma mark - 属性更新
// 当 JS 更新属性时调用
- (void)updateAttributes:(NSDictionary *)attributes {
[super updateAttributes:attributes];
if (attributes[@"text"]) {
self.text = attributes[@"text"];
self.contentLabel.text = self.text;
}
}
// 当 JS 更新样式时调用
- (void)updateStyles:(NSDictionary *)styles {
[super updateStyles:styles];
if (styles[@"color"]) {
self.textColor = [DCUniConvert UIColor:styles[@"color"]];
self.contentLabel.textColor = self.textColor;
}
if (styles[@"fontSize"]) {
self.fontSize = [DCUniConvert CGFloat:styles[@"fontSize"]];
self.contentLabel.font = [UIFont systemFontOfSize:self.fontSize];
}
}
#pragma mark - 导出方法供 JS 调用
UNI_EXPORT_METHOD(@selector(setText:))
- (void)setText:(NSString *)text {
self.text = text;
dispatch_async(dispatch_get_main_queue(), ^{
self.contentLabel.text = text;
});
}
UNI_EXPORT_METHOD(@selector(flash))
- (void)flash {
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.3 animations:^{
self.contentLabel.alpha = 0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.3 animations:^{
self.contentLabel.alpha = 1;
}];
}];
});
}
#pragma mark - 事件
// 添加事件监听
- (void)addEvent:(NSString *)eventName {
[super addEvent:eventName];
if ([eventName isEqualToString:@"tap"]) {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
}
- (void)handleTap:(UITapGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.view];
// 触发事件到 JS 层
[self fireEvent:@"tap" params:@{
@"x": @(point.x),
@"y": @(point.y),
@"text": self.text ?: @""
}];
}
@end
4.2 复杂组件示例:视频播放器
// DCVideoComponent.h
#import "DCUniComponent.h"
#import <AVFoundation/AVFoundation.h>
@interface DCVideoComponent : DCUniComponent
@end
// DCVideoComponent.m
#import "DCVideoComponent.h"
@interface DCVideoComponent ()
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) id timeObserver;
@property (nonatomic, copy) NSString *src;
@property (nonatomic, assign) BOOL autoplay;
@property (nonatomic, assign) BOOL loop;
@property (nonatomic, assign) BOOL muted;
@end
@implementation DCVideoComponent
- (instancetype)initWithRef:(NSString *)ref
type:(NSString *)type
styles:(NSDictionary *)styles
attributes:(NSDictionary *)attributes
events:(NSArray *)events
uniInstance:(DCUniSDKInstance *)uniInstance {
self = [super initWithRef:ref type:type styles:styles
attributes:attributes events:events uniInstance:uniInstance];
if (self) {
_src = attributes[@"src"];
_autoplay = [DCUniConvert BOOL:attributes[@"autoplay"]];
_loop = [DCUniConvert BOOL:attributes[@"loop"]];
_muted = [DCUniConvert BOOL:attributes[@"muted"]];
}
return self;
}
- (UIView *)loadView {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor blackColor];
return view;
}
- (void)viewDidLoad {
[super viewDidLoad];
if (self.src) {
[self setupPlayer];
}
}
- (void)setupPlayer {
NSURL *url = [NSURL URLWithString:self.src];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
self.player = [AVPlayer playerWithPlayerItem:playerItem];
self.player.muted = self.muted;
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
[self.view.layer addSublayer:self.playerLayer];
// 监听播放状态
[playerItem addObserver:self
forKeyPath:@"status"
options:NSKeyValueObservingOptionNew
context:nil];
// 监听播放结束
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerDidFinish:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:playerItem];
// 添加时间观察者
__weak typeof(self) weakSelf = self;
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 10)
queue:dispatch_get_main_queue()
usingBlock:^(CMTime time) {
[weakSelf handleTimeUpdate:time];
}];
if (self.autoplay) {
[self.player play];
}
}
- (void)layoutDidFinish {
[super layoutDidFinish];
self.playerLayer.frame = self.view.bounds;
}
#pragma mark - 属性更新
- (void)updateAttributes:(NSDictionary *)attributes {
[super updateAttributes:attributes];
if (attributes[@"src"] && ![attributes[@"src"] isEqualToString:self.src]) {
self.src = attributes[@"src"];
[self replacePlayerItem];
}
if (attributes[@"muted"]) {
self.muted = [DCUniConvert BOOL:attributes[@"muted"]];
self.player.muted = self.muted;
}
}
- (void)replacePlayerItem {
NSURL *url = [NSURL URLWithString:self.src];
AVPlayerItem *newItem = [AVPlayerItem playerItemWithURL:url];
[self.player replaceCurrentItemWithPlayerItem:newItem];
}
#pragma mark - 导出方法
UNI_EXPORT_METHOD(@selector(play))
- (void)play {
dispatch_async(dispatch_get_main_queue(), ^{
[self.player play];
[self fireEvent:@"play" params:@{}];
});
}
UNI_EXPORT_METHOD(@selector(pause))
- (void)pause {
dispatch_async(dispatch_get_main_queue(), ^{
[self.player pause];
[self fireEvent:@"pause" params:@{}];
});
}
UNI_EXPORT_METHOD(@selector(seek:))
- (void)seek:(NSDictionary *)options {
CGFloat position = [DCUniConvert CGFloat:options[@"position"]];
CMTime time = CMTimeMakeWithSeconds(position, NSEC_PER_SEC);
__weak typeof(self) weakSelf = self;
[self.player seekToTime:time completionHandler:^(BOOL finished) {
if (finished) {
[weakSelf fireEvent:@"seeked" params:@{@"position": @(position)}];
}
}];
}
UNI_EXPORT_METHOD_SYNC(@selector(getCurrentTime))
- (NSDictionary *)getCurrentTime {
CMTime time = self.player.currentTime;
Float64 seconds = CMTimeGetSeconds(time);
return @{@"currentTime": @(seconds)};
}
#pragma mark - 事件处理
- (void)handleTimeUpdate:(CMTime)time {
Float64 currentTime = CMTimeGetSeconds(time);
Float64 duration = CMTimeGetSeconds(self.player.currentItem.duration);
[self fireEvent:@"timeupdate" params:@{
@"currentTime": @(currentTime),
@"duration": @(isnan(duration) ? 0 : duration)
}];
}
- (void)playerDidFinish:(NSNotification *)notification {
[self fireEvent:@"ended" params:@{}];
if (self.loop) {
[self.player seekToTime:kCMTimeZero];
[self.player play];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = [change[NSKeyValueChangeNewKey] integerValue];
switch (status) {
case AVPlayerItemStatusReadyToPlay:
[self fireEvent:@"canplay" params:@{}];
break;
case AVPlayerItemStatusFailed:
[self fireEvent:@"error" params:@{
@"message": self.player.currentItem.error.localizedDescription ?: @"Unknown error"
}];
break;
default:
break;
}
}
}
#pragma mark - 清理
- (void)viewWillUnload {
[super viewWillUnload];
[self.player pause];
if (self.timeObserver) {
[self.player removeTimeObserver:self.timeObserver];
self.timeObserver = nil;
}
[self.player.currentItem removeObserver:self forKeyPath:@"status"];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
五、插件配置与打包
5.1 package.json 配置
{
"name": "DCTestPlugin",
"id": "DCTestPlugin",
"version": "1.0.0",
"description": "A test plugin for UniApp",
"_dp_type": "nativeplugin",
"_dp_nativeplugin": {
"ios": {
"plugins": [
{
"type": "module",
"name": "DCTestModule",
"class": "DCTestModule"
},
{
"type": "module",
"name": "DCEventModule",
"class": "DCEventModule"
},
{
"type": "component",
"name": "dc-video",
"class": "DCVideoComponent"
}
],
"integrateType": "framework",
"deploymentTarget": "12.0",
"frameworks": [
"AVFoundation.framework",
"CoreLocation.framework"
],
"embedFrameworks": [],
"capabilities": {
"entitlements": {}
},
"plists": {
"NSLocationWhenInUseUsageDescription": "需要访问您的位置信息",
"NSCameraUsageDescription": "需要访问您的相机"
},
"resources": [
"Resources/*.bundle"
],
"parameters": {
"apiKey": {
"des": "API密钥",
"key": "DCTestPlugin:apiKey"
}
},
"dependencies": [
"pod 'AFNetworking', '~> 4.0'"
]
}
}
}
5.2 插件目录结构
DCTestPlugin/
├── android/ # Android 插件目录
├── ios/
│ ├── DCTestPlugin.framework # 编译后的 Framework
│ └── DCTestPlugin.podspec # CocoaPods 规范(可选)
├── package.json # 插件配置文件
├── readme.md # 使用说明
└── example/ # 示例代码
└── pages/
└── test/
└── test.vue
5.3 本地调试配置
在 HBuilderX 项目中配置本地插件:
项目目录/
├── nativeplugins/
│ └── DCTestPlugin/
│ ├── ios/
│ │ └── DCTestPlugin.framework
│ ├── android/
│ └── package.json
├── manifest.json
└── pages/
manifest.json 配置:
{
"app-plus": {
"nativePlugins": [
{
"name": "DCTestPlugin",
"class": "DCTestModule",
"type": "local"
}
]
}
}
六、JavaScript 调用示例
6.1 Module 调用
// test.vue
<template>
<view class="container">
<button @click="testSync">测试同步方法</button>
<button @click="testAsync">测试异步方法</button>
<button @click="startLocation">开始定位</button>
<button @click="stopLocation">停止定位</button>
<view class="result">{{ result }}</view>
</view>
</template>
<script>
// 引入原生插件
const testModule = uni.requireNativePlugin('DCTestModule')
const eventModule = uni.requireNativePlugin('DCEventModule')
export default {
data() {
return {
result: ''
}
},
onLoad() {
// 监听全局事件
uni.$on('customEvent', this.handleCustomEvent)
},
onUnload() {
uni.$off('customEvent', this.handleCustomEvent)
this.stopLocation()
},
methods: {
// 同步方法调用
testSync() {
const result = testModule.syncMethod({
name: 'UniApp'
})
this.result = `同步结果: ${result}`
console.log('Sync result:', result)
},
// 异步方法调用
testAsync() {
testModule.asyncMethod({
message: 'Hello Native!'
}, (res) => {
console.log('Async callback:', res)
if (res.code === 0) {
this.result = `异步结果: ${res.data}`
} else {
this.result = `错误: ${res.message}`
}
})
},
// 持续回调示例
startLocation() {
eventModule.startLocationUpdates({
accuracy: 'high'
}, (res) => {
console.log('Location update:', res)
if (res.code === 0 && res.latitude) {
this.result = `位置: ${res.latitude}, ${res.longitude}`
}
})
},
stopLocation() {
eventModule.stopLocationUpdates()
},
// 全局事件处理
handleCustomEvent(data) {
console.log('Received custom event:', data)
this.result = `事件数据: ${JSON.stringify(data)}`
}
}
}
</script>
6.2 Component 调用
<!-- video-test.vue -->
<template>
<view class="container">
<!-- 使用原生组件 -->
<dc-video
ref="videoPlayer"
:src="videoUrl"
:autoplay="false"
:loop="false"
:muted="false"
:style="videoStyle"
@play="onPlay"
@pause="onPause"
@timeupdate="onTimeUpdate"
@ended="onEnded"
@error="onError"
/>
<view class="controls">
<button @click="play">播放</button>
<button @click="pause">暂停</button>
<button @click="seekTo(30)">跳转到30s</button>
</view>
<view class="progress">
<text>{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</text>
<slider
:value="progress"
:max="100"
@change="onSliderChange"
/>
</view>
</view>
</template>
<script>
export default {
data() {
return {
videoUrl: 'https://example.com/video.mp4',
videoStyle: {
width: '750rpx',
height: '422rpx'
},
currentTime: 0,
duration: 0
}
},
computed: {
progress() {
if (this.duration === 0) return 0
return (this.currentTime / this.duration) * 100
}
},
methods: {
// 调用组件方法
play() {
this.$refs.videoPlayer.play()
},
pause() {
this.$refs.videoPlayer.pause()
},
seekTo(seconds) {
this.$refs.videoPlayer.seek({
position: seconds
})
},
// 事件处理
onPlay(e) {
console.log('Video started playing')
},
onPause(e) {
console.log('Video paused')
},
onTimeUpdate(e) {
this.currentTime = e.detail.currentTime
this.duration = e.detail.duration
},
onEnded(e) {
console.log('Video ended')
},
onError(e) {
console.error('Video error:', e.detail.message)
uni.showToast({
title: '播放出错',
icon: 'none'
})
},
onSliderChange(e) {
const position = (e.detail.value / 100) * this.duration
this.seekTo(position)
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
}
}
</script>
<style>
.container {
flex: 1;
background-color: #000;
}
.controls {
flex-direction: row;
justify-content: center;
padding: 20rpx;
}
.progress {
padding: 20rpx;
}
</style>
七、实战案例:生物识别插件
7.1 完整实现
// DCBiometricModule.h
#import "DCUniModule.h"
@interface DCBiometricModule : DCUniModule
@end
// DCBiometricModule.m
#import "DCBiometricModule.h"
#import <LocalAuthentication/LocalAuthentication.h>
@implementation DCBiometricModule
#pragma mark - 检查生物识别可用性
UNI_EXPORT_METHOD_SYNC(@selector(checkBiometricAvailable))
- (NSDictionary *)checkBiometricAvailable {
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
BOOL canEvaluate = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
error:&error];
NSString *biometricType = @"none";
if (@available(iOS 11.0, *)) {
switch (context.biometryType) {
case LABiometryTypeTouchID:
biometricType = @"touchID";
break;
case LABiometryTypeFaceID:
biometricType = @"faceID";
break;
default:
biometricType = @"none";
break;
}
} else {
if (canEvaluate) {
biometricType = @"touchID";
}
}
return @{
@"available": @(canEvaluate),
@"biometricType": biometricType,
@"errorCode": @(error ? error.code : 0),
@"errorMessage": error.localizedDescription ?: @""
};
}
#pragma mark - 执行生物识别验证
UNI_EXPORT_METHOD(@selector(authenticate:callback:))
- (void)authenticate:(NSDictionary *)options
callback:(UniModuleKeepAliveCallback)callback {
NSString *reason = options[@"reason"] ?: @"请验证身份";
BOOL allowFallback = options[@"allowFallback"] ? [DCUniConvert BOOL:options[@"allowFallback"]] : YES;
LAContext *context = [[LAContext alloc] init];
// 配置是否允许回退到密码
if (!allowFallback) {
context.localizedFallbackTitle = @"";
} else {
context.localizedFallbackTitle = options[@"fallbackTitle"] ?: @"使用密码";
}
// 自定义取消按钮文字
if (@available(iOS 10.0, *)) {
context.localizedCancelTitle = options[@"cancelTitle"] ?: @"取消";
}
// 验证策略
LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
if (options[@"allowDeviceCredential"] && [DCUniConvert BOOL:options[@"allowDeviceCredential"]]) {
if (@available(iOS 9.0, *)) {
policy = LAPolicyDeviceOwnerAuthentication;
}
}
// 执行验证
[context evaluatePolicy:policy
localizedReason:reason
reply:^(BOOL success, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (success) {
callback(@{
@"success": @YES,
@"code": @0,
@"message": @"验证成功"
}, NO);
} else {
NSString *errorMessage = [self getErrorMessage:error];
NSInteger errorCode = [self getErrorCode:error];
callback(@{
@"success": @NO,
@"code": @(errorCode),
@"message": errorMessage,
@"nativeErrorCode": @(error.code)
}, NO);
}
});
}];
}
#pragma mark - 错误处理
- (NSString *)getErrorMessage:(NSError *)error {
if (!error) return @"未知错误";
switch (error.code) {
case LAErrorAuthenticationFailed:
return @"验证失败";
case LAErrorUserCancel:
return @"用户取消";
case LAErrorUserFallback:
return @"用户选择使用密码";
case LAErrorSystemCancel:
return @"系统取消(如来电等)";
case LAErrorPasscodeNotSet:
return @"设备未设置密码";
case LAErrorBiometryNotAvailable:
return @"生物识别不可用";
case LAErrorBiometryNotEnrolled:
return @"未录入生物识别信息";
case LAErrorBiometryLockout:
return @"生物识别已锁定,请使用密码";
default:
return error.localizedDescription ?: @"验证错误";
}
}
- (NSInteger)getErrorCode:(NSError *)error {
if (!error) return -999;
switch (error.code) {
case LAErrorAuthenticationFailed:
return 1;
case LAErrorUserCancel:
return 2;
case LAErrorUserFallback:
return 3;
case LAErrorSystemCancel:
return 4;
case LAErrorPasscodeNotSet:
return 5;
case LAErrorBiometryNotAvailable:
return 6;
case LAErrorBiometryNotEnrolled:
return 7;
case LAErrorBiometryLockout:
return 8;
default:
return -1;
}
}
#pragma mark - 取消验证
UNI_EXPORT_METHOD(@selector(cancelAuthentication))
- (void)cancelAuthentication {
// LAContext 没有取消方法,需要通过其他方式处理
// 通常通过销毁 context 来实现
}
@end
7.2 JavaScript 封装
// utils/biometric.js
const biometricModule = uni.requireNativePlugin('DCBiometricModule')
class BiometricAuth {
/**
* 检查生物识别是否可用
*/
static checkAvailable() {
return new Promise((resolve) => {
const result = biometricModule.checkBiometricAvailable()
resolve({
available: result.available,
type: result.biometricType, // 'faceID' | 'touchID' | 'none'
error: result.errorCode !== 0 ? result.errorMessage : null
})
})
}
/**
* 执行生物识别验证
* @param {Object} options 配置选项
* @param {string} options.reason 验证原因描述
* @param {boolean} options.allowFallback 是否允许回退到密码
* @param {string} options.fallbackTitle 回退按钮文字
* @param {string} options.cancelTitle 取消按钮文字
*/
static authenticate(options = {}) {
return new Promise((resolve, reject) => {
const defaultOptions = {
reason: '请验证身份以继续',
allowFallback: true,
fallbackTitle: '使用密码',
cancelTitle: '取消'
}
const finalOptions = { ...defaultOptions, ...options }
biometricModule.authenticate(finalOptions, (result) => {
if (result.success) {
resolve(result)
} else {
reject({
code: result.code,
message: result.message,
isCancelled: result.code === 2,
isLocked: result.code === 8
})
}
})
})
}
/**
* 获取生物识别类型的显示名称
*/
static getBiometricName(type) {
const names = {
'faceID': 'Face ID',
'touchID': 'Touch ID',
'none': '无'
}
return names[type] || '生物识别'
}
}
export default BiometricAuth
7.3 使用示例
<template>
<view class="container">
<view class="status" v-if="biometricStatus">
<text>生物识别状态: {{ biometricStatus.available ? '可用' : '不可用' }}</text>
<text v-if="biometricStatus.available">
类型: {{ biometricName }}
</text>
</view>
<button
class="auth-btn"
:disabled="!biometricStatus?.available"
@click="handleAuthenticate"
>
使用 {{ biometricName }} 验证
</button>
<view class="result" v-if="authResult">
<text :class="authResult.success ? 'success' : 'error'">
{{ authResult.message }}
</text>
</view>
</view>
</template>
<script>
import BiometricAuth from '@/utils/biometric.js'
export default {
data() {
return {
biometricStatus: null,
authResult: null
}
},
computed: {
biometricName() {
if (!this.biometricStatus) return ''
return BiometricAuth.getBiometricName(this.biometricStatus.type)
}
},
async onLoad() {
await this.checkBiometric()
},
methods: {
async checkBiometric() {
try {
this.biometricStatus = await BiometricAuth.checkAvailable()
console.log('Biometric status:', this.biometricStatus)
} catch (e) {
console.error('Check biometric error:', e)
}
},
async handleAuthenticate() {
try {
const result = await BiometricAuth.authenticate({
reason: '验证身份以访问敏感信息'
})
this.authResult = {
success: true,
message: '验证成功!'
}
// 验证成功后的业务逻辑
this.onAuthSuccess()
} catch (error) {
console.log('Auth error:', error)
if (error.isCancelled) {
this.authResult = {
success: false,
message: '已取消验证'
}
} else if (error.isLocked) {
this.authResult = {
success: false,
message: '验证已锁定,请稍后再试'
}
uni.showModal({
title: '提示',
content: '生物识别已锁定,请使用密码解锁后重试',
showCancel: false
})
} else {
this.authResult = {
success: false,
message: error.message
}
}
}
},
onAuthSuccess() {
// 执行验证成功后的操作
uni.showToast({
title: '验证成功',
icon: 'success'
})
}
}
}
</script>
八、调试与问题排查
8.1 常见问题
问题1:插件无法加载
// 检查插件是否正确加载
const module = uni.requireNativePlugin('DCTestModule')
console.log('Module:', module) // 如果为 undefined,说明插件未正确加载
// 可能的原因:
// 1. package.json 配置错误
// 2. Framework 未正确编译或导入
// 3. 类名与配置不匹配
问题2:方法调用无响应
// 确保使用正确的宏导出方法
// 同步方法
UNI_EXPORT_METHOD_SYNC(@selector(syncMethod:))
// 异步方法
UNI_EXPORT_METHOD(@selector(asyncMethod:callback:))
// 常见错误:方法签名不正确
// ❌ 错误
- (void)asyncMethod:(NSDictionary *)options :(UniModuleKeepAliveCallback)callback
// ✅ 正确
- (void)asyncMethod:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback
问题3:回调无法触发
// 确保在主线程回调
- (void)someMethod:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 异步处理
id result = [self doSomething];
// ⚠️ 回调必须在适当线程
dispatch_async(dispatch_get_main_queue(), ^{
if (callback) {
callback(@{@"result": result}, NO);
}
});
});
}
8.2 调试技巧
// 1. 添加日志
- (void)testMethod:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSLog(@"[DCTestModule] testMethod called with options: %@", options);
// 处理逻辑...
NSLog(@"[DCTestModule] testMethod completed");
}
// 2. 使用断言检查
- (void)safeMethod:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSAssert(options != nil, @"Options should not be nil");
NSAssert(callback != nil, @"Callback should not be nil");
// 处理逻辑...
}
// 3. 错误处理
- (void)robustMethod:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
@try {
// 可能抛出异常的代码
[self riskyOperation:options];
callback(@{@"code": @0, @"message": @"success"}, NO);
}
@catch (NSException *exception) {
NSLog(@"[DCTestModule] Exception: %@", exception);
callback(@{
@"code": @(-1),
@"message": exception.reason ?: @"Unknown error"
}, NO);
}
}
8.3 性能监控
// 方法执行时间统计
- (void)performanceTest:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
// 执行操作
[self doHeavyWork];
CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
NSTimeInterval duration = endTime - startTime;
NSLog(@"[Performance] Method took %.4f seconds", duration);
callback(@{
@"code": @0,
@"duration": @(duration * 1000) // 毫秒
}, NO);
}
九、发布与分发
9.1 打包 Framework
# 1. 选择 Generic iOS Device
# 2. Build (⌘B)
# 3. 在 Products 文件夹找到 .framework 文件
# 或使用脚本打包 Universal Framework
xcodebuild -scheme DCTestPlugin -configuration Release -sdk iphoneos
xcodebuild -scheme DCTestPlugin -configuration Release -sdk iphonesimulator
# 合并
lipo -create \
build/Release-iphoneos/DCTestPlugin.framework/DCTestPlugin \
build/Release-iphonesimulator/DCTestPlugin.framework/DCTestPlugin \
-output DCTestPlugin.framework/DCTestPlugin
9.2 提交到 DCloud 插件市场
- 准备完整的插件目录结构
- 编写详细的 readme.md
- 提供示例代码
- 在 DCloud 插件市场 提交审核
9.3 私有分发
// package.json 添加私有配置
{
"name": "DCPrivatePlugin",
"id": "DCPrivatePlugin",
"version": "1.0.0",
"private": true,
"_dp_nativeplugin": {
"ios": {
// ... 配置
}
}
}
十、最佳实践总结
10.1 代码规范
// 1. 统一的错误响应格式
NSDictionary *successResponse = @{
@"code": @0,
@"message": @"success",
@"data": resultData
};
NSDictionary *errorResponse = @{
@"code": @(-1),
@"message": @"error description",
@"data": [NSNull null]
};
// 2. 参数校验
- (void)safeMethod:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
// 必需参数校验
NSString *requiredParam = options[@"required"];
if (!requiredParam || ![requiredParam isKindOfClass:[NSString class]]) {
callback(@{
@"code": @(-2),
@"message": @"Missing required parameter: required"
}, NO);
return;
}
// 可选参数默认值
NSString *optionalParam = options[@"optional"] ?: @"default";
// 继续处理...
}
// 3. 线程安全
@property (nonatomic, strong) dispatch_queue_t workQueue;
- (instancetype)init {
if (self = [super init]) {
_workQueue = dispatch_queue_create("com.plugin.workqueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
10.2 内存管理
// 1. 避免循环引用
__weak typeof(self) weakSelf = self;
[self.manager startWithCompletion:^(id result) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf handleResult:result];
}];
// 2. 资源释放
- (void)dealloc {
[self cleanup];
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSLog(@"[DCTestModule] dealloc");
}
- (void)cleanup {
[self.timer invalidate];
self.timer = nil;
[self.task cancel];
self.task = nil;
}
10.3 安全性考虑
// 1. 敏感数据处理
- (void)handleSensitiveData:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSString *token = options[@"token"];
// 不要在日志中打印敏感数据
#ifdef DEBUG
NSLog(@"[DEBUG] Token received (length: %lu)", (unsigned long)token.length);
#endif
// 使用后及时清理
// ...
}
// 2. 输入验证
- (BOOL)validateURL:(NSString *)urlString {
if (!urlString || urlString.length == 0) return NO;
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return NO;
// 只允许 https
if (![url.scheme isEqualToString:@"https"]) return NO;
// 可以添加域名白名单
NSArray *allowedHosts = @[@"api.example.com", @"cdn.example.com"];
return [allowedHosts containsObject:url.host];
}
总结
本文详细介绍了 iOS 原生 UniApp 插件开发的完整流程:
- 基础概念:理解插件通信架构和类型区别
- 环境搭建:配置开发环境和工程结构
- Module 开发:同步/异步方法、持续回调、全局事件
- Component 开发:自定义视图组件的生命周期和交互
- 配置打包:package.json 配置和 Framework 打包
- 实战案例:生物识别插件的完整实现
- 调试技巧:常见问题排查和性能监控
- 最佳实践:代码规范、内存管理、安全性
掌握这些知识,你就能够开发出高质量的 UniApp 原生插件,为跨平台应用提供强大的原生能力支持。
参考资料
更多推荐




所有评论(0)