Donut 与原生的两次相遇
在大概半年前,工作原因采用了微信新出的 Donut 平台,从其域名可以看出对微信的重要性

Donut 是一个多端的应用程序开发方案,主要是通过微信小程序的开发方式开发移动应用程序,兼容 iOS 和 Android,当然对于应用程序来说,微信小程序的 API 和功能是不够的,尽管其推出了渲染性能和原生差别不大的 Skyline 渲染引擎
小程序的开发方式对于第三方 SDK 或插件来说是较为不友好的,特别是在移动应用程序中,而且在初期版本中,连用户对这个应用的授权情况都无法得知,于是有了第一次相遇
第一次相遇
刚开始 Donut 框架还是提供原生工程的,在初期 API 的支持确实过于贫瘠,所以我们的考虑是首个版本需要快速上线,就采用以 Donut 为主要工程,使用原生补充其功能,后期采用原声工程嵌入 Donut 模块

但是马上问题就随之而来,因为我们是早期的用户,所以 Donut 提供了技术支持,在将原生工程嵌入 Donut 项目的过程中,我们处处碰壁,在官方文档上也就只有简单的描述如何让小程序模块与原生工程通信

在尝试寻求 Donut 的帮助的时候,他们告知将来将不会提供原生工程,也让我们不要使用原生
随后不久官网说明也变成了下面这样子

我不会原生开发,当时也是硬着头皮做
但是适配工作已经开始了,在上线的 ddl 之前,Donut 显然无法做到我们能用的程度,我们还是硬着头皮继续研究我们的原生部分,在他们的工程中显然做了很大的限制,例如在 Android 中,小程序包启动完成后就销毁了对应的 Activity 等等
具体踩坑过程就不再赘述
第二次相遇
在前段时间,Donut 告知我们,使用原生工程构建的应用将在 12月底停用,届时所有用户将无法使用应用
于是我们又要做第二次的适配,当时我们的项目已是如此

但是好在前不久,Donut 推出了原生插件,并且这次提供了更加完善的接入原生的文档和支持,这次我们打算抛弃 React Native 部分,仅保留难以迁移的之前适配好的原生部分
我主要做 iOS 部分的适配,这里仅说明 iOS
原生插件的工程是 OC 的,“得益” 于 OC 的语法,我不得不使用 Swift
在 iOS 中,插件是一个 <pluginId>.framework
使用 Swift,首先就是要创建一个桥文件,新建文件后,在 #import
,但是无论怎么编译,就是找不到这个桥文件,最后翻遍了全网才看到官网上的描述,文档地址
#import <ProductName/ProductModuleName-Swift.h>
在解决完这个问题后终于能编译通过了
良好的编程体验
在原生插件中,分为同步方法和异步方法,都是直接暴露插件的原生函数,而定义函数又是需要在 OC 中进行,所以这及其影响体验
所以我在 OC 中只暴露了 sync 和 async 两个方法
// 声明插件同步方法
WEAPP_EXPORT_PLUGIN_METHOD_SYNC(sync, @selector(sync:))
// 声明插件异步方法
WEAPP_EXPORT_PLUGIN_METHOD_ASYNC(async, @selector(async:withCallback:))
- (id)sync:(NSDictionary *)param {
NSLog(@"sync %@", param);
// 提取方法名和数据
NSString *methodName = param[@"name"];
id data = param[@"data"] ?: nil;
// 调用 invoke 方法,并传递提取出的方法名和数据
return [self.nativeSync invoke:methodName withData:data];
}
- (void)async:(NSDictionary *)param withCallback:(WeAppNativePluginCallback)callback {
NSLog(@"async %@", param);
// 提取方法名和数据
NSString *methodName = param[@"name"];
id data = param[@"data"] ?: nil;
// 调用 Swift 的异步 invoke 方法
[self.nativeAsync invoke:methodName withData:data withCallback:^(id result) {
// 处理回调结果,这里使用了一个示例回调数据
NSDictionary *callbackData = @{ @"data": result };
callback(callbackData);
}];
}
@objcMembers
class NativeSync: NSObject {
public func invoke(_ methodName: String, withData data: Any?) -> NSDictionary {
let selectorName = methodName + (data != nil ? "WithData:" : "")
let selector = Selector(selectorName)
if self.responds(to: selector) {
let result = self.perform(selector, with: data)
return result?.takeUnretainedValue() as? NSDictionary ?? RespObject(msg: "Error")
} else {
return RespObject(msg: "No such method")
}
}
/*
自定义原生同步函数编写规则:
1. 函数接受参数必须为 data,且类型只能为 Any,类型判断在函数内进行
2. 函数返回值必须为 NSDictionary,可以使用 RespObject 函数构建返回值
*/
func someFunction(data: Any) -> NSDictionary {
RespObject(data: "Result from someFunction \(data)")
}
}
@objcMembers
class NativeAsync: NSObject {
public func invoke(_ methodName: String, withData data: Any?, withCallback callback: @escaping (NSDictionary) -> Void) {
let selectorName = methodName + (data != nil ? "WithData:callback:" : "WithCallback:")
let selector = Selector(selectorName)
let block: @convention(block) (NSDictionary) -> Void = callback
if self.responds(to: selector) {
if let data = data {
self.perform(selector, with: data, with: block)
} else {
self.perform(selector, with: block)
}
} else {
callback(RespObject(msg: "\(selectorName) is not a valid method."))
}
}
/*
自定义原生异步函数编写规则:
1. 函数接受参数必须为 data,且类型只能为 Any,类型判断在函数内进行
2. 函数接受闭包必须为 callback,且类型只能为 (NSDictionary) -> Void,可以使用 RespObject 函数构建
*/
func someAsyncFunction(callback: @escaping (NSDictionary) -> Void) {
DispatchQueue.global().async {
DispatchQueue.main.async {
callback(RespObject(data: "Result from someAsyncFunction"))
}
}
}
}
var sendMsgClosure: ((Any) -> Void)?
@objcMembers
class NativeMsg: NSObject {
static let shared = NativeMsg()
private override init() { }
class func configure(_ block: @escaping (Any) -> Void) {
sendMsgClosure = block
}
class func sendMsg(_ name: String, with data: NSDictionary?) {
var msg: [String: AnyHashable] = ["name": name]
if data != nil {
msg["data"] = data
}
sendMsgClosure?(msg)
}
class func sendMsg(_ name: String) {
let msg: NSDictionary = ["name": name]
sendMsgClosure?(msg)
}
}
这样就能在 Swift 中实现所有原生逻辑了,当然在开发过程中还做了原生部分单方面对小程序发送消息,这是封装好的调用插件的东西,时间关系没有更深入封装
export class Plugin {
eventListeners = {};
plugin = null;
constructor(pluginId) {
let that = this;
console.log('init plugin')
wx.miniapp.loadNativePlugin({
pluginId,
success: (res) => {
that.plugin = res
// 监听事件
res.onMiniPluginEvent((event) => {
const name = event.name;
const data = event.data;
// 根据事件名,找到对应的事件id
const eventIds = Object.keys(that.eventListeners).filter(eventId => {
return that.eventListeners[eventId].name === name;
});
// 根据事件id,找到对应的回调函数
eventIds.forEach(eventId => {
const callback = that.eventListeners[eventId].callback;
callback(data);
});
})
},
fail: (e) => {
console.log('load plugin fail', e)
}
})
}
async getPlugin() {
let plugin = null
if (this.plugin) {
plugin = this.plugin
} else {
plugin = await new Promise((resolve, reject) => {
setTimeout(() => {
reject('timeout');
}, 1500);
let interval = setInterval(() => {
if (this.plugin) {
clearInterval(interval)
resolve(this.plugin)
}
}, 10)
})
}
return plugin
}
add(name, callback) {
// 随机取一个唯一的事件id
const eventId = Math.random().toString(36).substr(2);
// 在这个事件id下,存储需要监听的事件名和回调函数
this.eventListeners[eventId] = {
name,
callback,
};
// 返回这个事件id,用于后续的移除事件监听
return eventId;
}
once(name, callback) {
// 用add方法添加事件监听
const eventId = this.add(name, (data) => {
// 执行回调函数
callback(data);
// 执行完回调函数后,移除事件监听
this.remove(eventId);
});
}
remove(eventId) {
// 从eventListeners中删除这个事件id
delete this.eventListeners[eventId];
}
}
let pluginInstance = new Plugin('')
export default pluginInstance
最后
抱着一开始的设想,如果能使用原生工程中嵌入小程序模块,对开发者来说无疑是最好的,有着优秀的拓展性和足够简单的开发方式,例如 Widget 等,如果只能在小程序工程中嵌入原生模块,就只能等 Donut 的更新