首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS WebView 的 Hybrid 框架设计

iOS WebView 的 Hybrid 框架设计

作者头像
前端黑板报
发布2018-12-24 13:10:17
1.6K0
发布2018-12-24 13:10:17
举报

前言

随着移动互联网的发展,APP 开发模式也在不断的创新,从最初的 Native 开发到后来的 Hybrid 混合开发,再到最近比较火爆的 React Native、Weex 等项目,这些都标志着 APP 开发已经不再是纯 Native 的工作,还要涉及很多跨平台的技术。

作为一种混合开发模式,Hybrid APP 底层依赖 Native 端的 Web 容器(UIWebview 和 WKWebview),上层使用前端 Html5、CSS、Javascript 做业务开发,这种开发模式非常适合业务快速拓展和迭代,在不发版本的前提下直接更新线上资源,受到不少公司的青睐与关注。

对于 Hybrid APP 开发,虽然业内早已出现 Cordova(PhoneGap)、jQuery Mobile 等框架,但是由于性能、维护成本等原因,并没有在业内非常流行,有些公司转而选择自己开发一套 Hybrid 框架,但是由于没有丰富的经验和应用场景导致开发出来的 Hybrid 框架后期维护成本很高。本文我将对在公司开发的 Hybrid 解决方案跟大家做一个介绍,希望对各位的技术选型起到帮助,也欢迎大家积极交流。

Hybrid APP 特点

Hybrid APP 优势很明显:

  • 跨平台,开发效率高,节约开发成本
  • 业务快速拓展和迭代
  • 及时修复线上 Bug,不需发版

但是 Hybrid 也有自己的劣势,比如体验上肯定比不了 Native,而且对于一个 Native 开发者而言要理解前后端的技术,对开发者的要求较高,但我相信这是好事儿~~

根据之前的经验,我觉得 Hybrid 需要找到自己的应用场景,比如营销、活动等需要快速试错和占领市场的团队来说,Hybrid 很适用,但是对于像 APP 首页这样要求体验高的场景 Hybrid 就不太适用,具体情况可以根据自己公司的 APP 场景做适当的调整。

Hybrid APP 框架

一个完整的 Hybrid APP 框架主要包括 WebView 容器、Bridge、UI、预加载、缓存等模块儿,当然 Bridge、预加载、缓存等也需要相应前后端的支持,比如发布平台、灰度平台、增量更新、CDN 平台等等。

框架结构如下:

在设计这套框架之前,需要弄清楚 Native 与前端的分工,Native 主要提供一个宿主环境,对 WebView 进行封装,提供 Bridge 方法,Header 组件设计,账号信息设计,底层提供预加载和缓存机制,框架的业务方是各个前端团队,所以我们需要站在前端的角度对以上方面进行考虑。本文主要对 WebView、Bridge、Header 设计进行介绍,后续文章会对账号信息设计、预加载和缓存进行持续跟进。

UIWebView 和 WKWebView 兼容

iOS8 以后苹果推出了一套新的 WKWebView,对于 UIWebView 和 WKWebView 的区别,总结如下:

WKWebView 的主要优点是 JS 执行速度快、内存占用小,刚一推出就被开发者所追捧,但是不知道是不是因为苹果爸爸太任性,WKWebView 设计上并没有与 UIWebView 保持一致,无法自动存储 Cookie 和不能通过 NSURLProtocol 自定义请求等坑~导致 WKWebView 并没有被开发者大规模推荐使用。

本套框架的预加载和缓存模块儿需要借助 NSURLProtocol 实现,所以这里还是优先使用 UIWebView(想吐个槽,其实如果预加载和缓存这套系统做好以后,UIWebView 的效果并没不比 WKWebView 差),这里也不能把 WKWebView 一棒子打死不用,对于那些对无需预加载和缓存的页面,可以为前端提供参数(比如 wkwebview=true)让前端自己的去选择是否使用 WKWebView,所以这里需要对 WKWebView 进行兼容。

YZWebView 是对 UIWebView 和 WKWebView 进行封装的类,结构设计如下:

YZWebViewDelegate,UIWebView 和 WKWebView 代理的回调代理。

@protocol YZWebViewDelegate <NSObject>

@optional
- (BOOL)webView:(YZWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(YZWebView *)webView;
- (void)webViewDidFinishLoad:(YZWebView *)webView;
- (void)webView:(YZWebView *)webView didFailLoadWithError:(NSError *)error;

@end

NJKWebViewProgressDelegate,进度条代理方法。

protocol NJKWebViewProgressDelegate <NSObject>

- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress;

@end

YZWebView 初始化方法,通过参数 usingUIWebView 来决定初始化 WKWebView 或者 UIWebView,

- (instancetype)initWithFrame:(CGRect)frame usingUIWebView:(BOOL)usingUIWebView {
    self = [super initWithFrame:frame];
    if (self) {
        _usingUIWebView = usingUIWebView;
        [self p_initSelf];
    }
    return self;
}

- (void)p_initSelf {
    Class wkWebView = NSClassFromString(@"WKWebView");
    if (wkWebView && !self.usingUIWebView) {
        [self initWKWebView];     //初始化WKWebView
    } else {
        [self initUIWebView];     //初始化UIWebView
    }
    [self addSubview:self.currentWebView];
}

- (void)initWKWebView {
    ......
    WKWebView *webView =
    [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.bounds
                                             configuration:webViewConfig];
    webView.UIDelegate = self;
    webView.navigationDelegate = self;
    [webView setAutoresizesSubviews:YES];
    [webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
    [webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleBottomMargin];
    webView.backgroundColor = [UIColor clearColor];
    [webView.scrollView setShowsHorizontalScrollIndicator:NO];

    [webView addObserver:self
              forKeyPath:@"estimatedProgress"
                 options:NSKeyValueObservingOptionNew
                 context:nil];
    _currentWebView = webView;
}

//NJKWebViewProgress 没兼容WKWebView,这里需要通过KVO进行监测
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        self.estimatedProgress = [change[NSKeyValueChangeNewKey] doubleValue];
        if (_progressDelegate && [_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) {
            [_progressDelegate webViewProgress:nil updateProgress:_estimatedProgress];
        }
    }
}

- (void)initUIWebView {
    ......
    UIWebView *uiWebView = [[UIWebView alloc] initWithFrame:self.bounds];
    [uiWebView setAutoresizesSubviews:YES];
    [uiWebView setScalesPageToFit:YES];
    [uiWebView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
    [uiWebView setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleBottomMargin];
    uiWebView.keyboardDisplayRequiresUserAction = NO;
    uiWebView.backgroundColor = [UIColor clearColor];
    [uiWebView.scrollView setShowsHorizontalScrollIndicator:NO];
    uiWebView.delegate = self;

    self.njkWebViewProgress = [[NJKWebViewProgress alloc] init];
    uiWebView.delegate = _njkWebViewProgress;
    _njkWebViewProgress.webViewProxyDelegate = self;
    _njkWebViewProgress.progressDelegate = self;
    _currentWebView = uiWebView;
}

WebView 最关键的地方就是能捕获到前端资源的请求,UIWebView 的捕获方法是 webView:shouldStartLoadWithRequest:request navigationType:,WKWebView 的捕获方法是 webView:decidePolicyForNavigationAction:decisionHandler:,同时 WebView 有完整的生命周期回调(start,finish,fail等)。

#pragma mark - UIWebViewDelegate- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    self.currentRequest = request;
    BOOL result = [self callback_webViewShouldStartLoadWithRequest:request navigationType:navigationType];
    return result;}......#pragma mark - WKNavigationDelegate- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    self.currentRequest = navigationAction.request;
    BOOL result = [self callback_webViewShouldStartLoadWithRequest:navigationAction.request
                                      navigationType:navigationAction.navigationType];
    if (result) {
        decisionHandler(WKNavigationActionPolicyAllow);
    } else {
        decisionHandler(WKNavigationActionPolicyCancel);
    }}......#pragma mark - YZWebViewCallback- (BOOL)callback_webViewShouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(NSInteger)navigationType {
    BOOL result = YES;
    if ([self.delegate respondsToSelector:@selector(webView:
                                                    shouldStartLoadWithRequest:
                                                    navigationType:)]) {
        if (navigationType == -1) {
            navigationType = UIWebViewNavigationTypeOther;
        }
        result = [self.delegate webView:self shouldStartLoadWithRequest:request navigationType:navigationType];
    }
    return result;}......#pragma mark - NJKWebViewProgressDelegate- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress {
    self.estimatedProgress = progress;
    if (_progressDelegate && [_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) {
        [_progressDelegate webViewProgress:webViewProgress updateProgress:_estimatedProgress];
    }}

需要强调的一点是:(UIWebView 的捕获方法是 webView:shouldStartLoadWithRequest:request navigationType:,WKWebView 的捕获方法是 webView:decidePolicyForNavigationAction:decisionHandler)这两个方法只能控制一个请求可不可以被 WebView 发出,比如 Bridge 就可以在这层进行捕获,但是并不可以做请求定制的功能。请求的定制需要借助 NSURLProtocol。

Bridge设计

Hybrid APP 的交互无非是 Native 调用前端页面的 JS 方法,或者前端页面通过 JS 调用 Native 提供的接口,两者交互的桥梁皆 Webview:

通过调研,前端可以通过在 DOM 注入 iframe 发起 Bridge 请求,该请求可以被 webView:shouldStartLoadWithRequest:request navigationType: 方法捕获,从而执行相应的操作,但是属于异步操作;还有一种前端可以通过 Ajax 发起 Bridge 请求,可以有同步异步两种方式,不过在 WebView 这层捕获不到此请求,只能通过 NSURLProtocol 拦截,所以这也是 WKWebView 的一个限制。

WebViewJavascriptBridge是一个不错的JavaScript与Native之间双向通信的库,多个厂家包括Facebook在使用,并且新的版本开始支持WKWebView,对了解Native与JS的交互非常有帮助。

Bridge 设计至关重要,设计的好坏对后续开发、前端框架维护会造成深远的影响,并且这种影响往往是不可逆的,所以这里需要前端与 Native 好好配合,提供通用的接口。对于一个公司来说,往往一套底层框架需要服务于多条业务线、多个 APP,这就需要在设计的时候考虑好哪些桥接可以在框架层实现(比如跳转 Web 页面,设置数据,获取数据,Back 事件,Close 事件,Alert 弹框,获取定位等等),而与业务相关的桥接需要框架提供接口让业务方去注册(比如跳转 Native 页面,授权跳转等等)。

首先设计数据格式,根据 URL 格式:

scheme://host/path?query

与前端约定请求的格式是:

hybrid_scheme://hybrid_api?hybrid_params={params need encode}&callback=callback_ID

客户端需要根据约定,在 Bridge 处理结束后通过 WebView window 对象中的 callback_ID 调用回调,数据返回的格式约定为:

{ data : {}, err : 0, //非0提示msg msg : "success or fail message" }

Native 解析 Bridge 代码逻辑:

#pragma mark - YZWebViewDelegate

- (BOOL)webView:(YZWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

    ......

    if ([_jsBridge webViewShouldStartLoadWithRequest:request navigationType:navigationType]) {
        //符合桥接规则
        return NO;
    }

    return YES;
}

- (BOOL)webViewShouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    if ([self isWebViewJavascriptBridgeURL:url]) {
        if ([self isWebViewJavascriptBridgeHost:url.host]) {
            YZURLParseModel* model = [self p_parseWebViewRequestWithURL:url];
            if ([[_messageHandlers allKeys] containsObject:model.method]) {
                YZJSBridgeHandler messageHandler = _messageHandlers[model.method];
                if (messageHandler) {
                    messageHandler(model.params);
                }
                .....
            } else {
            ......
            }
        }
        return YES;
    } else {
        return NO;
    }
}


- (void)registerHandler:(NSString *)handlerName handler:(YZJSBridgeHandler)handler {
    if (![_hostsArray containsObject:handlerName]) {
        [_hostsArray addObject:handlerName];
    }
    _messageHandlers[handlerName] = [handler copy];
}

//业务注册桥接接口
static NSMutableDictionary* vocationalAsyncJSBridge = nil;

+ (void)setVocationalJSBridgeWithHandler:(NSString *)handlerName handler:(YZJSBridgeHandler)handler {
    if (!vocationalAsyncJSBridge) {
        vocationalAsyncJSBridge = [[NSMutableDictionary alloc] initWithCapacity:1];
    }
    vocationalAsyncJSBridge[handlerName] = handler;
}

+ (void)setVocationalJSBridge:(NSMutableDictionary*)dic {
    if (!vocationalAsyncJSBridge) {
        vocationalAsyncJSBridge = [[NSMutableDictionary alloc] initWithCapacity:1];
    }
    [vocationalAsyncJSBridge addEntriesFromDictionary:dic];
}

公共 Bridge 设计

  • 跳转 跳转包括三类: ① 页面内跳转,无需走 hybrid。 ② H5 跳转新开 WebView 页面。 ③ H5 跳转 Native 页面。

H5 跳转新开 WebView 页面:

协议标准 hybrid_scheme://gotoWebview?params={url:~}
__weak typeof(self) weakSelf = self;
//gotoWebview桥接
    [self.jsBridge registerHandler:@"gotoWebview" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(gotoWebPageWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate gotoWebPageWithDatas:data])  //业务子类进行拓展
                return;
        }
        if ([data isKindOfClass:[NSDictionary class]]) {
            NSString* url = [data objectForKey:@"url"];
            if (url) {
                YZWebViewContainerViewControllerBase* vc = [[strongSelf getCurrentViewController] routeWithParams:@{@"url" : [NSURL URLWithString:url]}];
                [strongSelf.navigationController pushViewController:vc animated:YES];
            }
        }
    }];

H5 跳转 Native 页面:

协议标准 hybrid_scheme://gotoNative?params={page:~}__weak typeof(self) weakSelf = self;//gotoNative桥接
    [self.jsBridge registerHandler:@"gotoNative" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(gotoNativeWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate gotoNativeWithDatas:data]) //业务子类进行拓展
                return;
        }
    }];
  • 功能 API 例:Back 事件、Reload 事件、Share 方法等。
协议标准 hybrid_scheme://doAction?params={action:back}协议标准 hybrid_scheme://doAction?params={action:reload}协议标准 hybrid_scheme://doAction?params={action:share, title:, subtitle:, context:, imgUrl:}//doAction桥接
    [self.jsBridge registerHandler:@"doAction" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(doActionWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate doActionWithDatas:data]) //业务子类进行拓展
                return;
        }
        if ([data isKindOfClass:[NSDictionary class]]) {
            NSString* action = [data objectForKey:@"action"];
            if (action) {
                if ([action isEqualToString:@"back"]) {
                    [strongSelf p_back];
                } else if ([action isEqualToString:@"page_reload"]) {
                    [strongSelf p_reload];
                } else if ([action isEqualToString:@"share"]) {
                    [strongSelf p_shareWithParams:data];
                }
            }
        }
    }];
  • Header 组件设计 对于 Header 组件,需要完成以下功能: ① Header 的左侧具有返回键和关闭键(类似微信等 APP),右侧可配置文字和图标,并且可以控制回调。 ② Title 通常在 WebView 加载完成后去获取 document.title 来显示,这里可以做到可配置。 ③ Title 可以设置一些特别的 TitleView,比如 SegmentView、ListView 等等。

以设置右侧按钮为例:

协议标准 hybrid_scheme://configNative?params={configs:[{type:nav_item_right, title:, icon_url:, action:, action_parameters: }]}//configNative桥接
    [self.jsBridge registerHandler:@"configNative" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(configNativeWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate configNativeWithDatas:data])  //业务子类进行拓展
                return;
        }
        if ([data isKindOfClass:[NSDictionary class]]) {
            NSString* strOfConfigs = data[@"configs"];
            if (strOfConfigs == nil || [strOfConfigs isEqualToString:@""])
                return;
            NSError *error = nil;
            NSArray *arrConfigs = [NSJSONSerialization JSONObjectWithData:[strOfConfigs dataUsingEncoding:NSUTF8StringEncoding]
                                                                  options:NSJSONReadingAllowFragments error:&error];
            if (error || !arrConfigs)
                return;

            for (NSDictionary* config in arrConfigs) {
                NSString *strType = config[@"type"];
                if (!strType || [strType isEqualToString:@""])
                    continue;
                if ([strType isEqualToString:@"nav_item_right"]) {
                    YZNavigationItemConfigModel *model = [[YZNavigationItemConfigModel alloc] initWithDictionary:config error:&error];
                    if (error) {
                        error = nil;
                        continue;
                    }
                    [strongSelf.rightMenuBarButtonItems addObject:[strongSelf p_configRightBarButtonItemsWithModel:model]];
                    if (strongSelf.rightMenuBarButtonItems) {
                        [strongSelf.navigationItem setRightBarButtonItems:strongSelf.rightMenuBarButtonItems];
                    }
                }
            }
        }
    }];

总结

Hybrid 框架依靠快速迭代,快速试错在业务开发中使用非常广泛。本文初衷是想为那些准备使用Hybrid框架的人提供设计上的思路,并通过实际的事例去展示结果,希望对 Hybrid 感兴趣的朋友一起来把 Hybrid 一整套解决方案落地并且能够提供开源。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端黑板报 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Hybrid APP 特点
  • Hybrid APP 框架
  • UIWebView 和 WKWebView 兼容
  • Bridge设计
  • 公共 Bridge 设计
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档