OC 代码 webview 执行 JS,如何有效的消除回调地狱
OC 代码 webview 执行 JS,如何有效的消除回调地狱
在项目中,偶发的报一个 JS 错误 A lavaScript exception occurred ,找到这段的具体实现
const scrDom = document.createElement("script");
scrDom.src = basic.min.js;
scrDom.addEventListener("load", () => {
if (!haveMacJS) {
window.webkit.messageHandlers.setPlanSucceed.postMessage("");
}
});
const macScrDom = document.createElement("script");
macScrDom.src = mac.min.js;
macScrDom.addEventListener("load", () => {
window.webkit.messageHandlers.setPlanSucceed.postMessage("");
});
发现逻辑是当需要在 webView 中 加载两个精算文件,并且等到这两个精算文件都加载完毕后,再通知 native 端,把参数传回到 webView,执行计算,发现这段代码并不是按顺序执行完,然后才通知 native 端的.
出现问题的原因是,需要在 JS 端和 Native 端来回传参,无形中增加了问题出现的概率,为了遵循相同逻辑,同一地方处理的原则,就在 native 端加载这两个 JS 文件
NSString *basicString = [NSString stringWithContentsOfFile: basicJsPath encoding:NSUTF8StringEncoding error: nil];
// 加载第一个js文件
[webView evaluateJavaScript: basicString completionHandler:^(id _Nullable res, NSError * _Nullable basicError) {
//如果需要加载第二个文件
if ([RFileTool existFileAtPath: macroJsPath])
{
//第二个精算文件存在的话,加载它
NSString *macroString = [NSString stringWithContentsOfFile: macroJsPath encoding:NSUTF8StringEncoding error:nil];
[weakWebView evaluateJavaScript: macroString completionHandler:^(id _Nullable response, NSError * _Nullable macroError) {
// 第二个文件加载成功的话,通知webView,执行初始化操作
NSString *javaScriptString = [NSString stringWithFormat:@"setPlan('%@',%d)", weakSelf.lastPlanId, YES];
[weakWebView evaluateJavaScript:javaScriptString completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//初始化成功,调用native的计算方法.
[weakSelf loadCalculate];
}];
}];
}
else
{
// 不需要加载第二个精算文件的话,通知webView,执行初始化操作
NSString *javaScriptString = [NSString stringWithFormat:@"setPlan('%@',%d)", weakSelf.lastPlanId, NO];
[weakWebView evaluateJavaScript:javaScriptString completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//初始化成功,调用native的计算方法.
[weakSelf loadCalculate];
}];
}
}];
以上一个的方法中,套了三层回调,这还是省略了处理错误的情况下,如果再加上错误处理,这里的代码将会变的非常的多以及丑陋.
如果能像其他语言中,Promise 链式调用方式,代码就会变的清晰了,可惜 OC 不支持 Promise,只能自己实现了.
先看下最终效果,然后讲解具体的实现逻辑,虽然代码量看上去没少多少,但是调用逻辑清晰多了,也没有那么多重复代码了.当然这里每一个 promise 对象都有一个 catch,这里是为了统计具体是哪一个 js 加载出错了,如果不需要这样统计,可以修改代码,统一 catch
//promise入口,使用类方法firstlyWithJSEvaluator,创建第一个promise
[[[[[[[RJSEvalPromise firstlyWithJSEvaluator: webView js: ^NSString * (void) {
//返回需要加载的js文件
NSString *basicString = [NSString stringWithContentsOfFile: basicJsPath encoding:NSUTF8StringEncoding error: nil];
return basicString;
}] catch:^(NSError * _Nonnull basicError) {
//处理加载js出错的情况
NSLogDebug(@"❌---load basic js file:%@ faile--error:%@---❌", self.lastPlanId, basicError);
}] then: ^NSString * (void) {
//第一个js执行完毕后,这里返回需要加载的第二个js文件
if (isHaveMacro)
{
NSString *macroString = [NSString stringWithContentsOfFile: macroJsPath encoding:NSUTF8StringEncoding error: nil];
return macroString;
}
return nil;
}] catch:^(NSError *macroError) {
//处理加载js出错的情况
NSLogDebug(@"❌---load macro js file:%@ faile--error:%@---❌", self.lastPlanId, macroError);
}] then: ^NSString * (void) {
//通知webView,执行初始化操作
NSString *javaScriptString = [NSString stringWithFormat:@"setPlan('%@',%d)", self.lastPlanId, isHaveMacro];
return javaScriptString;
}] catch:^(NSError *setPlanError) {
NSLogDebug(@"❌---set plan:%@ faile--error:%@---❌", self.lastPlanId, setPlanError);
}] finaly: ^NSString * (void) {
//初始化成功,调用native的计算方法.
[self loadCalculate];
}];
RJSEvalPromise 的实现逻辑
firstlyWithJSEvaluator 这个方法需要传两个参数,第一个是 JS 执行环境,也就是 webView, 第二个是一个回调函数,类型定义为 typedef NSString *_Nullable(^SQSPromiseFirstCallback)(void); ,回调函数调用后,返回的就是需要执行的 JS 代码,这里直接返回了 promise,却没对它强引用,会不会导致在 JS 执行前自动释放呢,答案是不会,因为在 aciton 实现中, webView 的回调函数会捕获这个 promise 对象
//RJSEvalPromise.m
+ (instancetype)firstlyWithJSEvaluator:(WKWebView *)jsEvaluator js:(SQSPromiseFirstCallback)js {
RJSEvalPromise* promise = [RJSEvalPromise makePromiseWithJSEvaluator: jsEvaluator js: js];
promise.aciton();
promise.aciton = nil;
return promise;
}
makePromiseWithJSEvaluator 就是真正创建 promise 对象的方法,这里定义 promise 的具体执行逻辑,这里 promise 的 action 中直接使用了 promise,却不会循环引用的原因是上边在调用 action 后,直接把 action 手动置为 nil,消除了循环引用,但是编译器会弹出警告,所以加上 diagnostic ignored 让编译器忽略掉这个警告
+ (instancetype)makePromiseWithJSEvaluator:(WKWebView *)jsEvaluator js:(SQSPromiseFirstCallback)js{
RJSEvalPromise * promise = [[RJSEvalPromise alloc] init];
promise.jsEvaluator = jsEvaluator;
promise.state = RJSEvalPromiseStatePending;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
promise.aciton = ^{
NSString *string = js();
// if no need evaluateJavaScript, then Fulfilled.
if (!string)
{
promise.state = RJSEvalPromiseStateFulfilled;
[promise resume];
return;
}
[jsEvaluator evaluateJavaScript: string completionHandler:^(id _Nullable result, NSError * _Nullable error) {
promise.result = result;
promise.error = error;
if (error) {
promise.state = RJSEvalPromiseStateRejected;
}else{
promise.state = RJSEvalPromiseStateFulfilled;
}
[promise resume];
}];
};
#pragma clang diagnostic pop
return promise;
}
webView 调用evaluateJavaScript 方法,执行 JS 后,拿到执行的具体结果,给当前的 promise 赋值对应的状态.然后调用 resume 方法,看是否当前的 promise 链,已经调用完毕了
resume 的实现就比较简单了,如果状态是 Pending 的话,直接返回,是 Rejected ,那么出现错误了,调 catch 回调函数,然后看是否有下一个 promise,有的话就执行它的 action 方法,直到 promise 链调用完毕
- (void)resume {
if (self.state == RJSEvalPromiseStatePending)
{
return;
}
if (self.state == RJSEvalPromiseStateRejected)
{
if (self.catchCallback) {
self.catchCallback(self.error);
}
}
if (self.nextFuture.aciton) {
self.nextFuture.aciton();
}
self.nextFuture.aciton = nil;
if (self.finalCallback) {
self.finalCallback(self.result);
}else if (self.emptyFinalCallback) {
self.emptyFinalCallback();
}
}
当然这里,也可以修改为,如果发生错误的话,就不再执行下一个 promise 了,直接循环查找最后一个 catch 回调.
if (self.state == RJSEvalPromiseStateRejected)
{
while (self.nextFuture && self.nextFuture.catchCallback) {
self.catchCallback = self.nextFuture.catchCallback;
}
if (self.catchCallback) {
self.catchCallback(self.error);
}
}
哦,对了,还有 then 方法,它的实现和 firstlyWithJSEvaluator基本相同,调用 makePromiseWithJSEvaluator 然后保存创建的这个 promise 对象.
- (instancetype)then:(SQSPromiseFirstCallback)callback {
RJSEvalPromise* promise = [RJSEvalPromise makePromiseWithJSEvaluator: self.jsEvaluator js: callback];
self.nextFuture = promise;
[self resume];
return promise;
}
还有 catch 和 finally 方法都是仅仅保存了回调函数,等待执行 JS 出结果后,在 resume 方法中调用
- (instancetype)catch:(void (^)(NSError * _Nonnull))callback {
self.catchCallback = callback;
[self resume];
return self;
}
- (void)finally:(void (^)(void))callback {
self.emptyFinalCallback = callback;
[self resume];
}
当然这样做,有一大堆的[] , 如何通过.(点)调用方法呢? iOS 中使用点调用属性的本质都是调用属性的 getter 方法来获取属性值. 那么连续使用的.调用,需要满足什么条件呢?
1: 必须是对象方法;
2: 必须有返回值,返回值的类型是当前对象类型
3: 一定是没有参数的
看到第三点的要求,不能传参数,那怎么把要执行的 JS 传递过去呢? 那么可以做个变更,返回值不再是一个 promise 对像,而返回一个 block,这个 block,调用后,再返回个 promise 对象,就满足了要求
typedef RJSEvalPromise *_Nullable(^SQSPromiseDotThen)(SQSPromiseFirstCallback);
- (SQSPromiseDotThen)then {
return ^RJSEvalPromise * _Nullable(SQSPromiseFirstCallback _Nonnull callback) {
RJSEvalPromise* promise = [RJSEvalPromise makePromiseWithJSEvaluator: self.jsEvaluator js: callback jsResult: nil result: nil];
self.nextFuture = promise;
[self resume];
return promise;
};
}
这样就达到了,使用.语法的链式调用了
[[RJSEvalPromise firstlyWithJSEvaluator: webView js: ^NSString * (void) {
NSString *basicString = [NSString stringWithContentsOfFile: basicJsPath encoding:NSUTF8StringEncoding error: nil];
return basicString;
}].dotThen(^NSString * (void) {
if (isHaveMacro)
{
NSString *macroString = [NSString stringWithContentsOfFile: macroJsPath encoding:NSUTF8StringEncoding error: nil];
return macroString;
}
return nil;
}).dotThen(^NSString * (void) {
NSString *javaScriptString = [NSString stringWithFormat:@"setPlan('%@',%d)", self.lastPlanId, isHaveMacro];
return javaScriptString;
}) finally:^{
}];
至此,完成了回调地狱到 promise 链式调用的转换.
本博客文章采用 CC 4.0 协议,转载需注明出处和作者。