自己动手搭建一个基于WKWebView的webview架构

一、前言

关于WKWebView(或者UIWebView)的用法,网上的资料很多,也很详细,这篇文章不会详细介绍这些知识点。写这篇文章,是因为项目中有一些界面是通过H5实现的(其实就跟大家每天浏览的网页一样,不要被H5这个词吓到),为了方便使用就封装了一些功能,包括与js之间的相互调用等,再加上看到经常有人在Q群里问webview如何与js交互,所以决定把项目中这部分内容整理一下,开源出来,希望能对一些同行有一点点帮助(其实只是简单的封装,谈不上什么开源...😁 )

先放源码吧 -> SHWKWebView

二、应该具备哪些功能

使用WebView嵌套H5页面,我们经常会遇到以下需求:
1、导航栏下面要能显示进度条
2、导航栏上的标题要显示成H5页面上的title
3、要能跟js互相调用,包括有时候会涉及到一些返回值的处理
4、能下拉刷新webview内容
5、页面加载失败要给提示
6、如果APP里用户已经登录,需要把登录信息(比如token)传给H5
甚至还会有以下需求:
7、(接着第6条)H5页面里有时也会有一些超链接跳转(href),需要截获这些跳转自动补上token参数
8、(接着第7条)当H5页面上的token参数与APP里保存的token参数不一致时,要使用APP里的token替换掉
9、需要记录H5页面内的跳转历史,点击后退的时候回到的是上一个历史
10、(接着9)可以指定某个页面后退到的H5页面
等等等。。。

我列举出来的这些都是自己项目中实际遇到的 😭 😭 😭 ,当然也是已经解决的。
这次整理出来的源码里没有包含所有功能(只包含了1、2、3、5这几个功能),有一些涉及到二次封装的内容,如果都放出来略显杂乱,有一些功能现在回想一下感觉并不是很合理。如果大家想了解,我回头再整理一下解决方案和思路吧。

三、一些实现方案

  • SHWKWebView的封装
    加载url:只需要传url的相对路径和参数即可(这2个方法也支持绝对路径)
- (void)loadRequestWithRelativeUrl:(nonnull NSString *)relativeUrl;

- (void)loadRequestWithRelativeUrl:(nonnull NSString *)relativeUrl params:(nullable NSDictionary *)params;

实现代码:


- (void)loadRequestWithRelativeUrl:(NSString *)relativeUrl params:(NSDictionary *)params {
    
    NSURL *url = [self generateURL:relativeUrl params:params];
    
    [self loadRequest:[NSURLRequest requestWithURL:url]];
}

- (NSURL *)generateURL:(NSString*)baseURL params:(NSDictionary*)params {
    
    self.webViewRequestUrl = baseURL;
    self.webViewRequestParams = params;
    
    NSMutableDictionary *param = [NSMutableDictionary dictionaryWithDictionary:params];
    
    NSMutableArray* pairs = [NSMutableArray array];

  //可以在这里将token参数添加进去,这样就可以实现第6点功能    

    for (NSString* key in param.keyEnumerator) {
        NSString *value = [NSString stringWithFormat:@"%@",[param objectForKey:key]];

        NSString* escaped_value = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
                                                                              (__bridge CFStringRef)value,
                                                                              NULL,
                                                                              (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ",
                                                                              kCFStringEncodingUTF8);
        
        [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]];
    }
    
    NSString *query = [pairs componentsJoinedByString:@"&"];
    baseURL = [baseURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    
    NSString* url = @"";
    if ([baseURL containsString:@"?"]) {
        url = [NSString stringWithFormat:@"%@&%@",baseURL, query];
    }
    else {
        url = [NSString stringWithFormat:@"%@?%@",baseURL, query];
    }
    //绝对地址
    if ([url.lowercaseString hasPrefix:@"http"]) {
        return [NSURL URLWithString:url];
    }
    else {
        return [NSURL URLWithString:url relativeToURL:self.baseUrl];
    }
}

为了做demo测试,额外增加了一个加载本地html文件的方法


/**
 *  加载本地HTML页面
 *
 *  @param htmlName html页面文件名称
 */
- (void)loadLocalHTMLWithFileName:(nonnull NSString *)htmlName

实现代码:


- (void)loadLocalHTMLWithFileName:(nonnull NSString *)htmlName {

    NSString *path = [[NSBundle mainBundle] bundlePath];
    NSURL *baseURL = [NSURL fileURLWithPath:path];
    NSString * htmlPath = [[NSBundle mainBundle] pathForResource:htmlName
                                                          ofType:@"html"];
    NSString * htmlCont = [NSString stringWithContentsOfFile:htmlPath
                                                    encoding:NSUTF8StringEncoding
                                                       error:nil];
    
    [self loadHTMLString:htmlCont baseURL:baseURL];
}
  • SHWKWebViewController的封装
    这是一个Controller,可以直接使用,也可以创建新的Controller继承SHWKWebViewController来使用,我推荐使用继承的方式,因为可以把不同的页面区分开,每个页面加载的url和相关的业务逻辑都可以单独处理,代码易读,也容易维护。而且如果你的项目里需要添加一些统计(比如友盟的页面统计),也很好处理。
    SHWKWebViewController主要完成了对一些功能的封装,比如进度条、页面title以及webview的生命周期。
    进度条和title都是通过KVO实现:

  if (self.shouldShowProgress) {
        [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
    }

    if (self.isUseWebPageTitle) {
        [self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
    }

其中,进度条用的是UINavigationController+SGProgress(已经通过文件的形式引入到项目中)


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        
        if (object == self.webView) {
            [self.navigationController setSGProgressPercentage:self.webView.estimatedProgress*100 andTintColor:[UIColor colorWithRed:24/255.0 green:124/255.0 blue:244/255.0f alpha:1.0]];
        }
        else{
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    else if ([keyPath isEqualToString:@"title"]){
        if (object == self.webView) {
            if ([self isUseWebPageTitle]) {
                self.title = self.webView.title;
            }
        }
        else{
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

四、关于Objective-C与js的相互调用

把这一块单独提出来是因为这一块很重要,因为大家遇到问题最多的可能就是这块了。这里说一下项目里对js调用的处理逻辑。
WKWebView要处理js调用,需要添加ScriptMessageHandler(这一步在SHWKWebView里已经添加)

[configuration.userContentController addScriptMessageHandler:self name:@"webViewApp"];

然后是实现WKScriptMessageHandler代理


- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    NSLog(@"message:%@",message.body);
    if ([message.body isKindOfClass:[NSDictionary class]]) {
        
        NSDictionary *body = (NSDictionary *)message.body;
        
        SHScriptMessage *msg = [SHScriptMessage new];
        [msg setValuesForKeysWithDictionary:body];
        
        if (self.sh_messageHandlerDelegate && [self.sh_messageHandlerDelegate respondsToSelector:@selector(sh_webView:didReceiveScriptMessage:)]) {
            [self.sh_messageHandlerDelegate sh_webView:self didReceiveScriptMessage:msg];
        }
    }
    
}

可以看到,我们将js脚本调用封装了一个对象SHScriptMessage,


/**
 *  WKWebView与JS调用时参数规范实体
 */
@interface SHScriptMessage : NSObject

/**
 *  方法名
 *  用来确定Native App的执行逻辑
 */
@property (nonatomic, copy) NSString *method;

/**
 *  方法参数
 *  json字符串
 */
@property (nonatomic, copy) NSDictionary *params;

/**
 *  回调函数名
 *  Native App执行完后回调的JS方法名
 */
@property (nonatomic, copy) NSString *callback;

@end

同时提供delegate方法供SHWKWebViewController实现


/**
 *  JS调用原生方法处理
 */
- (void)sh_webView:(SHWKWebView *)webView didReceiveScriptMessage:(SHScriptMessage *)message {
    
    NSLog(@"webView method:%@",message.method);
    
    //返回上一页
    if ([message.method isEqualToString:@"tobackpage"]) {
        [self.navigationController popViewControllerAnimated:YES];
    }
    //打开新页面
    else if ([message.method isEqualToString:@"openappurl"]) {
        
        NSString *url = [message.params objectForKey:@"url"];
        if (url.length) {
            SHWKWebViewController *webViewController = [[SHWKWebViewController alloc] init];
            webViewController.url = url;
            
            [self.navigationController pushViewController:webViewController animated:YES];
        }
    }
}

只需要比较message.method就可以知道js想调用原生的哪个方法了(当然了,method和params是需要跟H5开发人员约定好的)

可能你还记得,前面我是推荐使用继承的方式来使用SHWKWebViewController了,那关于js调用原生方法的处理应该写在哪里呢?是SHWKWebViewController里还是具体继承的Controller里呢?
关于这块,我们最开始是写在继承的Controller里的,好处是不同的业务逻辑分开处理,业务代码集中在一个Controller里,这样就更容易理解和维护。后来发现会有很多通用的方法,比如打开新页面openappurl,这个方法可能会在每个H5页面都会有,要是每个Controller里都写肯定是不合适的。
因此,通用的js方法,最好写在SHWKWebViewController里,其他与业务相关的js最好写在具体的Controller里。
在Controller里重写这个delegate方法(注意else的时候要调用super的delegate,否则那些通用的js方法就没法在这个Controller里调用了)


- (void)sh_webView:(SHWKWebView *)webView didReceiveScriptMessage:(SHScriptMessage *)message {

    if ([message.method isEqualToString:@"hello"]) {
        
        if (message.callback.length) {
            [self.webView callJS:[NSString stringWithFormat:@"%@('hello-JS')",message.callback] handler:^(id  _Nullable response) {
                NSLog(@"调用callback结果:%@",response);
            }];
        }
    }
    else {
        [super sh_webView:webView didReceiveScriptMessage:message];
    }
}

上面是原生APP里对js调用的一些准备工作,具体js调用方法如下(具体见main.html文件):

function call(text) {
                var message = {
                    'method' : 'hello',
                    'params' : {
                        'name':'张三',
                        'age':28
                    },
                    'callback': 'callback'
                };
                window.webkit.messageHandlers.webViewApp.postMessage(message);
            }

关于原生调用js,这个WKWebView本身就提供了方法,而且还可以接收到js的返回值:

- (void)callJS:(NSString *)jsMethod handler:(void (^)(id _Nullable))handler {
    
    NSLog(@"call js:%@",jsMethod);
    [self evaluateJavaScript:jsMethod completionHandler:^(id _Nullable response, NSError * _Nullable error) {
        if (handler) {
            handler(response);
        }
    }];
}

另外,再说明下,对于js里的alert和confirm方法,默认在WKWebView是没有效果的,需要重写下面这2个方法:

#pragma mark - WKUIDelegate

/**
 *  处理js里的alert
 *
 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }]];
    
    [self presentViewController:alert animated:YES completion:nil];
}

/**
 *  处理js里的confirm
 */
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert];
    
    [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }]];
    
    [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }]];
    
    [self presentViewController:alert animated:YES completion:nil];
}

最后

文笔和能力有限,如果上述内容有误,欢迎指出,我会及时改正!希望对你有一丢丢帮助!
最后,Enjoy Yourself!

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

推荐阅读更多精彩内容

  • 前言 关于UIWebView的介绍,相信看过上文的小伙伴们,已经大概清楚了吧,如果有问题,欢迎提问。 本文是本系列...
    CoderLF阅读 8,927评论 2 12
  • iOS 的 Cookie 存取 https://juejin.im/entry/58d4c4cc44d904006...
    Farmers阅读 5,861评论 0 16
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,144评论 1 23
  • WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨...
    Aiana阅读 4,541评论 1 8
  • 转载:http://www.cnblogs.com/NSong/p/6489802.html 导语 WKWebVi...
    李小威阅读 4,837评论 8 9