论 iOS 开发中 JS 与 Native 的交互方式

前言

在 iOS 开发中,JS 与 Native 的交互分为两种,第一种是 Native 调 JS,即通过在 Native 代码中执行 JS 达到在 webkit 控件中展现相应 JS 代码的效果;另一种就是 JS 调用 Native ,通过 web 前段 JS 的执行来调用 Native 本地的方法,用以实现例如开启照相机、数据持久化等等只能通过 Native 代码实现的效果。

目前进行 JS 和 Native 交互主要有两种方式,下面进行一一介绍:

一、WebView 方法/代理方法

通常来说,iOS 中实现加载 web 页面主要有两种控件,UIWebView 和 WKWebview,两种控件对应具体的实现方法不同,我们在这里分开进行介绍:

UIWebView控件

  • Native 调用 JS:

在 Native 中执行 JS 语句非常简单, JS 作为脚本语言它的执行需要解释器的存在,即浏览器,所以 UIWebView 作为浏览器控件,提供了 native 调用 JS 的对象方法:

//script 是要执行的 JS 语句
//返回值为 JS 执行结果,如果 JS 执行失败则返回 nil,如果 JS 执行没有返回值,则返回值为空字符串
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

这里编写了一个 demo 仅供参考:

- (void)webViewDidFinishLoad:(UIWebView*)webView
{
    NSString* str = [self.webView stringByEvaluatingJavaScriptFromString:@"pageDidLoad()"];
    NSLog(@"%@", str);
}

当 WebView 加载完毕的时候调用 JS 中的 pageDidLoad 方法,并在控制台打印 JS 的执行结果。

  • JS 调用 Native:
    使用 WebView 方法/代理方法完成 JS 调用 Native 要稍微复杂一点,需要 Native前端和 web 前端的良好配合,主要原理是通过 UIWebVIew 的代理方法截取 web 前端的跳转请求,通过识别与 web 前端约定好的自定义协议头来判断本次请求是否为 JS 调用 Native 的请求,来调用对应的 Native 方法。
    其中涉及到的 UIWebView 代理方法为:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

下面通过例子来进行演示:

JavaScript 代码:

function btnOnClickBaidu() {
        var url = "http://www.baidu.com";
        alert("马上跳转的页面是:" + url);
        window.location.href = url;
    }
    function btnOnClickNative() {
        var url = "DZBridge://printSomeWords";
        alert("马上跳转的页面是:" + url);
        window.location.href = url;
    }
    function btnOnClickNativeWithConfig() {
        var url = "DZBridge://printSomeWords?{\"string\":\"Hello World\"}";
        alert("马上跳转的页面是:" + url);
        window.location.href = url;
    }
    function pageDidLoad() {
        alert("页面加载完毕!");
        return 11;
    }

OC代码:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    //dzbridge 为约定好的协议头,如果是,则页面不进行跳转
    if ([request.URL.scheme isEqualToString:@"dzbridge"]) {
        //截取字符串来判断是否存在参数
        NSArray<NSString*>* arr = [request.URL.absoluteString componentsSeparatedByString:@"?"];
        if (arr.count > 1) {
            NSString* str = [arr[1] stringByRemovingPercentEncoding];
            NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];
            NSLog(@"%@", dict[@"string"]);
        }
        else {
            NSLog(@"没有参数的打印");
        }
        return NO;
    }
    //不是自定义协议头,跳转页面
    return YES;
}

WKWebView控件

iOS8 以后,苹果推出了新框架 WKWebKit, 其中提供了可以替换 UIWebView 的组件 WKWebView。原来 UIWebView 的各种问题得到了改善,速度更快了,占用内存少了(模拟器加载百度与开源中国网站时,WKWebView 占用23M,而UIWebView 占用85M),目前来看,WKWebView 是 App 内部加载网页更佳的选择!
WKWebView 相对 UIWebView 做了较大幅度的重构,将 UIWebViewDelegate 与 UIWebView 重构成了14类与3个协议,因此,在 WKWebView 中进行 JS 与 Native 的交互与 UIWebView 相比也有较大的不同。

  • Native 调用 JS:
    在 WKWebView 中 Native 调用 JS 的方式与 UIWebview 中比较相似,也是通过自己本身的一个对象方法:
// javaScriptString 为待执行的 JS 语句
// completionHandler 为执行 JS 完毕后的回调,block 的第一个参数为执行结果,第二个参数为错误
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

看下面一个小例子:

#pragma mark----- WKNavigationDelegate -----

- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
{
    [self.webView evaluateJavaScript:@"pageDidLoad()" completionHandler:^(id _Nullable value, NSError* _Nullable error) {
        NSLog(@"%@", value);
    }];
}
  • JS 调用 Native:
    WKWebView 中 JS 调用 Native 与 UIWebView 有着比较大的不同,首先需要介绍几个类(/协议/属性):
  1. WKWebViewConfiguration:是 WKWebView 初始化时的配置类,里面存放着初始化 WK 的一系列属性;
  2. WKUserContentController:为 JS 提供了一个发送消息的通道并且可以向页面注入 JS 的类;
  3. WKScriptMessageHandler:一个协议,协议中只有一个方法,这个方法是页面执行特定 JS 的一个回调,这个特定的 JS 格式为:window.webkit.messageHandlers.<name>.postMessage(<messageBody>)

WKWebViewConfiguration作为 WK 的配置类,其中有一个属性为

@property (nonatomic, strong) WKUserContentController *userContentController;

WKUserContentController的一个实例,WKUserContentController有一个对象方法为:

/*! @abstract Adds a script message handler.
 @param scriptMessageHandler The message handler to add.
 @param name The name of the message handler.
 @discussion Adding a scriptMessageHandler adds a function
 window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all
 frames.
 */
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

从苹果给出的注释来看,通过该方法能够添加一个脚本消息的处理器,即(id <WKScriptMessageHandler>)scriptMessageHandler,另外还能发现,添加脚本处理器后,需要在 JS 中添加window.webkit.messageHandlers.<name>.postMessage(<messageBody>)才能起作用。

demo:

// 创建并配置 WKWebView 的相关参数
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// self 指代的对象需要遵守 WKScriptMessageHandler 协议
[userContent addScriptMessageHandler:self name:@"test"];
config.userContentController = userContent;

在页面上的 JS 执行window.webkit.messageHandlers.<name>.postMessage(<messageBody>)时,被添加的ScriptMessageHandler就会执行实现的WKScriptMessageHandler协议的方法,例如:

#pragma mark----- WKScriptMessageHandler -----
/**
 *  JS 调用 OC 时 webview 会调用此方法
 *
 *  @param userContentController  webview 中配置的 userContentController 信息
 *  @param message                js 执行传递的消息
 */
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message
{
    NSLog(@"%@", message);
}

在代理方法中实现相应的 Native 代码,即完成了 JS 调用 Native 的过程。

二、JavaScriptCore

OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入 JavaScript。

JavaScriptCore中类及协议

  • JSContext:JavaScript 运行的上下文环境
  • JSValue:JavaScript 和 Objective-C 数据和方法的桥梁
  • JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议
  • JSManagedValue:管理数据和方法的类
  • JSVirtualMachine:处理线程相关,使用较少

JavaScript 调用 Native

使用 JavaScriptCore 进行 JS 和 Native 的交互,无论想要实现什么样的效果都需要获得一个有效的 JSContext 实例,即一个有效的 JS 运行的上下文(这一步骤以下不再重复提及)。

  • 获得当前的 JSContext:
    可以在页面加载完毕后,采用 KVC 的方式从webView 中获得,如下:
JSContext* jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  • 将想要被暴露给 JS 的方法抽象成为一个协议(protocol),该协议需要遵守 JSExport 协议:
@protocol JSObjcDelegate <JSExport>
- (void)callCamera;
- (NSString*)share:(NSString*)shareString;
@end
  • 将要暴露给 JS 的对象的类需要遵守自定义的协议,如上:JSObjcDelegate
  • 将 OC 对象桥接到 JS 环境中,并设置异常处理
// 将本对象与 JS 中的 DZBridge 对象桥接在一起,在 JS 中 DZBridge 代表本对象
[self.jsContext setObject:self forKeyedSubscript:@"DZBridge"];
self.jsContext.exceptionHandler = ^(JSContext* context, JSValue* exceptionValue) {
        context.exception = exceptionValue;
        NSLog(@"异常信息:%@", exceptionValue);
    };
  • 在 JS 中通过 DZBridge 调用本对象暴露出的方法:
var callShare = function() {
        var shareInfo = JSON.stringify({"title": "标题", "desc": "内容", "shareUrl": "http://www.jianshu.com"});
        var str = DZBridge.share(shareInfo);
        alert(str);
    }

Native 调用 JavaScript

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

推荐阅读更多精彩内容