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

参与工作时间比较长了,随着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()

//初始化一个闭包
//由于OC中block与Swift中的closure结构并不一样,需要使用`@convention(block) `关键词声明一下
let stringHandler : @convention(block)  (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") - - 很无解有没有!!!!(之前)~~
// 结果:  I am Swift ,result = Optional("Hello I am appending word with closure!")
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注入失败了...

Objective

@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

Swift

import UIKit

/// 必须追加@objc
@objc protocol RITLJSSwiftExport: JSExport {
    
    /// 方法的标签一定记得去掉
    func say(_ something: String)
}

/// 必须追加@objc
@objc class RITLExportSwiftObject: NSObject {

    var doSomething: ((String?) -> Void)?
    override init() {
        super.init()
    }
}

extension RITLExportSwiftObject : RITLJSSwiftExport {
    
    func say(_ something: String) {
        doSomething?(something)
    }
}

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)


        // 使用Swift的Export对象
        let exportObject = RITLExportSwiftObject()
        
        exportObject.doSomething =  { [weak self](value) in
            guard let value = value else { return }
            DispatchQueue.main.async {
                //设置导航栏
                self?.navigationItem.title = value
                
                //执行js告知,修改导航栏完毕
                webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已将\(value)设置成导航Title')")//回应
            }
        }
                
        context.setObject(exportObject, forKeyedSubscript: "RITLExportObject" as NSString)
    }
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 跟原生开发相比,H5的开发相对来一个成熟的框架和团队来讲在开发速度和开发效率上有着比原生很大的优势,至少不用等待审...
    大冲哥阅读 1,827评论 0 7
  • 前言 Web 页面中的 JS 与 iOS Native 如何交互是每个 iOS 猿必须掌握的技能。而说到 Nati...
    幽城88阅读 2,191评论 1 8
  • 随着H5技术的兴起,在iOS开发过程中,难免会遇到原生应用需要和H5页面交互的问题。其中会涉及方法调用及参数传值等...
    Chris_js阅读 3,056评论 1 8
  • JavaScriptCore框架主要是用来实现iOS与H5的交互。由于现在混合编程越来越多,H5的相对讲多,所以研...
    水灵芳蕥阅读 1,382评论 1 8
  • 每次看完一部剧,无论结果是好是坏,是悲是喜,心中总是涌起一股伤感。刚看完战争剧战长沙,长沙这个我今后将生活与学习的...
    柚柚柚子青茶阅读 259评论 0 0