iOS开发----JavaScriptCore、UIWebView及WKWebView交互的那些事

参与工做时间比较长了,随着Web前端行业的发展(你们都懂得..),客户端与Web端的交互也愈来愈频繁。其实本人不太喜欢依赖第三方,那种看不到摸不着的东西用起来总感受不是很安心,同时也是为了保证双方都可以高效完成交互的途中不出现一些意料不到的异常,对此,研究了一下JavaScriptCore这个库仍是颇有必要的,并分别结合UIWebView以及WKWebView作了一下交互总结。javascript

写的比较多,若是是第一次接触这个库,建议仍是看一看;若是时间比较紧,想直接知道结果的,送你一个捷径😀传送门,有帮助能够Star一下,十分感谢前端

假设一个简单的场景

  • Web经过一个<input/>输入一个字符串,经过点击按钮设置成导航标题
  • 原生设置完导航标题后,告知Web"以将<#字符串#>"设置成导航Title,并在网页最底下的label显示出来。

分别使用UIWebView以及WKWebView实现效果以下:
java

UIWebView.gif

WKWebView.gif

JavaScriptCore

类库里面有12个类(还有两个是负责导入相关类的头文件以及一个关于WebKit的宏定义);在基本的交互过程当中,其实最常使用的有三个:JSContext、JSValue、JSExportios

JSContext

简单的理解为执行JavaScript的一个环境,就好像咱们在绘制View时候须要获取的CGContext同样,JS的执行须要在此环境之下。git

JSValue

能够理解成 一种供iOS数据结构与JS数据结构相互转换的包装,也能够当作一种桥接关系,咱们执行JS获取的结果就是经过JSValue对象进行包装传给客户端进行处理的,类型转换官方文档描述以下:github

Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)复制代码

JavaScriptType返回的JSValue数据可经过JSValue.toXXX()转成客户端相应的数据结构;反之,客户端对象也能够经过JSValue()的构造方法将相应的数据结构封装成JSValue。web

JSExport

这是一个协议,官方文档没有暴露出任何的open协议方法,能够理解为一个空协议。bash

一般用法是自定义一个CustomExport : JSExport,里面将JS能够调用的属性或者方法进行暴露,JS就能够直接使用暴露的属性与方法了。微信

ObjC方法定义样式是很是特殊的,但官方文档给出了转换后JS调用的样式:数据结构

//Objective-C
- (void)doFoo:(id)foo withBar:(id)bar;

//JS
doFooWithBar(foo,bar)复制代码

但这样会有一个缺点,万一,方法有不少个参,拼接起来的JS方法名简直就是日了X;不过这点Apple已经帮咱们想到了,使用JSExportAs宏,能够将方法名简化,就像Swift中的typealias以及ObjC中的typedef

//这样在JS中直接调用doFoo(foo,bar)便可
 JSExportAs(doFoo,
  - (void)doFoo:(id)foo withBar:(id)bar
  );复制代码

以上三个文件就算理解完了,下面来一段小应用😀。

客户端调用JavaScript

执行简单的JavaScript

let context = JSContext()

//方法函数定义采用的是ES6语法,由于最近正在学习RN,习惯这么写了呢😀
let _ = context?.evaluateScript("var textnumber = 1")
let _ = context?.evaluateScript("var names = ['Yue','Xiao','Wen']")
let _ = context?.evaluateScript("var triple = (value) => value + 3")
let returnValue = context?.evaluateScript("triple(3)") //由于有返回值,须要接收一下

//打印结果:returnValue = Optional(6)
print("__testValueInContext --- returnValue = \(returnValue?.toNumber())")复制代码

获取定义的JavaScript变量

//经过变量名字获取对象
let names = context?.objectForKeyedSubscript("names")

//经过定义顺序的下标获取对象,就是取['Yue','Xiao','Wen']的第0个元素
let firstName = names?.objectAtIndexedSubscript(0) //Yue

//打印结果:names = Optional([Yue, Xiao, Wen]) firstName = Optional(Yue)
print("__testValueInContext --- names = \(names?.toArray())\nfirstName = \(firstName)")

/// 得到context建立的函数变量
let function = context?.objectForKeyedSubscript("triple") //运行 let result = function?.call(withArguments: [3]) //打印结果:context-function's result = Optional(6) print("__testValueInContext --- context-function's result = \(result?.toNumber())")复制代码

捕获执行异常

/// 捕获JS运行错误
context?.exceptionHandler = {(context,exception) in
    print("__testValueInContext --- JS error = \(exception)\n")//打印错误
}

/** 执行一个错误的js,由于没有函数Triple(上面的方法名第一字母是小写的),会调用上面的exceptionHandler 打印结果: JS error = Optional(ReferenceError: Can't find variable: Triple) */
let _ = context?.evaluateScript("Triple(3)")复制代码

JavaScript 调用客户端

仔细看看JSValue的类型转换,就能够知道,JS中方法就是客户端中的闭包,不过这里楼主采用了Swift和ObjC混编模式,至于缘由下面会说一下:

//得到处理完毕的数据
let result = RITLJSCoreObject.textJavaScriptUseiOS(inObjC: "Hello")

//结果 I am Objc, result = Optional("Hello I am append String")
print("I am Objc, result = \(result?.toString())\n")复制代码

实现方法:

+(JSValue *)textJavaScriptUseiOSInObjC:(NSString *)value
{
    JSContext * context = [JSContext new];
    
    //设置block
    context[@"stringHandler"] = ^(NSString * oldValue){
        NSMutableString * valueHandler = [[NSMutableString alloc]initWithString:oldValue];
        [valueHandler appendString:@" I am append String"];
        return valueHandler;
    };
    
    NSString * js = [NSString stringWithFormat:@"stringHandler('%@')",value];
    //注入
    return [context evaluateScript:js];
}复制代码

Swift版本以下,功能实如今本人看来应该是同样的,但在进行注入的时候出现了问题,致使执行方法出现了undefined

多是`Swift`的一个bug,也多是我使用不当
若是是我使用错了,还请知道缘由的小伙伴私信一下,十分感谢。复制代码
let context = JSContext()

//初始化一个闭包
let stringHandler : (String) -> String = { (value) in
    var value = value
    value.append(" I am appending word with closure!")
    return value
}

//封装成JSValue
let handerValue = JSValue(object: stringHandler, in: context)

//问题语句$$$$,我怀疑是注入失败..见鬼了
context?.setObject(handerValue, forKeyedSubscript: "stringHandler" as NSString)
let result = context?.evaluateScript("stringHandler('Hello')")

// 结果:I am Swift ,result = Optional("undefined") - - 很无解有没有!!!!
print("I am Swift ,result = \(result?.toString())\n")复制代码

实现场景

终于能够运用上面的一些方法来实现功能啦。

JavaScript中的逻辑以下:

  • 确认当前使用的是UIWebView仍是WKWebView,并经过变量ritl_type肯定
  • 点击按钮,根据类型执行不一样的操做
  • 客户端经过执行iosTellSomething方法告知Web,修改当前label的值
// 默认为WKWebView
var ritl_tyle = "WKWebView";

// 肯定是webView仍是WKWebView
function sureType(value){
  ritl_tyle = value;
};

// 按钮点击
function buttonDidTap (){
  var inputValue = $('#input').val()

  if (ritl_tyle == "UIWebView"){//若是是UIWebView
        RITLExportObject.say(inputValue)//经过注入的对象进行通知客户端
  }

  else if (ritl_tyle == "WKWebView"){//若是是WKWebView
        alert("WKWebView");
        window.webkit.messageHandlers.ChangedMessage.postMessage(inputValue);
    }
};

function iosTellSomething(value){
    //document.getElementById("label").value = "收到啦";//设置给label
    $('#label').text(value);
}复制代码

UIWebView

JSExport

定义一个自定义的协议RITLJSExport,这里仍然采用混编模式,由于我仍是Swfit注入失败了...

@protocol RITLJSExport <NSObject,JSExport>

// 相似typedef 将saySomething定义为say,便于JS调用
JSExportAs(say,
- (void)saySomething:(NSString *)thing
);
@end

@interface RITLExportObject : NSObject

/// 进行的回调
@property (nonatomic, copy) void(^dosomething)(NSString *);

/// 将本身注册到JSContext
- (void)registerSelfToContext:(JSContext *)context;

@end

@interface RITLExportObject (RITLJSExport)<RITLJSExport>
    
@end复制代码

UIWebViewDelegate

UIWebViewDelegate中的webViewDidFinishLoad()方法中对JSContext进行截取,并执行操做:

// MARK: UIWebView-Delegate 系列
extension RITLJSWebViewController : UIWebViewDelegate {
    
    func webViewDidFinishLoad(_ webView: UIWebView) {
        
        //得到JSContent对象
        guard  let context : JSContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext? else {
            return
        }
        
        //告诉web,这里是UIWebView
        webView.stringByEvaluatingJavaScript(from: "sureType('UIWebView')")
        
        /* 使用的ObjC的Export对象 */
        let exportObject = RITLExportObject()
        exportObject.dosomething = { [weak self](value) in
            
            guard let value = value else { return }
            self?.navigationItem.title = value //设置导航栏
            
            //执行js告知,修改导航栏完毕
            webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已将\(value)设置成导航Title')")//回应
        }
        
        //进行注入
        exportObject.registerSelf(to: context)
    }
}
复制代码

WKWebView

首先有一点,WKWebView是获取不到JSContext的,那咋办?不要紧,WKWebView提供给了咱们很是便利的交互,不详细说了,以前写的一篇博文已经介绍了,有兴趣能够看看iOS开发-------基于WKWebView的原生与JavaScript数据交互

添加JavaScript交互

// 使用WkWebView
lazy var wkWebView : WKWebView = {
    
    let webView: WKWebView = WKWebView(frame: self.view.bounds)
    
    webView.navigationDelegate = self
    webView.uiDelegate = self
    webView.configuration.userContentController.add(RITLSciptMessageHandler(self), name: "ChangedMessage")// 添加处理
    
    return webView
}()复制代码

在WKNavigationDelegate中告知web当前使用webView的类型:

// 是为了使用JS确认一下类型,实际开发不须要在这个代理下进行以下操做
extension RITLJSWebViewController : WKNavigationDelegate {
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
        
        //确认类型
        webView.evaluateJavaScript("sureType('WKWebView')", completionHandler: nil)
    }
}复制代码

履行WKScriptMessageHandler协议,完成交互操做便可

// MARK: WKWebView-Delegate 系列
extension RITLJSWebViewController : WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
    {
        //若是body体是约定好的字符串,而且经过标志ChangedMessage传递而且存在body体
        guard message.body is String,message.name ==  "ChangedMessage",let body:String = message.body as? String else { return }
        
        navigationItem.title = body//设置导航
        
        //执行通知HTML
        wkWebView.evaluateJavaScript("iosTellSomething('已将\(body)设置成导航Title')") { (_, error) in
            print("error = \(error?.localizedDescription)")
        }
    }
}复制代码

最后记得移除哦

deinit {
        print("\(type(of: self)) deinit")
        if ritl_useWkWebView {
            wkWebView.configuration.userContentController.removeAllUserScripts()
        }
    }复制代码

这样子,基于JavaScriptCore的UIWebView以及WKWebView交互就算圆满完成啦,欢迎前去Start



做者:RITL
连接:https://www.jianshu.com/p/d8f7c5e237e8

此文章来源于第三方转载!!

 

小编这呢,给你们推荐一个优秀的iOS交流平台,平台里的伙伴们都是很是优秀的iOS开发人员,咱们专一于技术的分享与技巧的交流,你们能够在平台上讨论技术,交流学习。欢迎你们的加入(想要进入的可加小编微信)。

微信号13142121176

相关文章
相关标签/搜索