[17年跨领域学习]从 WKWebview 再谈混合开发 | 掘金技术征文

2,290 阅读9分钟
作者:滴滴公共前端团队 - 小春

前言

首先祝福各位同学新年快乐,17 年我们在大前端领域讨论点什么呢?

这个问题我相信很多公司的前端负责人都会思考。这里不作预言,只是带着前端同学们实实在在地一起再来深入地看看混合开发

之前我们会强调前后端交互的重要性,比如:

1、 POST 请求的 Content-Type 是 application/json 还是 application/x-www-form-urlencoded,因为对应后端语言(PHP、Go、java 等) 获取数据的方式会有差异。

2、再比如不同后端处理跨域的方式。

那类似的其实也有很多人也关注到前端和客户端交互的重要性,毕竟现在的 APP 内嵌开发频度要比传统的 Web 网站多。

我们以 WKWebview 这个切入点来谈谈所谓的混合开发。

注释

1、本文中会穿插大量的 iOS 代码和词汇,而且也会对 iOS 代码风格和规范进行标注,来方便前端同学了解 iOS 的部分细节实现。

2、本文更多是从一个前端 iOS 客户端的双重角度去看待混合开发。

如有错误请指正。

正文

很简单的问题:我们以往的前端代码都在什么环境(容器下)运行?

答案便是:浏览器(包含 PC 的各种浏览器以及手机上的浏览器)以及我们先要介绍的 WebView

PC 浏览器内核的移植

对硬件原生 API 支持和 Webkit 特性都支持不够

所以很多混合开发解决方案的第一个点就是:

做一个增强版本的 WebView

还有人记得 PhoneGap 吗?

很多我们早期的混合开发者基本都听过或者用过它,尤其被前端同学喜欢:

因为开发都是用我们擅长的前端技术开发 App

但是相比客户端开发同学会发现它有很多诟病:

因为它不是原生和前端混合使用

在 iOS 中内部为 UIWebView

用于 iOS 网络视图加载网页

它有哪些能力?

1、指定一个在线网页地址,通过 NSURLRequest 类创建一个网络请求

配合 UIWebViewloadRequst 来进行网络视图的加载

代码实例

- (void) viewDidLoad {

[super viewDidLoad];

UIWebView *webview = [[UIWebView alloc] initWithFrame:self.view.frame];

NSURL *url = [NSURL URLWithString:@"https://zhuanlan.zhihu.com/ddfe-weekly"];

NSURLRequst *request = [NSURLRequest requestWithURL:url];

[webView loadRequest:request];

[self.view addSubview:webview];

}

给前端同学的注释:

// 语法内容

- :方法里面的加号和减号,减号一般是对象方法

viewDidLoad :视图的生命周期方法,和我们前端的 onLoad 或者 jq 里面的 ready 一样,初始化用到

NSURL - 类似我们的 Location 对象,能解析 URL

// 代码风格规范

1、使用 4 个空格缩进,和前端开发规范一样

2、方法的书写:

* - 和 (void) 有一个空格,第一行结束的 { 在当前行的末尾

PS:据说有一些公司的 C 语言规范是第一个大括号独占一行

2、UIWebView 类也支持加载 HTML 文件来实现远程下载或者本地离线加载

通过 UIWebViewloadHTMLString

注释:HTML 字符串引号需要转义

3、代理(Delegate)

在哪定义当前视图状态呢?

就是:UIWebViewDelegate

一般我们会定义 **ViewController 类

4、NSData 加载

一般针对图片资源加载。

给前端同学的注释:

NSData 应用于文件读取,可以设置缓冲区

NSData 是不变缓冲区

NSMutableData 是可变缓冲区

5、我们看看 UIWebView 源码里面都定义了哪些属性和方法:

注释:iOS 9.3 UIKit UIWebView.h

PS:

苹果内部对 WebView 有缓存机制,部分打开过的资源第二次访问的时候都会尝试本地读取,但是不太稳定,关掉之后,系统会清理它。

转折点来了,这个事情也使得很多一部分同学认识到一个新词:

17年 1 月 6 号,微信团队在公众号发文:

微信 iOS 客户端将于 3 月 1 日逐步升级为 WKWebView 内核

WKWebView 又是什么?

苹果支持最新的 Webkit 功能

从 iOS 8 开始引入的网页浏览控件(组件)

-- 高性能的 Web View 解决方案

好像是救世主?

1、运行消耗的内存明显减小:App 启动更快、稳定性更高

2、最新的 Web 标准

3、高达 60 fps 的滚动刷新率,内置手势探测

等等

H5 和 APP 交互方式变了?

大部分的人都会提到 jsbridge 这个词,那真正的内涵是什么呢?

WebViewJavascriptBridge

一个解决 OC 和 Javascript 通讯的 bridge 框架

An iOS/OSX bridge for sending messages between Obj-C and JavaScript in WKWebViews, UIWebViews & WebViews.

那原理到底是什么?

1、OC 通过 WebView 的 stringByEvaluatingJavaScriptFromString 来调用 js

2、js 调用 OC:

* iOS7 引入了 JavaScriptCore,可以初始化一个 JSContext 对象,然后约定好一个方法名就好了。

* 一般也可以通过私有协议 Scheme,客户端会拦截指定的协议

* 还有人也提到轮询,但个人感觉这种方式在一般业务场景并不是很多,除了个别特定场景,而且客户端开销也大

注释:WebView 渲染是独立线程,所以 js 代码实际是异步的

说了这么多,我们看看源码(pod 版本 6.0.2):

// WebViewJavascriptBridge/WebViewJavascriptBridge.m #103

- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {

return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];

}

// WebViewJavascriptBridge/WebViewJavascriptBridge.m #178

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

//...

}

当然细心的同学会发现,它也支持 WKWebViews,我们在源码包里面也看到了文件:

我们也都知道在 H5 里面可以通过私有协议来唤起 App 以及细化到某个页面。

对于 WKWebView 呢?

最早在 stackoverflow 上有一篇 Q:

《WKWebView and NSURLProtocol not working》

里面也提到:

When using the old UIWebView you could catch the requests by implementing a custom NSURLProtocol. I use this to handle requests that requires authentication.

I tried the same code and it doesn't work with the new WKWebView but my protocol class isn't called at all.

我们看看回答:

WKWebView makes requests and renders content out-of-process, meaning your app does not hear the requests they make.

If you are missing a functionality, now is the time to open a bug report and/or an enhancement request with Apple.

As of iOS 10.3 SDK, WKWebView is still unable to make use of custom NSURLProtocols using public APIs.

当然后面也贴了:

Enterprising developers have found an interesting method: +[WKBrowsingContextController registerSchemeForCustomProtocol:]

It supposedly adds the provided scheme to a list of custom protocol handled schemes and should then work with NSURLProtocol.

所以大部分的混合方案都是从入口 URL 拦截

这里有几个区别

WKWebView 拦截 decidePolicyForNavigationAction 方法

我们可以在上面提到的 WebViewJavascriptBridge 的 WKWebViewJavascriptBridge.m 源码文件可以看到:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

//...

}

UIWebView拦截 shouldStartLoadWithRequest 代理方法(这个前面也提到了)

那很多人肯定会问:切换到这个新的有什么风险吗?

1、看看是否页面有适配问题

2、看看 jsbridge 是否有错误

3、看看是否有崩溃

个人觉得这些风险都是可控的,毕竟也会经历一个灰度的过程。部分同学也看过腾讯 Bugly 之前 1 月份发的《WKWebView 那些坑》的文章,里面也提到了几点和前端有关系的:

  • WKWebView 是多进程组件:Network Loading 和 UI Rendering 在其他进程执行。在一些 WebGL 渲染的复杂页面总体内存占用也不低,过大的似乎也会 crash 导致白屏

  • 页面适配问题:比如调用 window.innerHeight 导致页面被拉伸

  • 视频自动播放的设置

  • goBack 回退上一页不触发 onload,也不会执行 js

  • Cookie 存储的问题:WKWebView 发起的请求不会自动带上存储在 NSHTTPCookieStorage 容器的 Cookie;而且存储时机会有延时。

这个就导致我们之前的前端统一登录组件换了一种方案去 hot fix,更多客源查看文末的原文链接。

我们提一下豆瓣混合开发框架:Rexxar (之前也邀请来滴滴分享过一次)

它主要分 3 个部分:

  • Rexxar Web

  • Rexxar IOS

  • Rexxar Android

本文我们重点看一下 iOS 的 Rexxar Container

容器 -- 其实就是一个内置的 WebView

但是增加了原生的一些功能支持:图片缓存、Native UI 的调用等

说了这么多,我们看看源码:

//rexxar-ios/Rexxar/Core/RXRViewController.h

/**

* 内置的 WebView。

*/

@property (nonatomic, strong, readonly) UIWebView *webView;

那很多专业的人要吐槽了:

Rexxar 采用了原生的 WebView,是对 App 体积没影响

但是之前很多个 WebView 带来的内存问题也同样存在

-- 这个是滴

同样的 Rexxar Container 和 Web 如何交互呢?

前面我们介绍了 iOS 一般采用 WebViewJavascriptBridge,但它这里不是,采用发送 HTTP 请求(套路基本大家都是采用 iframe 加载特殊约定的 URL),然后 Container 来拦截。其实类似 Proxy,Web 发出的请求都会被 Proxy 处理一下。

那这里好像没有用到 WKWebView,为什么呢?

这个我们之前也在分享的时候请教过豆瓣的同学,他们也尝试过,毕竟我们前面介绍了那么多 WKWebView 的好处,大致的结论:

和他们的设计冲突

NSURLProtocol 无法截获 WKWebView 中的请求

当然之前也听过美团大众点评的 Hybrid 方案分享,他们的流程基本也类似:

一个 URL 请求在客户端发起,有一个 Router 来查询本地的路由配置表(这个配置表是 App 从后台路由配置服务拉取的),根据对应的规则去跳转到 H5 还是 Native.

那如何评估一个混合应用的好坏呢,一般几个维度:

1、开发效率高

其实大家发现后面的混合方案基本的初衷都是利用前端一些优秀的地方:模块化,组件化,工程化。

当然客户端和前端在协调开发的时候也有一些效率工具:比如客户端加载一个前端的 demo 页面,同时给前端打一个模拟器安装包,以及类似 RN 这种 debugger 调试。

2、缓存带来的加载快,资源文件可以本地化,而且我们可以灵活配置化的管理缓存

  • 稳定

1、js 错误可以通过 WebView 来捕获,然后通过 App 日志发送服务端来展示

2、WebView 的 Crash 也可以采用 fabric 这些来收集

总结

17 年我们会放出更多跨领域的内容,来提升前端同学的事业,在互相协作的技术解决方案实施过程中知其所以然。

加油 & 再次新年快乐。

同时感谢:滴滴 iOS 高工文杰老师对 iOS 代码的指导

扩展阅读:

github.com/marcuswesti…

www.infoq.com/cn/news/201…

mp.weixin.qq.com/s/rhYKLIbXO…

征文活动:

掘金年度征文 | 2016 与我的技术之路


欢迎关注DDFE
GITHUB:github.com/DDFE
微信公众号:微信搜索公众号“DDFE”或扫描下面的二维码