1.前言
在UNIAPP----Android端原生插件开发实战二进行了Android端的原生插件的开发,由于本业务同样需要IOS端的代理传输,所以下面介绍一下IOS端的原生插件开发过程。
2.工具材料清单
工具/材料 | 版本/版本名 |
---|---|
HBuilder X |
3.1.18 |
Xcode |
Version 12.1 (12A7403) |
UNI-SDK |
iOSSDK@3.1.18.80433_20210609 |
3.原生环境配置
3.1创建插件工程
根据iOS平台uni原生插件开发教程,我们打开 Xcode,创建一个新的工程(Project),template 选择 Framework ,然后点击 Next。
根据文档描述,工程存放路径,建议直接存放在 iOSSDK目录中的 HBuilder-uniPluginDemo 插件开发主工程目录下,如下图所示,然后点击 Create
Project创建完成之后,删除掉自动创建的头文件,删除后的样子如下
然后选中工程名,在TARGETS->Build Settings
中,将 Mach-O Type
设置为 Static Library
如下图所示
然后将插件工程关闭,接下来需要将插件工程导入到插件开发主工程中。
3.2导入插件工程
打开 iOSSDK/HBuilder-uniPluginDemo
工程目录,双击目录中的HBuilder-uniPlugin.xcodeproj
文件运行插件开发主工程
在 Xcode 项目左侧目录选中主工程名,然后点击右键选择Add Files to “HBuilder-uniPlugin”
然后选择您刚刚创建的插件工程路径中,选中插件工程文件,勾选Create folder references
和Add to targets
两项,然后点击Add
这时在 Xcode 左侧目录中可以看到插件工程已经添加到了主工程中,如下图所示
3.3 工程配置
在 Xcode 项目左侧目录选中主工程名,在TARGETS->Build Phases->Dependencies
中点击+
在弹窗中选中插件工程,如图所示,然后点击Add
,将插件工程添加到Dependencies
中
然后在Link Binary With Libraries
中点击+
,同样在弹窗中选中插件工程,点击Add
此时可以看到 Dependencies
和 Link Binary With Libraries
都添加了插件工程,如下图所示
接下来需要在插件工程的Header Search Paths
中添加开发插件所需的头文件引用,头文件存放在主工程的HBuilder-Hello/inc
中,添加方法如下图所示,在 Xcode 项目左侧目录选中插件工程名,找到TARGETS->Build Settings->Header Search Paths
双击右侧区域打开添加窗口,然后将inc
目录拖入会自动填充相对路径,然后将模式改成recursive
4.SDK配置
SDK的库文件SecurePortal.framework
是必须的,其包括了用户NBA的登录认证以及代理业务功能。
SecurePortal.framework
包含两个头文件一个是SPNBAClient.h
和LibSecIDLite.h
SPNBAClient.h
包含NBA的登录认证,代理和相关的接口。
LibSecIDLite.h
是获取360ID的动态口令以及二维码授权等接口。
4.1.导入SDK文件
SDK文档中描述到:
App工程添加
SecurePortal.framework
的工程配置如下:
需要将动态库添加到Embeded Binaries
和Linked Frameworks and Libraries
这两个选项里面。
再结合官网的描述
如果您的插件需要依赖第三方的SDK,开发阶段引入三方SDK的时候要引入到主工程,然后将三方SDK提供的 .h 头文件直接添加到插件工程中这样就可以正常调用三方SDK的Api了,功能开发完毕后在构建插件包的时候,需要将依赖的三方SDK库文件放到ios路径下,然后按照规范编辑 package.json;
因此,先进行第一步,将SecurePortal.framework
添加到iosTunnel.xcodeproj
为方便引用,先在目录下新建一个Frameworks
目录
然后将SecurePortal.framework
拷贝到这个目录下面,如下图所示
拷贝成功后可以同时看到Build Phases
里面也有了这个库文件的link
framework引入完毕后,接下来在主工程对SDK进行引用
选中工程,如图所示,然后在Link Binary With Libraries
点击+
会发现没有看到这个SecurePortal.framework
,这时候选择下面的Add Files
选择我们放到iosTunnel
目录下面的SecurePortal.framework
然后在Embed Frameworks
处同样添加SecurePortal.framework
4.2业务代码实现
新建头文件和.m文件,如下图所示(直接选择新建cocopods class可以同时生成.h和.m文件)
存放到iosTunnel目录下
开始业务代码的实现,头文件中先把官方文档的部分抄过来,在引入SDK的头文件
//tunnel.h
#import <Foundation/Foundation.h>
// 引入 DCUniModule.h 头文件
#import "DCUniModule.h"
#import <SecurePortal/SPNBAClient.h>
@interface tunnel : DCUniModule
@property (nonatomic, strong) UniModuleKeepAliveCallback callback;
@end
这几行代码引入过后,等待IDE编译小半分钟(此时Xcode不会提示你它正在编译),如果对着SPNBAClient等类右键能够跳转到定义的话就说明头文件引入成功了。
下面进行方法的实现
//tunnel.m
#import "tunnel.h"
@interface tunnel()<SPNBAClientDelegate,NSURLSessionDelegate>
typedef void (^CompletioBlock)(NSDictionary *dic, NSURLResponse *response, NSError *error);
typedef void (^SuccessBlock)(NSDictionary *data);
typedef void (^FailureBlock)(NSError *error);
@end
@implementation tunnel
UNI_EXPORT_METHOD(@selector(connectNBA:callback:))
// 通过宏 UNI_EXPORT_METHOD 将异步方法暴露给 js 端
//UNI_EXPORT_METHOD(@selector(testAsyncFunc:callback:))
/// 异步方法(注:异步方法会在主线程(UI线程)执行)
/// @param options js 端调用方法时传递的参数
/// @param callback 回调方法,回传参数给 js 端
- (void)connectNBA:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
// NSLog(@"传递过来的参数是%@",options);
self.callback = callback;
NSString* NBA_host = @"xxx.xxx.xxx.xx";
NSString* NBA_port = @"xxx";
NSString* auth_username = options[@"NBAUsername"];
NSString* auth_password = options[@"NBAPassword"];
NSDictionary *loginDic = @{
@"NBA_host" : NBA_host,
@"NBA_port" : NBA_port,
// @"auth_server" : @"认证服务器名,默认选取第一个",
@"auth_username" : auth_username,
@"auth_password" : auth_password,
@"auth_mode" : @0, //0用户名密码登录,1,证书登录, 2 动态口令, 3 二维码
@"auth_autologin": @1, //1自动登录,0手动登录,需要实现登录界面
// @"extra_xxxxxx" : extra_ 开头的额外的参数
};
NSLog(@"loginDic%@",loginDic);
}
- (void)didLoginSuccess {
NSLog(@"登录成功");
self.callback(@"隧道登录成功", NO);
}
- (void)onLoginErrorID:(NSInteger)errid msg:(NSString*)errmsg {
if (self.callback)
{
NSLog(@"失败信息---%@",errmsg);
self.callback(errmsg, NO);
}
}
// 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
UNI_EXPORT_METHOD_SYNC(@selector(get:callback:))
- (void)get:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSString *urlString = options[@"url"];
NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];
NSDictionary *proxyConfigDic = nil;
NSInteger proxyPort = [SPNBAClient queryProxyPort];
//NSLog(@"端口=%ld",proxyPort);
if(proxyPort)
{
NSString *host = @"xxx.x.x.x";
proxyConfigDic = @{(NSString*)kCFStreamPropertyHTTPProxyHost: host,
(NSString*)kCFStreamPropertyHTTPProxyPort: @(proxyPort),
(NSString*)kCFNetworkProxiesHTTPEnable:@YES,
(NSString*)kCFStreamPropertyHTTPSProxyHost: host,
(NSString*)kCFStreamPropertyHTTPSProxyPort:@(proxyPort)
};
}
NSURLSessionDataTask* sessionTask = [self createSessionWithRequest:request withProxyConfig:proxyConfigDic
options:options
callback:callback];
[sessionTask resume];
}
// 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
UNI_EXPORT_METHOD_SYNC(@selector(post:callback:))
- (void)post:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
NSString *urlString = options[@"url"];
NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
NSDictionary *proxyConfigDic = nil;
NSInteger proxyPort = [SPNBAClient queryProxyPort];
NSLog(@"url=%@",url);
NSLog(@"端口=%ld",proxyPort);
if(proxyPort)
{
NSString *host = @"xxx.x.x.x";
proxyConfigDic = @{(NSString*)kCFStreamPropertyHTTPProxyHost: host,
(NSString*)kCFStreamPropertyHTTPProxyPort: @(proxyPort),
(NSString*)kCFNetworkProxiesHTTPEnable:@YES,
(NSString*)kCFStreamPropertyHTTPSProxyHost: host,
(NSString*)kCFStreamPropertyHTTPSProxyPort:@(proxyPort)
};
}
NSURLSessionDataTask* sessionTask = [self createSessionWithRequest:request withProxyConfig:proxyConfigDic
options:options
callback:callback];
[sessionTask resume];
}
- (NSURLSessionDataTask*)createSessionWithRequest:(NSURLRequest*)aRequest
withProxyConfig:(NSDictionary*)proxyConfigDic
options: (NSDictionary *)options
callback:(UniModuleKeepAliveCallback)callback
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.connectionProxyDictionary = proxyConfigDic;
if([options objectForKey:@"token"])
{
NSLog(@"token=%@",options[@"token"]);
config.HTTPAdditionalHeaders = @{@"token":options[@"token"]};
}
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:nil];
NSURLSessionDataTask *sessionTask = [session dataTaskWithRequest:aRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if(error)
{
NSLog(@"请求错误:%@",[error localizedDescription]);
callback([error localizedDescription], NO);
}
else
{
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"请求成功:%@",dataStr);
callback(dataStr, NO);
}
}];
return sessionTask;
}
@end
5.原生应用测试
5.1 编写UNI端业务代码
<template>
<view>
<!--状态栏 -->
<view class="status_bar"></view>
<view class="login">
<view class="content">
<!-- 头部logo -->
<view class="header">
<image src="@/static/images/logo.png"></image>
</view>
<text class="title"></text>
<!-- 主体表单 -->
<view class="main">
<wInput v-model="account" type="text" placeholder="账号" :focus="isFocus" :disabled="disabled">
</wInput>
<wInput v-model="password" type="password" placeholder="密码" :disabled="disabled"></wInput>
</view>
<wButton class="wbutton" text="登 录" :rotate="isRotate" @click="startLogin"></wButton>
</view>
<yomol-upgrade :type="upgradeType" :url="upgradeUrl" title="发现新版本" :content="upgradeContent"
ref="yomolUpgrade"></yomol-upgrade>
<!-- 隧道modal -->
<tui-modal :show="modal" :custom="true" fadeIn >
<view class="tui-modal-custom">
<view class="tui-prompt-title">NBA账号</view>
<input class="tui-modal-input" v-model="NBAUsername" />
<view class="tui-prompt-title">NBA密码</view>
<input class="tui-modal-input password" v-model="NBAPassword" />
<tui-button height="72rpx" :size="28" shape="circle" @click="requestNBA">提交</tui-button>
</view>
</tui-modal>
</view>
</view>
</template>
<script>
let tunnel
if (uni.getSystemInfoSync().platform == "android")
tunnel = uni.requireNativePlugin('NBATunnel')
if (uni.getSystemInfoSync().platform == "ios")
tunnel = uni.requireNativePlugin("NBATunnel-NBATunnel")
import wInput from "@/components/watch-login/watch-input.vue";
import wButton from "@/components/watch-login/watch-button.vue";
import tuiModal from '@/components/tui-modal/tui-modal.vue';
import { DbHelper } from "@/js/db.js";
export default {
data() {
return {
account: "",
password: "",
isRotate: false, //是否加载旋转
isFocus: false, // 是否聚焦
disabled: false,
upgradeType: "pkg", //pkg 整包 wgt 升级包
upgradeContent: "", //更新内容
upgradeUrl: "", //更新地址
NBAUsername:'',
NBAPassword:'',
modal:false,
};
},
components: {
wInput,
wButton,
},
async mounted() {
await DbHelper.init();
this.isLogin();
},
methods: {
async isLogin() {
if (uni.getSystemInfoSync().platform == "android")
{
uni.showLoading({mask:true})
//无本地NBA数据
if(!uni.getStorageSync('NBAInfo'))
{
uni.hideLoading()
let [,res] = await uni.showModal({
content: '即将发起NBA权限请求,请点击确认,若在此过程中退出应用或者拒绝了权限,需要重装本应用才能重新发起NBA权限请求!',
showCancel:false,
});
this.modal = true
}
else//有本地NBA数据,说明之前已经建立了网卡
{
let NBAInfo = uni.getStorageSync('NBAInfo')
this.NBAUsername = NBAInfo.NBAUsername
this.NBAPassword = NBAInfo.NBAPassword
uni.hideLoading()
await this.requestNBA()
}
}
if (uni.getSystemInfoSync().platform == "ios")
{
uni.showLoading({mask:true})
//无本地NBA数据
if(!uni.getStorageSync('NBAInfo'))
{
uni.hideLoading()
let [,res] = await uni.showModal({
content: '请输入正确的NBA账号密码才能后续登录!',
showCancel:false,
});
this.modal = true
}
else//有本地NBA数据,说明之前已经建立了网卡
{
let NBAInfo = uni.getStorageSync('NBAInfo')
this.NBAUsername = NBAInfo.NBAUsername
this.NBAPassword = NBAInfo.NBAPassword
uni.hideLoading()
await this.requestNBA()
}
}
},
/**
* @description 连接NBA服务器
*/
async requestNBA(){
return new Promise((resolve,rejcet) => {
uni.showLoading({
title: 'NBA连接中...',
mask: true
});
if (!this.NBAUsername)
return uni.showToast({
title: "NBA账号不能为空!",
icon: "none"
}); // 显示提示框
if (!this.NBAPassword)
return uni.showToast({
title: "NBA密码不能为空!",
icon: "none"
});
if (uni.getSystemInfoSync().platform == "android")
{
tunnel.connectNBA({
NBAUsername:this.NBAUsername,
NBAPassword:this.NBAPassword
},async res=>{
this.modal = false
uni.hideLoading()
if(res == '隧道登录成功' || res == '请求权限')
{
let NBAInfo = {
NBAUsername:this.NBAUsername,
NBAPassword:this.NBAPassword
}
uni.setStorageSync('NBAInfo',NBAInfo);
let { account,password } = uni.getStorageSync("userInfo"); // 从本地缓存中同步获取指定 key 对应的内容。
if (!account) return; // 本地没有用户信息 直接返回(停留在登录页面)
this.isFocus = false;
this.isRotate = true;
this.disabled = true;
this.account = account;
this.password = password;
setTimeout(()=>{this.getUpdate()},1000)
}
else
{
if(/02000405/.test(res))
{
await uni.showModal({
content:`NBA账号或者密码错误,请重新输入` ,
showCancel:false,
});
this.NBAUsername = ''
this.NBAPassword = ''
uni.removeStorageSync('NBAInfo');
this.modal = true
}
else
{
uni.showModal({
content:res,
showCancel:false
});
}
rejcet(res)
}
})
}
if (uni.getSystemInfoSync().platform == "ios")
{
let NBAInfo = {
NBAUsername:this.NBAUsername,
NBAPassword:this.NBAPassword
}
tunnel.connectNBA(NBAInfo,async res=>{
console.log(res);
this.modal = false
uni.hideLoading()
if(res == '隧道登录成功' || res == '请求权限')
{
uni.setStorageSync('NBAInfo',NBAInfo);
let { account,password } = uni.getStorageSync("userInfo"); // 从本地缓存中同步获取指定 key 对应的内容。
if (!account) return; // 本地没有用户信息 直接返回(停留在登录页面)
this.isFocus = false;
this.isRotate = true;
this.disabled = true;
this.account = account;
this.password = password;
setTimeout(()=>{uni.reLaunch({url: "/pages/home/home"})},1000)
}
else
{
if(/用户名或密码错误/.test(res))
{
await uni.showModal({
content:`NBA账号或者密码错误,请重新输入` ,
showCancel:false,
});
this.NBAUsername = ''
this.NBAPassword = ''
uni.removeStorageSync('NBAInfo');
this.modal = true
}
else
{
uni.showModal({
title:"NBA登录失败",
content:res,
showCancel:false
});
}
rejcet(res)
}
})
}
})
},
// 检查网络状态,并进一步检查APP更新情况(有网条件)
async getUpdate() {
let [, netWork] = await uni.getNetworkType()
if (netWork.networkType == "2g" || netWork.networkType == "none")
{
if (uni.getStorageSync("userInfo"))
uni.reLaunch({url: "/pages/home/home"});
}
else
{
plus.runtime.getProperty(plus.runtime.appid, async widgetInfo => {
let {data: res} = await this.$http.get('/api/basedata/GetAppUpdateMsg',{
params:{
appid: plus.runtime.appid,
version: plus.runtime.version,
imei: plus.device.imei,
}
})
if (res.data)
{
this.upgradeUrl = res.data.DownLoadURL;
this.upgradeContent = res.data.Describe || "1.性能优化\n2.修复部分错误"
this.$refs.yomolUpgrade.show();
} else
uni.reLaunch({url: "/pages/home/home"})
});
}
},
async startLogin(e) {
if (this.isRotate) return;
if (!this.account)
return uni.showToast({
title: "账号不能为空!",
icon: "none"
}); // 显示提示框
if (!this.password)
return uni.showToast({
title: "密码不能为空!",
icon: "none"
});
this.isRotate = true;
this.disabled = true;
let res;
if (uni.getSystemInfoSync().platform == "android")
{
try {
let data = await this.$http.post("/api/security/token", {
username: this.account,
password: this.password,
});
res = data.data;
} catch (e) {
this.isRotate = false;
this.disabled = false;
return;
}
let {data: res2} = await this.$http.get("/api/account/GetUserInfo",{
custom: { auth: false },
header: { token: res.token }
});
let userInfo = {
account: this.account,
password: this.password,
token: res.token
};
for (let key in res2.data) {
userInfo[key] = res2.data[key];
}
uni.setStorageSync("userInfo", userInfo);
await this.getUpdate()
this.isRotate = false;
}
if (uni.getSystemInfoSync().platform == "ios")
{
tunnel.post({
url:`${this.$http.config.baseURL}/api/security/token?username=${this.account}&password=${this.password}`,
},callBack=>{
callBack = JSON.parse(callBack)
console.log(callBack);
//存储token
if(callBack.status != 0)
{
uni.showToast({
title: callBack.msg,
icon: 'none'
});
this.isRotate = false;
this.disabled = false;
return
}
tunnel.get({
url:`${this.$http.config.baseURL}/api/account/GetUserInfo`,
token:callBack.token
},callBack2=>{
callBack2 = JSON.parse(callBack2)
console.log(callBack2);
let userInfo = {
account: this.account,
password: this.password,
token: callBack.token
};
for (let key in callBack2.data)
{
userInfo[key] = callBack2.data[key];
}
console.log(userInfo);
uni.setStorageSync("userInfo", userInfo);
uni.reLaunch({url: "/pages/home/home"})
})
})
}
},
},
};
</script>
编写完成后,右键UNI项目: 发行-原生APP本地打包-生成本地打包APP资源
打开APP资源路径,然后删除掉HBuilder-uniPlugin/HBuilder-Hello/Pandora/apps/_UNI_33C5A38/www
这个WWW文件夹,然后把生成的离线WWW文件拷贝过去。
资源文件拷贝完毕后,选中工程中的HBuilder-uniPlugin-Info.plist
文件右键->Open As->Source Code
找到dcloud_uniplugins
节点,copy下面的内容添加到dcloud_uniplugins节点下,按您插件的实际信息填写对应的项
编写plist文件完毕后,选择HBuilder
这个target后,插上真机,command+B后等待一段时间即可进行测试。
6.插件打包
测试完毕之后,首先生成iosTunnel.Framework
文件,
再选中iosTunnel,command+B进行framework文件编译生成
生成完成后,将第三方SDK库文件与我们自己写生成的库文件放置为如下目录
//package.json
{
"name": "原生插件",
"id": "NBATunnel",
"version": "1.0",
"description": "原生插件",
"_dp_type":"nativeplugin",
"_dp_nativeplugin":{
"ios": {
"plugins": [{
"type": "module",
"name": "NBATunnel-NBATunnel",
"class": "NBATunnel"
}],
"frameworks": ["SecurePortal.framework"],
"integrateType": "framework",
"deploymentTarget": "9.0"
}
}
}
然后再manifest.json选择本地插件,提交云端打包即可。