×

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

96
RITL
2017.03.05 12:47* 字数 1228

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

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

假设一个简单的场景

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

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

UIWebView.gif

WKWebView.gif

JavaScriptCore

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

JSContext

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

JSValue

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

   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。

JSExport

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

通常用法是自定义一个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

iOS-学习笔记
Web note ad 1