JavaScriptBridge

跟Native开发相比, 网页开发虽然在用户体验上跟Native有不小的差距, 但其在开发周期、更新方式上却有不可比拟的优势。有时为了快速上线新的业务或动态支持第三方的业务,App中不可避免的会使用webview。一般情况下, webview或多或少的都会用到cookie、定位、支付、分享等信息或功能,而这些功能却是Native; 另一方面,统一UI交互:选择列表、信息提示等对提升用户体验也非常有帮助,直接使用Native显然又要比使用web来模拟Native的UI要简单的多。综合来看,webview调用Native模块完成部分交互是必不可少的,那么,如何做到呢?

Native vs Web

App开发的同学们都知道从产品体验的角度出发,webview绝不是App的第一选择,跟Native相比webview在产品体验上有下面几个方面的不足:

  1. 页面渲染慢。网页的资源加载、UI位置计算、渲染、逻辑运算等都是单线程,无法比拟Native多线程
  2. 流畅性不够好。eg. 上下滑动页面时, Native的滚动流畅性要远高于WebView,可以拿两个列表页试试
  3. 手势的支持比较少。webview支持的手势要比Native少,比如多点触控、ShakeGesture等
  4. 很难做到UI的一致。用户首先对系统原生的UI控件比较熟悉,App的开发大都基于这些基本的控件。使用webview时,由于不能直接使用很多Native原生控件,而用JS实现的控件跟原生又有差异,所以UI的一致性是大有折扣的。即使Web控件的样式跟原生差别很小,iOS 每年的系统更新带来的UI跳转就会破坏这些花大力气实现的UI

但从业务的角度出发,有时我们不得不选择使用webview,这主要基于以下几个方面的考虑:

  1. webview开发周期短,可以随时上线更新
  2. 出于发版的考量,一些业务功能Native开发的周期过长,需要webview接入快速上线
  3. 有些服务来自第三方,App开发者本身不需要支持这些服务, 使用webview展示第三方服务

OC中的webview

UIWebView始于iOS2.0(WKWebView iOS8.0), 网页跟Native交互的API非常简单:

  1. 通过delegate, 可以监听网页的load事件(startload, finishload, error)
  2. 调用 stringByEvaluatingJavaScriptFromString:,可以让网页执行一段JS代码, 返回值为字符串

OC与JavaScript

iOS7.0之前,stringByEvaluatingJavaScriptFromString: 是OC执行JavaScript代码的唯一方式; iOS7.0的发布带来了JavaScriptCore Framework,这是对WebKit’s JavaScript Engine的一个封装,无需借助UIWebView 就可以直接以Native的方式执行JavaScript代码。

1. JavaScript in String

1
2
3
4
JSContext *context = [[JSContext alloc] init];//新建一个JS的执行环境, 可建多个互不影响的Context
[context evaluateScript:@"var num = 10 + 1;"];
[context evaluateScript:@"function sum(a,b){return a+b;}"];//在context中定义函数sum
JSValue *sumVal = [context evaluateScript:@"sum(num, num)"];//调用sum函数,参数为num(11)

2. JavaScript in Native

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  //以Native的方式定义一个函数,可以在JS中直接调用, 其实就是把一个Native函数导出到JSContext
//方法有两种:
// 1. 以block方式定义一个函数并赋值给context中的一个变量
// 2. 把实现JSExport协议的对象赋值给context中的一个变量
context[@"makeNSColor"] = ^(NSDictionary *rgb) {//方式1
float r = rgb[@"red"].floatValue;
float g = rgb[@"green"].floatValue;
float b = rgb[@"blue"].floatValue;
return [NSColor colorWithRed:(r/255.0) green:(g/255.0) blue:(b/255.0) alpha:1.0];
}
//================== 方式2 ===========================
//MTNBExport.h
@protocol MTNBExport <JSExport>
- (void)handleMessageFromJs:(NSString *)message;
- (void)logFromJs:(NSString *)message;
@end
//MTNBForwarder.h
@interface MTNBForwarder:NSObject<MTNBExport>
- (void)handleMessageFromJs:(NSStrig *)message;
- (void)logFromJs:(NSString *)message;
@end
//使用
context[@"_MTNB"] = [[MTNBForwarder alloc] init];//JS中:_MTNB.handleMessageFromJs可以直接调用

【注】查看JSExport的文档

两种JavaScriptBridge的实现

1. 开源项目 WebViewJavaScriptBridge

这是个著名的开源项目,几乎不限制iOS的系统版本,实现方式比较简单但逻辑却比较绕, 核心的思想就是JavaScript通过load事件给Native发送消息, 之后Native通过stringByEvaluatingJavaScriptFromString: 完成通信,下图可以描述核心思想:

Alt text

2. 美团项目 MTNB

MTNB的实现基于JavaScriptCore, 相比于上一个开源项目, MTNB的实现更加简单、优雅、自定义功能扩展更加方便,另外,MTNB基于安全考虑,网页端加入了鉴权部分, 使用可以参考 官方原理接入备忘。MTNB需要Native跟JS的配合, 下边是MTNB的的实现描述:

[JavaScript Code]
1
2
3
4
5
6
7
8
9
//定义一些变量...
function send(msg, callback) {
// 保存callback, 把callbackId写入msg
_MTNB.handleMessageFromJs(JSON.stringify(msg));//直接调用Native方法
}
function runCallback(response) {
// 获取并删除response.callbackId对一个的callback
callback(response);//执行回调
}
[Object-C code]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//MTNBExport.h
@protocol MTNBExport <JSExport>
- (void)handleMessageFromJs:(NSString *)message;
@end
//MTNBForwarder.h
@interface MTNBForwarder:NSObject<MTNBExport>
@property (nonatomic, weak) UIWebView *webView;
- (void)handleMessageFromJs:(NSStrig *)message;
@end

//通过JSExport的方式, 把Native方法注册到JSContext中
//webView:didCreateJavaScriptContext:forFrame: 被调用是,完成JSExport对象注入

context[@"_MTNB"] = [[MTNBForwarder alloc] init];//JS中:_MTNB.handleMessageFromJs可以直接调用

//handleMessageFromJs执行后, 如果有JS回调, 则提取callbackId 和 response
//调用 stringByEvaluatingJavaScriptFromString:
//
[webView stringByEvaluatingJavaScriptFromString:"runCallback(XXX)"];//xxx为字符串化后的参数

后记

WKWebView作为UIWebView的替代者, 随着iOS8.0的发布浮出水面。跟UIWebView相比, WKWebView渲染网页的速度更快,提供的接口也更加丰富,但这并不意味着我们可以放弃Bridge,相反,当App支持的iOS版本提升到8.0的时候,要考虑同WKWebView替换UIWebView升级Bridge