Swift+JavaScriptCore
JavaScriptCore概述
JSValue: 代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean, integers, doubles,甚至包括对象和函数。
JSManagedValue: 本质上是一个JSValue,但是可以处理内存管理中的一些特殊情形,它能帮助引用技术和垃圾回收这两种内存管理机制之间进行正确的转换。
JSContext: 代表JavaScript的运行环境,你需要用JSContext来执行JavaScript代码。所有的JSValue都是捆绑在一个JSContext上的。
JSExport: 这是一个协议,可以用这个协议来将原生对象导出给JavaScript,这样原生对象的属性或方法就成为了JavaScript的属性或方法,非常神奇。
JSVirtualMachine: 代表一个对象空间,拥有自己的堆结构和垃圾回收机制。大部分情况下不需要和它直接交互,除非要处理一些特殊的多线程或者内存管理问题。
Swift调用JavaScript
首先准备一份JavaScript代码
新建一个jsSource.js文件, 写入以下JavaScript代码
//一个变量
var helloWorld = "Hello World!"
//一个方法
function getFullname(firstname, lastname) {
return firstname + " " + lastname;
}
Swift访问JavaScript变量
func swiftCallJS() {
//构建一个JSContext对象
let jsContext = JSContext()
//加载JavaScript文件
if let jsSourcePath = Bundle.main.path(forResource: "jsSource", ofType: "js") {
do {
//读取文件的代码
let jsSourceContents = try String(contentsOfFile: jsSourcePath)
//将JavaScript代码传递给JSContext环境
_ = jsContext?.evaluateScript(jsSourceContents)
}
catch {
print(error.localizedDescription)
}
}
//通过JSContext访问JavaScript
if let variableHelloWorld = jsContext?.objectForKeyedSubscript("helloWorld") {
print(variableHelloWorld.toString())
}
}
Swift访问JavaScript方法
func swiftCallJSFunc() {
//构建一个JSContext对象
let jsContext = JSContext()
//加载JavaScript文件
if let jsSourcePath = Bundle.main.path(forResource: "jsSource", ofType: "js") {
do {
//读取文件的代码
let jsSourceContents = try String(contentsOfFile: jsSourcePath)
//将JavaScript代码传递给JSContext环境
_ = jsContext?.evaluateScript(jsSourceContents)
}
catch {
print(error.localizedDescription)
}
}
let firstname = "Mickey"
let lastname = "Mouse"
//通过JSContext访问JavaScript, objectForKeyedSubscript()方法返回一个JSValue对象, JSValue对象包装了一个JavaScript实体
if let functionFullname = jsContext?.objectForKeyedSubscript("getFullname") {
//使用JSValue对象去掉用JavaScript方法, 并传递参数, 参数是以数组的方式传递
if let fullname = functionFullname.call(withArguments: [firstname, lastname]) {
print(fullname.toString())
}
}
}
JavaScript调用Swift就是把Swift的闭包包装成一个对象, 并把这个对象传递给JSContext, 由JSContext对象进项JavaScript和Swift之间的调度
同样, 先在jsSource.js文件中加入以下JavaScript代码
//这个方法是通过swift调用的JS方法
function swiftCallJavaScript() {
var luckyNumbers = [];
while (luckyNumbers.length != 6) {
var randomNumber = Math.floor((Math.random() * 50) + 1);
if (!luckyNumbers.includes(randomNumber)) {
luckyNumbers.push(randomNumber);
}
}
//JavaScriptCallSwift方法是我们通过JavaScriptCore, 把该方法与Swift中定义的闭包进行绑定
JavaScriptCallSwift(luckyNumbers);
}
首先先创建一个闭包的全局属性, 以便在全局范围内都可以访问到
//定义一个变量闭包, 参数是一个Int类型的数组, 无返回值
var swiftClosure:(@convention(block) ([Int]) -> Void)?
闭包是一个保存在栈区的代码块, 在使用闭包之前, 我们需要先对闭包进行定义
在viewDidLoad里面对变量闭包进行赋值
override func viewDidLoad() {
//第一步: 定义一个闭包
//第一种定义方式
let willCallClosure: @convention(block) ([Int]) -> Void = { luckyNumbers in
print(luckyNumbers)
}
/**
//第二种定义方式, 这种方式更简便
let willCallClosure = {
(luckyNumbers:[Int]) -> Void
in
print(luckyNumbers)
}
*/
//把闭包赋值给变量闭包
swiftClosure = willCallClosure
JavaScriptCallSwift()
}
JavaScript访问Swift
func JavaScriptCallSwift() {
let jsContext = JSContext()
jsContext?.exceptionHandler = { context, exception in
if let exc = exception {
print("JS Exception:", exc.toString())
}
}
if let jsSourcePath = Bundle.main.path(forResource: "jsSource", ofType: "js") {
do {
let jsSourceContents = try String(contentsOfFile: jsSourcePath)
_ = jsContext?.evaluateScript(jsSourceContents)
}
catch {
print(error.localizedDescription)
}
}
//第二步: 将闭包快转换成一个 AnyObject 对象
let swiftAnyObject = unsafeBitCast(self.swiftClosure, to: AnyObject.self)
/**
* 第三步: 将swiftAnyObject 传递给 jsContext
* 将闭包块与JS进行关联, JavaScriptCallSwift就是即将要执行的JS方法, 这个方法会调用都到swift的原生代码
*/
jsContext?.setObject(swiftAnyObject, forKeyedSubscript: "JavaScriptCallSwift" as (NSCopying&NSObjectProtocol)!)
//第四部: 通过JSContext计算JavaScriptCallSwift
_ = jsContext?.evaluateScript("JavaScriptCallSwift")
//第五步: 通过JSContext 调用方法
if let functionGenerateLuckyNumbers = jsContext?.objectForKeyedSubscript("swiftCallJavaScript") {
_ = functionGenerateLuckyNumbers.call(withArguments: nil)
}
}
以上就是JavaScript与Swift之间的交互, 是用的都是本地的JavaScript文件, 在实际工作中, 我们的JavaScript代码是从服务器获取的.
接下来介绍使用WebView与WKWebView不同的获取JavaScript内容的方法.
WebView获取JavaScript
extension NextVCViewController:UIWebViewDelegate {
func webViewDidFinishLoad(_ webView: UIWebView) {
let jsContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext
}
通过这种方式就可以获取JavaScript的内容了, 只需把上面的JavaScriptCallSwift()方法中生成JSContext的方法修改下就可以了. 在JavaScriptCallSwift()方法中我们是通过JSContext() 创建了一个JSContext, 次数我们同伙获取JavaScript创建一个JSContext.
WKWebView获取JavaScript
WKWebView是比较坑的是不能通过javaScriptCore进行交互, 所以就无法获取JSContext, 所以以上介绍的说有内容对于WKWebView来说就是沙滩之子(son of the bitch).
在WKWebView中使用WKUserContentController类来进行交互
首先我们先配置下WKWebView
func configWebView() {
let wkWebConfig = WKWebViewConfiguration.init()
wkWebConfig.selectionGranularity = .character
wkWebConfig.preferences = WKPreferences.init()
wkWebConfig.preferences.minimumFontSize = 14
wkWebConfig.preferences.javaScriptEnabled = true
wkWebConfig.preferences.javaScriptCanOpenWindowsAutomatically = true
wkWebConfig.userContentController = WKUserContentController.init()
//这个是关键, 设置WKScriptMessageHandler的代理
//AppModel是我们注入到JavaScript中的一个标识, JavaScript会通过这个标识来给Swift发送消息
wkWebConfig.userContentController.add(self as WKScriptMessageHandler, name: "Bridge")
let wkWeb = WKWebView(frame:CGRect(x: 0, y: 64, width: 200, height: 200), configuration: wkWebConfig);
let path = Bundle.main.path(forResource: "web", ofType: "html")
let url = NSURL(fileURLWithPath: path!)
let request = NSURLRequest(url: url as URL)
wkWeb.load(request as URLRequest)
self.view.addSubview(wkWeb)
}
然后实现与JavaScript通信的代理方法
extension WKWebViewController:WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.name == "Bridge") {
// 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
// NSDictionary, and NSNull类型
print(message.body);
}
}
}
接下来我们修改下JavaScript代码
这次使用加载html的方式来获取JavaScript
web.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
</head>
<body>
<script>
//注册一个定时器, 模仿JavaScript调用
var myVar=setInterval(function(){
swiftCallJavaScript()
},1000);
function swiftCallJavaScript() {
//这个是固定格式, Bridgre就是刚才我们的标识
window.webkit.messageHandlers.Bridge.postMessage({body: 'call swift'});
}
</script>
</body>
</html>
这样就可以实现JavaScript调用Swift的代码了, 我们通过一个代理来获取JavaScript方法被调用的消息, 然后再进行响应的处理.
至于上面提到的 window.webkit.messageHandlers.Bridge.postMessage({body: 'call swift'});
为什么要这样写, 这个在苹果的源码里有注释
/*! @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;