一、概述
- JSValue
代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean, integers, doubles,甚至包括对象和函数。 - JSManagedValue
本质上是一个JSValue,但是可以处理内存管理中的一些特殊情形,它能帮助引用计数和垃圾回收这两种内存管理机制之间进行正确的转换。 - JSContext
代表JavaScript的运行环境,你需要用JSContext来执行JavaScript代码。所有的JSValue都是捆绑在一个JSContext上的。 - JSExport
这是一个协议,可以用这个协议来将原生对象导出给JavaScript,这样原生对象的属性或方法就成为了JavaScript的属性或方法,非常神奇。 - JSVirtualMachine
代表一个对象空间,拥有自己的堆结构和垃圾回收机制。大部分情况下不需要和它直接交互,除非要处理一些特殊的多线程或者内存管理问题。
二、简单交互
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// 通过JSContext执行js代码
let context: JSContext = JSContext()
let result1: JSValue = context.evaluateScript("1 + 3")
print(result1) // 输出4
// 定义js变量和函数
context.evaluateScript("var num1 = 10; var num2 = 20;")
context.evaluateScript("function sum(param1, param2) { return param1 + param2; }")
// 通过js方法名调用方法
let result2 = context.evaluateScript("sum(num1, num2)")
print(result2) // 输出30
// 通过下标来获取js方法并调用方法
if let squareFunc = context.objectForKeyedSubscript("sum") {
let result3 = squareFunc.call(withArguments: [10,20])?.toString()
print(result3) // 输出30
}
}
三、Swift调用本地JavaScript
首先准备一份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
先在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);
}
首先在swift中先创建一个闭包的全局属性, 以便在全局范围内都可以访问到
//定义一个变量闭包, 参数是一个Int类型的数组, 无返回值
var swiftClosure:(@convention(block) ([Int]) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//第一步: 定义一个闭包
//第一种定义方式
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)
}
}
五、WebView获取JavaScript
extension ViewController:UIWebViewDelegate {
func webViewDidFinishLoad(_ webView: UIWebView) {
let jsContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext
}
六、WKWebView获取JavaScript
WKWebView是比较坑的是不能通过javaScriptCore进行交互, 所以就无法获取JSContext, 在WKWebView中使用WKUserContentController类来进行交互
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
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 ViewController:WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.name == "Bridge") {
// 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
// NSDictionary, and NSNull类型
print(message.body);
}
}
}
七、在Swift中通过JSContext注入模型
首先定义定义协议SwiftJavaScriptDelegate 该协议必须遵守JSExport协议
@objc protocol SwiftJavaScriptDelegate: JSExport {
// js调用App的微信支付功能 演示最基本的用法
func wxPay(orderNo: String)
// js调用App的微信分享功能 演示字典参数的使用
func wxShare(dict: [String: AnyObject])
// js调用App方法时传递多个参数 并弹出对话框 注意js调用时的函数名
func showDialog(title: String, message: String)
// js调用App的功能后 App再调用js函数执行回调
func callHandler(handleFuncName: String)
}
然后定义一个模型 该模型实现SwiftJavaScriptDelegate协议
@objc class SwiftJavaScriptModel: NSObject, SwiftJavaScriptDelegate {
weak var controller: UIViewController?
weak var jsContext: JSContext?
func wxPay(orderNo: String) {
print("订单号:", orderNo)
// 调起微信支付逻辑
}
func wxShare(dict: [String: AnyObject]) {
print("分享信息:", dict)
// 调起微信分享逻辑
}
func showDialog(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default, handler: nil))
self.controller?.present(alert, animated: true, completion: nil)
}
func callHandler(handleFuncName: String) {
let jsHandlerFunc = self.jsContext?.objectForKeyedSubscript("\(handleFuncName)")
let dict = ["name": "sean", "age": 18] as [String : Any]
jsHandlerFunc?.call(withArguments: [dict])
}
}
然后使用WebView加载对应的网页,这里加载例子中的demo.html文件
func addWebView() {
self.webView = UIWebView(frame: self.view.bounds)
self.view.addSubview(self.webView)
self.webView.delegate = self
self.webView.scalesPageToFit = true
// 加载本地Html页面
let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html")
let request = NSURLRequest(URL: url!)
// 加载网络Html页面 请设置允许Http请求
//let url = NSURL(string: "http://www.mayanlong.com");
//let request = NSURLRequest(URL: url!)
self.webView.loadRequest(request)
}
最后在webViewDidFinishLoad代理中将我们定义的模型注入到网页中,暴露给JS
func webViewDidFinishLoad(webView: UIWebView) {
self.jsContext = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
let model = SwiftJavaScriptModel()
model.controller = self
model.jsContext = self.jsContext
// 这一步是将SwiftJavaScriptModel模型注入到JS中,在JS就可以通过WebViewJavascriptBridge调用我们暴露的方法了。
self.jsContext.setObject(model, forKeyedSubscript: "WebViewJavascriptBridge")
// 注册到本地的Html页面中
let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html")
self.jsContext.evaluateScript(try? String(contentsOfURL: url!, encoding: NSUTF8StringEncoding))
// 注册到网络Html页面 请设置允许Http请求
//let url = "http://www.mayanlong.com";
//let curUrl = self.webView.request?.URL?.absoluteString //WebView当前访问页面的链接 可动态注册
//self.jsContext.evaluateScript(try? String(contentsOfURL: NSURL(string: url)!, encoding: NSUTF8StringEncoding))
self.jsContext.exceptionHandler = { (context, exception) in
print("exception:", exception)
}
}
JS调用Swift方法
WebViewJavascriptBridge.wxPay('66666')
WebViewJavascriptBridge.wxShare({
'title' : '我就是我',
'description' : '给个关注行不行',
'url' : 'https://github.com/chenxifanfan'
})
WebViewJavascriptBridge.showDialogMessage('6666', '给个关注行不行')