WKWebView如何接 H5 SDK

3,450 阅读6分钟

  WKWebView是苹果在iOS8中引入的新组件,目的是给出一个新的高性能的WebView解决方案,摆脱过去 UIWebView 的老旧笨重特别是内存占用量巨大的问题,本文主要是想讨论使用iOS控件WKWebView通过中间页加载H5 SDK,并且会根据企业开发需求配置不同环境(测试、线上、预发)下的H5 SDK,在讨论这个问题之前会稍带啰嗦总结下WKWebView的加载方式和基本API

WKWebView加载H5方式

1. 通过url地址加载网络资源

- (void)loadWithUrl{
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com/"]]];
    [self.view addSubview:self.webView];
}


Snip20220206_3.png

2.通过html加载本地资源


html文件

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>iOS and JS</title>
        <style type="text/css">
            * {
                font-size: 40px;
            }
        </style>
    </head>
    <body>
        <div style="margin-top: 10px">
            <h1 style="color: red;">加载本地html文件</h1><br/>
        </div>
    </body>
</html>


加载函数

- (void)loadWithHtml {
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    NSURL *path = [[NSBundle mainBundle] URLForResource:@"text" withExtension:@"html"];
    [self.webView loadRequest:[NSURLRequest requestWithURL:path]];
    [self.view addSubview:self.webView];
}


3.直接加载html代码

- (void)loadWithHtmlStr {
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    //注意:" 要用 \"代替
    NSString *HTMLData = @"<h1 style = \"color: red;\">直接加载本地htmlstr</h1>";
    [self.webView loadHTMLString:HTMLData baseURL:nil];
    [self.view addSubview:self.webView];
}


WKWebView基本API

WKWebView的基本API在blog.csdn.net/baihuaxiu12… 已经介绍的很详细了,这里不做过多赘述,仅挑比较常用的进行说明。

WKUserContentController内容交互控制器

// 只读属性,所有添加的WKUserScript都在这里可以获取到
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
 
// 注入JS
- (void)addUserScript:(WKUserScript *)userScript;
 
// 移除所有注入的JS
- (void)removeAllUserScripts;
 
// 添加scriptMessageHandler到所有的frames中,则都可以通过
// window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
// 发送消息
// 比如,JS要调用我们原生的方法,就可以通过这种方式了
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
 
// 根据name移除所注入的scriptMessageHandler
- (void)removeScriptMessageHandlerForName:(NSString *)name;

  我们可以在创建WKWebView的时候为其config.userContentController添加handler,并且在遵守WKScriptMessageHandler前提下可以在协议方法- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{}进行监听

WKScriptMessageHandler

@protocol WKScriptMessageHandler <NSObject>

@required

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

@end

结合例子

OC中:
config.userContentController = [[WKUserContentController alloc]init];
[config.userContentController addScriptMessageHandler:self name:@"AppModel"];
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];

JS中:
<script type="text/javascript">
  function text(option) {
    window.webkit.messageHandlers.AppModel.postMessage("向OC中传递的数据");
  }
 </script>

OC中协议监听:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"AppModel"]) {
        NSLog(@"回调监听 === %@", message.body); // 打印结果:向OC中传递的数据
    }
}

  在上述的小列子中,在WKWebView进行实例化时添加了name为"AppModel"的handler,一旦在JS中触发了text方法,js通过window.webkit.messageHandlers.AppModel.postMessage()就可以给OC传递消息,在OC端的监听方法中可以对其进行处理。

WKNavigationDelegate

@protocol WKNavigationDelegate <NSObject>
 
@optional
 
// 决定导航的动作,通常用于处理跨域的链接能否导航。WebKit对跨域进行了安全检查限制,不允许跨域,因此我们要对不能跨域的链接
// 单独处理。但是,对于Safari是允许跨域的,不用这么处理。
// 这个是决定是否Request
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
 
// 决定是否接收响应
// 这个是决定是否接收response
// 要获取response,通过WKNavigationResponse对象获取
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
 
// 当main frame的导航开始请求时,会调用此方法
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
 
// 当main frame接收到服务重定向时,会回调此方法
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
 
// 当main frame开始加载数据失败时,会回调
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
 
// 当main frame的web内容开始到达时,会回调
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
 
// 当main frame导航完成时,会回调
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
 
// 当main frame最后下载数据失败时,会回调
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
 
// 这与用于授权验证的API,与AFN、UIWebView的授权验证API是一样的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
 
// 当web content处理完成时,会回调
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
 
@end

在代理协议WKNavigationDelegate中使用最多的还是didFinishNavigation:可以在导航加载完成时,在此回调配合evaluateJavaScript:去主动调用JS的一些方法.

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{

    // 直接执行text函数
    [self.webView evaluateJavaScript:@"text()" completionHandler:^(id _Nullable res, NSError * _Nullable error) {
        NSLog(@"respose = %@ -- error = %@", res, error);
    }];
    
    //执行text函数并带入参数
    NSString *text = [NSString stringWithFormat:@"text('%@')",self.userId];
    [self.webView evaluateJavaScript:text completionHandler:^(id _Nullable res, NSError * _Nullable error) {
        NSLog(@"respose = %@ -- error = %@", res, error);
    }];
}

WKWebView中间页加载H5 SDK

  说了这么多终于来到了我们要讨论的问题,在OC的开发中避免不了需要在OC中展示H5页面和进行一些交互。一般企业推出的SDK都会包含了iOS、Android、和H5的三种接入方式,其中H5不仅可以适用于web也可以用iOS和Android来接入。在此例子中使用[网易七鱼]qiyukf.com/docs/guide/… 在iOS中接入H5 SDK的客服咨询聊天功能为例(此例子仅作接入思路参考,真正开发中不建议这么做,毕竟接入H5 SDK 会散失一部分原本属于SDK的功能,比如它的客服消息提示等)
  在上述例子中的SDK可以使用url和本地html文件的方式接入,但是url地址进行拼接的一些参数又是无效的(匿名用户),只能排除,使用html文件的接入方式,一般开发中会根据不同环境(开发、测试)来进行初始化SDK。我们可以在html文件中定义一个js方法,触发方法时带着一些必需的参数进行初始化

html:
function register(isDev, appkey, uid, name){
    var url;
    if (isDev) { // 不同环境域名不相同
      url = `https://aaa.aaa.aaa/script/${appkey}.js?hidden=1`;
    } else {
      url = `https://bbb.bbb.bbb/script/${appkey}.js?hidden=1`;
    }
    
    //初始化
    (function (w, d, n, a, j) {
        w[n] = w[n] || function () {
            (w[n].a = w[n].a || []).push(arguments);
        };
        j = d.createElement('script');
        j.async = true;
        j.src =`${url}`;
        d.body.appendChild(j);
    })(window, document, 'ysf');
    
    //上报用户信息
    ysf('config', {
        uid:`${uid}`,
        name:`${name}`,
        data:JSON.stringify(xxx),
        success: function() {
           window.webkit.messageHandlers.AppModel.postMessage('true');
        },
        error: function() {
           window.webkit.messageHandlers.AppModel.postMessage('fase');
        }
    });
}

  在上述的html文件中使用传参isDev来区分开发环境还是线上环境,传参appkey来初始化sdk,uid和name来配置上报用户信息

- (NSString *)javascriptStr {
    NSString *domain = xxx;
    NSString *account = xxx;
    //线上环境
    if (setting == Online) {
        return [NSString stringWithFormat:@"register(0,'%@','%@','%@')",kOnlineAppKey, domain, account];
    }
    //测试环境
    if (setting == Test) {
        return [NSString stringWithFormat:@"register(1,'%@','%@','%@')",kTestAppKey, domain, account];
    }
    //预发环境
    if (setting == OnlinePre) {
        return [NSString stringWithFormat:@"register(0,'%@','%@','%@')",kPreAppKey, domain, account];
    }
    return @"";
}

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
    // 拿到不同环境的配置字符串
    NSString *javascriptStr = [self javascriptStr];
    if (![NSString isEmptyStr:javascriptStr]) {
        //执行JS代码
        [self.webView evaluateJavaScript:javascriptStr completionHandler:^(id _Nullable res, NSError * _Nullable error) {
            // NSLog(@"res = %@ -- error = %@", res, error);
        }];
    } else {
        NSLog(@"Toast提示")
    }
}

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"AppModel"]) {
        if ([message.body isEqualToString:@"true"]) {
            NSLog(@"初始化上报成功") ---(执行OC或者JS代码等操作)
        } else {
            NSLog(@"初始化上报失败") ---(Toast提示等操作)
        }
    }
}

  在上述的OC文件中在中间页加载完成之后调用evaluateJavaScript:来执行html的JS代码进行初始化和上报用户信息等操作,操作执行后的回调在WKScriptMessageHandler中监听,对应的成功失败在进行其他的业务逻辑处理。

总结

  WKWebView作为OC侧用来展示交互H5重要控件,包含了众多的代理方法可以很好的执行JS代码和感知JS操作后的回调,十分方便灵活,需要注意的是在执行本地html文件时,需要注意本地文件读取和跨域的问题。