急急急! 快从UIWebView安全更新到WKWebView(包括做好线上兼容)

一、背景

查找很多零散博客,头疼,根据本次升级经验,还是自己整理出全面的一篇~✌️

公司项目相当老,苹果已下最后通牒,2020年4月之前必须更新为 WKWebView 了,就有了这次小心翼翼的升级。

首先考虑两点:

前端:JS 代码保证旧版和新版都能用,也就调用客户端 UIWebView 和 WKWebView 的方法都行得通才行;
客户端:写法确实有些不一样。

首先,Demo 写的很全,两端代码都有,方便学习,可以直接下载对着博客查看。


更新:终于做完了,比想象的要辛苦。方案没有问题,但还是有些经验可以总结的,见第四节的总结(筋疲力尽~~~~~~~)。

二、JS 端

这一块可以提供给前端小伙伴,毕竟需要他们帮忙协助本次升级嘛~

2.1 缩放:解决滑动问题

“minimum-scale=1.0, maximum-scale=1.0, user-scalable=no”

2.2 JS调用iOS,传参数

try { // 新
    window.webkit.messageHandlers.callAMethod.postMessage('callcallcall');
} catch (error) { // 旧
    window.callAMethod('callcallcall');
}

注意:

  1. 新版postMessage必须有参数,无也要传参:.postMessage(null)
  2. 多个参数,打包传参:
    数组:.postMessage(['第一个参数','第二个参数','第三个参数'])
    字典:.postMessage({'arg1' : '第一个参数', 'arg2' : '第二个参数', 'arg3' : '第三个参数'})

2.3 JS调用iOS,并🌹同步的🌹拿到返回值

注意:先约定 type 为JSbridge

这里的try-catch是demo要这么写,前端同学有自己的判断代码的,不贴了哦。

var result;
try { // 新
    var type = "JSbridge";
    var functionName = "getReturnValue:";
    var args = "一个参数";

    var payload = {"type": type, "functionName": functionName, "arguments": args};
    result = prompt(JSON.stringify(payload));
    
} catch (error) { // 旧
    result = window.getReturnValue();
}

2.4 iOS主动调用JS,或作为JS🌹异步的🌹拿到传值

// 🌹异步的🌹响应客户端的调用
function jsResponseClient(response) {
    alert(response);
}

三、客户端

这块代码太多了... 上面 JS 那块代码及说明友友们需要粘走给前端小伙伴查看。这里咱们自己看,我就写重点了哈。

3.1 响应JS调用

见代码吧亲,注释也很详细,这里就提醒两点:

  1. 和UIWebView不同的是,生成 webView 的时候就先注册和 JS 交互的方法名;
// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController *userContent = [[WKUserContentController alloc] init];
// JS回调方法的监听,这里要注意避免释放问题
// (懒得引入YY或写一个了,写的时候别忘了哈)
id handler = self;//[YYWeakProxy proxyWithTarget:self];
for (NSString *name in self.scriptNameList) {
    [userContent addScriptMessageHandler:handler name:name];
}
  1. 接收 JS 调用的代理方法。
webView.navigationDelegate = self;
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    if ([message.name isEqualToString:kJSMoreParameter]) {
        // 多个参数
        NSLog(@"%@", message.body);
    }
}

3.2 响应JS调用,并同步返回值

这里同步的给 JS 返回值比较特别,需要先提到 JS 的弹框 WKWebView 不显示问题,需要 webView 接受以下代理方法,并代码显示弹框:

webView.UIDelegate = self;
/** 显示一个按钮。点击后调用completionHandler回调 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }]];
    [self presentViewController:alertController animated:YES completion:nil];
}

/** 显示两个按钮。通过completionHandler回调判断用户点击的确定还是取消按钮 */
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }]];
    [alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }]];
    [self presentViewController:alertController animated:YES completion:nil];
}

/** 显示一个带有输入框和一个确定按钮的。通过completionHandler回调用户输入的内容 */
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
    
    // 拦截弹框,同步返回传值给JS
    NSError *err = nil;
    NSData *dataFromString = [prompt dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:dataFromString options:NSJSONReadingMutableContainers error:&err];
    if (!err) {
        NSString *type = [payload objectForKey:@"type"];
        if (type && [type isEqualToString:@"JSbridge"]) {
            NSString *returnValue = @"";
            NSString *functionName = [payload objectForKey:@"functionName"];
            NSDictionary *args = [payload objectForKey:@"arguments"];
            
            SEL selector = NSSelectorFromString(functionName);
            if ([self respondsToSelector:selector]) {
                returnValue = [self performSelector:selector withObject:args];
            }
            completionHandler(returnValue);
            return ;
        }
    }
    
    // 显示弹框
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        
    }];
    [alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields.lastObject.text);
    }]];
    [self presentViewController:alertController animated:YES completion:nil];
}

呐,重点就是上面的拦截弹框,同步返回传值给JS啦~

原因就是 JS 同步拿 WKWebView 的返回值方法也比较特殊,是通过promot弹框方式拿的。所以我们把弹框拦截,同步给返回值即可。

3.3 主动调用JS,或作为异步返回值给JS

/** 主动调用JS方法 */
- (void)callJSMethod {
    NSString *aString = @"客户端主动调用JS方法,也可用于JS🌹异步的🌹拿到客户端返回值";
    NSString *func = [NSString stringWithFormat:@"jsResponseClient('%@')", aString];
    [self.webView evaluateJavaScript:func completionHandler:^(id _Nullable object, NSError * _Nullable error) {
        NSLog(@"%@   %@", object, error);
    }];
}

以上相关功能 UIWebView 也全部实现,代码就不贴了,见 Demo 哈。


更新:

四、总结

做完很辛苦,都是泪,然后还是有些经验可以总结的。。。

  1. 首先,如果你的项目很老,写一堆太多了。我们要注重项目结构,使用分类来分担业务功能:

- EHIWebDefines // 事件名、静态URL等
- EHIWebViewController+Intercept // 发送请求前的拦截操作,例如h5里的调微信/支付宝支付
- EHIWebViewController+JSAction // JS调用的一堆事件
- EHIWebViewController+JSGetValue // JS调用同步获取值

  1. JSAction 和 JSGetValue 中是方法实现,调起的优化可以使用 Runtime 优雅实现:

#pragma mark - WKScriptMessageHandler ⚠️JS调用(具体实现在JSAction中)

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSString *functionName = message.name;
    // 函数统一都加分号(避免后来再加参数,前面版本不兼容)
    if (functionName.length && ![functionName hasSuffix:@":"]) {
        functionName = [NSString stringWithFormat:@"%@:", functionName];
    }
    NSDictionary *args = message.body;
    
    SEL selector = NSSelectorFromString(functionName);
    if ([self respondsToSelector:selector]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            kSuppressPerformSelectorLeakWarning([self performSelector:selector withObject:args]);
        });
    }
}

#pragma mark - WKUIDelegate ⚠️JS弹框的显示 或 同步获取值(具体实现在JSGetValue中)
/** 显示一个带有输入框和一个确定按钮的。通过completionHandler回调用户输入的内容 */
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
    
    // 拦截弹框,同步返回传值给JS
    NSError *err = nil;
    NSData *dataFromString = [prompt dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:dataFromString options:NSJSONReadingMutableContainers error:&err];
    if (!err) {
        NSString *type = [payload objectForKey:@"type"];
        if (type && [type isEqualToString:@"JSbridge"] && [webView.URL.host containsString:@"你的host,防止外者调用"]) {
            NSString *returnValue = @"";
            NSString *functionName = [payload objectForKey:@"functionName"];
            NSDictionary *args = [payload objectForKey:@"arguments"];
            
            SEL selector = NSSelectorFromString(functionName);
            if ([self respondsToSelector:selector]) {
                kSuppressPerformSelectorLeakWarning(returnValue = [self performSelector:selector withObject:args]);
            }
            completionHandler(returnValue);
            return ;
        }
    }
    
    // 显示弹框
    // ...
}

  1. 需要注册很多个方法也很烦,虽然我们项目老不会一次弄好,但是思考一下,以后可以用得着:

只注册一个方法,参数为 URL,就可以使用中间件来自由调用了!从扩展和维护角度都更好啊!!!

不做过这么折腾的事情是体会不到的,真的太痛苦了!好好写代码,为后人少点坑,ending...😭

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容