参与工作时间比较长了,随着Web前端行业的发展(大家都懂得..),客户端与Web端的交互也越来越频繁。其实本人不太喜欢依赖第三方,那种看不到摸不着的东西用起来总感觉不是很安心,同时也是为了保证双方都能够高效完成交互的途中不出现一些意料不到的异常,对此,研究了一下JavaScriptCore
这个库还是很有必要的,并分别结合UIWebView
以及WKWebView
做了一下交互总结。
写的比较多,如果是第一次接触这个库,建议还是看一看;如果时间比较紧,想直接知道结果的,送你一个捷径😀传送门,有帮助可以Star一下,十分感谢
假设一个简单的场景
- Web通过一个
<input/>
输入一个字符串,通过点击按钮设置成导航标题 - 原生设置完导航标题后,告知Web
"以将<#字符串#>"设置成导航Title
,并在网页最底下的label显示出来。
分别使用UIWebView
以及WKWebView
实现效果如下:
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