作者:翁阳 (沪江iOS架构师)
本文原创,转载请注明作者及出处。

继上一篇 Thinking 以来过去太久时间了,这些日子里一直在疲于奔命,无论如何繁忙,静下来写写博客还是一件非常值得的事情。

本篇我们来谈谈UIWebView,虽然在 iOS 8.0 之后更加推荐使用WKWebView,但在你没放弃 iOS 7.0 之前,不妨看看如何让这陈旧的UIWebView更加好用些,当然这里的一些思想同样可以迁移到WKWebView中。

用 WebView 的动机

在深入这个话题之前,我们先静下心来想想,一般在什么情况下我们会想到使用WebView?作为大前端名副其实的粘合剂,WebView无疑是移动端和触屏端最直接的连接方式。那么这个答案就很简单了,我们要接入触屏页面(HTML5),那为什么我们要接入触屏页面呢?或许是因为下面这些点:

  • 跨平台,减少开发资源
  • 热更新,避免发版等待
  • 多团队研发,集成成本低
  • 作为平台扩展,开发者门槛低

作为一个能够将触屏页很好的嵌入的WebView而言,并非简单的加载一个URL就足够的,我们一直都在致力于将触屏的体验和原生拉进,所以,原生和触屏之间的交互必不可少。

浅谈 JS && Native 交互

如何在UIWebView实现JavascriptNative的相互调用呢?目前大致有两种方案:JSBridgeJSCore

JSBridge

JSBridge实现的原理其实很简单,核心依赖于UIWebViewUIWebViewDelegate的两个方法:

  1. stringByEvaluatingJavaScriptFromString:
  2. webView:shouldStartLoadWithRequest:navigationType:

第一个方法用于Native调用Javascript方法,实现了原生调用触屏这一条道路。那么触屏如果想调用原生,可以使用iframe构建一个load请求,原生端通过第二个回调方法进行拦截,根据URL中所传递的信息重定向到特定的原生方法,虽然有点绕,但这条道路依然是行通了。

对于此方法,已经有相关比较好的开源库,比如 WebViewJavascriptBridge

JSCore

JSCore也就是WebKit中的JavascriptCore,这是一个非常直接的互通方式。最核心的类是JSContext,通过这个上下文,我们可以注入一个 OC 对象到 JS 的运行环境中,我们也可以通过这个上下文直接调用 JS 方法。具体就不展开,可以参考JavascriptCore的 API 文档。

如何组织好代码

在给UIWebView增加了很多业务相关功能后,常常会发现这个Controller会变得很庞大,又或是出现了很多类似的Controller,这通常会给后续的维护带来困扰。那么如何更好的来组织这些代码呢?

在面向对象的程序设计里,针对结构优化上通常有两个方向的思维:提取间接层拆解再组合,当然这是我总结出来的思维方式。提取间接层是一种纵向的维度,通常包括:

  • 提取公共基类
  • 增加 Mediator,隔离依赖

而“拆解再组合”是一种横向的维度,将一个大的代码结构进行模块划分,最后再进行组合。可以看我之前的这篇文章:《组合化繁为简的力量》

面对不同的结构问题,不同的人优化的方式也会有所不同,我更加倾向于“拆解再组合”。过多的层次很容易导致调用路劲过长,我觉得这并不易于理解,另外面向对象中常说组合胜于继承,这其中的缘由也就不展开了。

一个轻量、无侵入的方案

KakiWebView,一个简单易读的封装,核心思想便是采用了拆解再组合,以下是我设计的初衷:

  • 对于现有的UIWebView无侵入性的使用
  • 可扩展性强,可实现自定义扩展
  • 简单易用,学习成本低

项目地址:https://github.com/prinsun/KakiWebView

无论你现在的UIWebView是以何种方式使用的,都可以轻而易举的使用KakiWebView来获得扩展能力,该项目中已经内置了一些Plugin

  • KakiJavascriptCorePlugin:可注入 OC 对象到Javascript环境中,使用上JSCore的交互方式
  • KakiProgressPlugin:基于NJKWebViewProgress的一个进度条插件,安装该插件后UIWebView加载页面时将会带有进度显示
  • KakiPopGesturePlugin:屏幕边缘侧滑返回的插件,模仿 Safari 的回退效果
  • KakiTitleObserverPlugin:监控网页 Title 变化的插件,可以实时获取到网页标题变化

这些内置的插件非常通用,也是大多项目中所需要的,使用的方式非常简单:

// 启用 Kaki
[self.webView setEnableKakiPlugins:YES];

// 安装 Kaki 插件
[self.webView installKakiPlugin:[KakiProgressPlugin.alloc init]];
[self.webView installKakiPlugin:[KakiPopGesturePlugin.alloc init]];
[self.webView installKakiPlugin:[KakiTitleObserverPlugin.alloc init]];

// 配置插件
__weak __typeof(self) wself = self;
[self.webView.titleObserverPlugin setOnTitleChanged:^(NSString *title) {
    wself.titleLabel.text = title;
}];
self.webView.progressPlugin.progressColor = [UIColor redColor];

内置的这些插件是你学习如何自定义插件的很好示例,详细请阅读源码。

构建适合你的容器

当然 KakiWebView 绝对不能满足你所有的业务需求,而这个项目的初衷也并不是要完成一个大而全的万能WebView,正确的定位是一个提供了扩展UIWebView能力的基础组件。通过这样一个基础组件,自定义符合你业务需求的Plugin,从而构建一个真正符合你所期望的容器。

如果你并不喜欢JSCore的交互方式,你完全可以自定义一个Plugin来实现JSBridge的交互方式。另外可以通过自定义一个Plugin配合NSURLProtocol,实现离线浏览这样的功能。总而言之,有了这样的一个基础组件,无论是从代码的组织还是后续的扩展,都有很大的帮助。

那么,放飞思想,重构又或是去构建一个更强大的 Web 容器吧!