Objective-C/Swift与JavaScript交互

OC/Swift与JavaScript交互

问题

  • iOS中如何加载HTML页面?(包括Objective-CSwift
  • iOS中如何去执行一段JavaScript代码?(包括Objective-CSwift
  • iOS中为什么要使用原生语言去执行一段JavaScript代码?
  • iOS中如何监听到HTML页面中触发的事件?(JavaScript函数的触发)
  • iOS混合开发中,当JavaScript函数触发时,能否发送一些数据给iOS原生,如何发送?
  • iOS混合开发中,当JavaScript函数触发时,能否让OC/Swift执行一些操作,比如调用系统相机等?

iOS中加载HTML页面

UIWebView

OC版本

UIWebViewiOS 2.0就有的一个UI控件,是用来加载HTML页面的,属于UIKit框架。基本使用比较简单,代码如下:

1
2
3
4
5
6
7
//UI控件设置
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.delegate = self;
[self.view addSubview:webView];
NSURL *htmlUrl = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:htmlUrl];
[webView loadRequest:request];

1
2
3
4
5
6
7
8
9
10
// 实现代理 UIWebViewDelegate
- (void)webViewDidStartLoad:(UIWebView *)webView{
//网页开始加载时调用
}
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//网页加载完成时调用
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{
//网页加载失败时调用
}

Swift版本

1
2
3
4
5
6
//UI控件设置
let webView = UIWebView(frame: view.bounds)
webView.delegate = self
view.addSubview(webView)
let url = URL(string: "https://www.baidu.com")!
webView.loadRequest(URLRequest(url: url))
1
2
3
4
5
6
7
8
9
10
11
12
// 实现代理 UIWebViewDelegate
extension ViewController: UIWebViewDelegate {
func webViewDidStartLoad(_ webView: UIWebView) {
//网页开始加载时调用
}
func webViewDidFinishLoad(_ webView: UIWebView) {
//网页加载完成时调用
}
func webView(_ webView: UIWebView, didFailLoadWithError error: Error) {
//网页加载失败时调用
}
}

WKWebView

OC版本

WKWebViewiOS 8.0出现的一个UI控件,是用来加载HTML页面的,属于WebKit框架。基本使用,代码如下:

1
2
3
4
5
6
7
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
webView.UIDelegate = self;
webView.navigationDelegate = self;
[self.view addSubview:webView];
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//需要遵守协议 WKNavigationDelegate, WKUIDelegate
/*
WKNavigationDelegate实现:
首次载入调用顺序:didStartProvisionalNavigation -> didCommitNavigation -> didFinishNavigation
重定向:didStartProvisionalNavigation -> didReceiveServerRedirectForProvisionalNavigation -> didCommitNavigation -> didFinishNavigation
*/
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
NSLog(@"--%s",__func__);
//页面开始加载时调用
}
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
NSLog(@"--%s",__func__);
//当内容开始返回时调用
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
NSLog(@"--%s",__func__);
//页面加载完成之后调用
}
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
NSLog(@"--%s",__func__);
//服务器重定向页面时调用,并且在 didStartProvisionalNavigation 之后,didCommitNavigation之前调用。
}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"--%s",__func__);
//页面加载失败时调用
}

Swift版本

1
2
3
4
5
6
7
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)
let url = URL(string: "https://www.baidu.com")!
webView.load(URLRequest(url: url))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//需要遵守协议 WKNavigationDelegate, WKUIDelegate
/*
顺序:didStartProvisionalNavigation -> didCommit -> didFinish
*/
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print(#function)
//页面开始加载时调用
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print(#function)
//当内容开始返回时调用
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print(#function)
//页面加载完成之后调用
}
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
print(#function)
//服务器重定向页面时调用.
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
print(#function)
//页面加载失败时调用
}
}

iOS中执行一段JavaScript代码

iOS中使用原生语言去执行JavaScript脚本,是为了对已经加载好的HTMLDOM元素的增、删、改、查。同时,还可以通过执行JavaScript脚本获取DOM对象,进而获取一些HTML的页面信息,比如,通过脚本document.title可以获取当前页面的title,通过脚本document.location.href可以获取当前加载的HTML页面的url
通常这些操作的方法是:使用UIWebViewWKWebView的对象方法执行JavaScript脚本。通常这些操作的时机是:在HTML页面加载完成的原生回调中进行的。

使用UIWebView执行JS代码

1
2
3
4
5
6
7
8
9
10
<!--要加载的HTML文件 index.html -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf8">
</head>
<body>
<h1>这是一个网页</h1>
<p></p>
</body>
</html>

OC版本

使用UIWebView的对象方法- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;来执行一段JavaScript代码。一般是在UIWebView的代理方法:- (void)webViewDidFinishLoad:(UIWebView *)webView;中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.delegate = self;
webView.scrollView.hidden = YES;
webView.backgroundColor = [UIColor grayColor];
webView.scalesPageToFit = YES;
[self.view addSubview:webView];
NSURL *htmlUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
NSURLRequest *request = [NSURLRequest requestWithURL:htmlUrl];
[webView loadRequest:request];

//添加网络加载指示器
UIActivityIndicatorView *indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicatorView.center = CGPointMake(200, 200);
[self.view addSubview:indicatorView];
self.indicatorView = indicatorView;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 代理
- (void)webViewDidStartLoad:(UIWebView *)webView
{//网页开始加载时调用
//指示器开始显示动画
[self.indicatorView startAnimating];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView
{//网页加载完成时调用

//指示器结束显示动画
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
webView.scrollView.hidden = NO;
[self.indicatorView stopAnimating];
});

//注意:JavaScript脚本字符串中不需要添加<script></script>标签
NSString *jsStr_1 = @"alert('JS弹框')";
[webView stringByEvaluatingJavaScriptFromString:jsStr_1];

NSString *jsStr_2 = @"var p = document.getElementsByTagName('p')[0];";
NSString *jsStr_3 = @"p.innerHTML = '使用JavaScript很🐂';";
NSString *jsStr_4 = @"p.style.background = 'red';document.body.appendChild(p);";
[webView stringByEvaluatingJavaScriptFromString:jsStr_2];
[webView stringByEvaluatingJavaScriptFromString:jsStr_3];
[webView stringByEvaluatingJavaScriptFromString:jsStr_4];

NSString *jsStr_5 = @"var li = document.createElement('li');li.innerHTML='执行js代码,dom操作元素';li.style.background = 'gray';document.body.appendChild(li);";
[webView stringByEvaluatingJavaScriptFromString:jsStr_5];
}

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ViewController: UIViewController {

//网络加载指示器
lazy var indicatorView: UIActivityIndicatorView = {
var indicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
indicator.center = CGPoint(x: 200, y: 200)
return indicator
}()

override func viewDidLoad() {
super.viewDidLoad()

let webView = UIWebView(frame: view.bounds)
webView.delegate = self
webView.scrollView.isHidden = true
webView.backgroundColor = .gray
webView.scalesPageToFit = true
view.addSubview(webView)

let url = Bundle.main.url(forResource:"index", withExtension:"html")!
webView.loadRequest(URLRequest(url: url))

//添加网络加载指示器
view.addSubview(indicatorView)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 代理
extension ViewController: UIWebViewDelegate {

func webViewDidStartLoad(_ webView: UIWebView) {//网页开始加载时调用

//指示器开始显示动画
indicatorView.startAnimating()
}
func webViewDidFinishLoad(_ webView: UIWebView) {//网页加载完成时调用

//指示器结束显示动画
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25) {
webView.scrollView.isHidden = false
self.indicatorView.stopAnimating()
}

//注意:JavaScript脚本字符串中不需要添加<script></script>标签
let jsStr_1 = "alert('JS弹框')"
webView.stringByEvaluatingJavaScript(from: jsStr_1)


let jsStr_2 = "var p = document.getElementsByTagName('p')[0];"
let jsStr_3 = "p.innerHTML = '使用JavaScript很🐂';"
let jsStr_4 = "p.style.background = 'red';document.body.appendChild(p);"
webView.stringByEvaluatingJavaScript(from: jsStr_2)
webView.stringByEvaluatingJavaScript(from: jsStr_3)
webView.stringByEvaluatingJavaScript(from: jsStr_4)


let jsStr_5 = "var li = document.createElement('li');li.innerHTML='执行js代码,dom操作元素';li.style.background = 'gray';document.body.appendChild(li);"
webView.stringByEvaluatingJavaScript(from: jsStr_5)
}
}

使用WKWebView执行JS代码

OC版本

使用WKWebView的对象方法- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;来执行一段JavaScript代码。一般是在WKWebView的代理方法:- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
webView.UIDelegate = self;
webView.navigationDelegate = self;
webView.scrollView.hidden = YES;
webView.backgroundColor = [UIColor grayColor];
[self.view addSubview:webView];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

//添加网络加载指示器
UIActivityIndicatorView *indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicatorView.center = CGPointMake(200, 200);
[self.view addSubview:indicatorView];
self.indicatorView = indicatorView;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 代理 WKNavigationDelegate
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{//页面开始加载时调用
//指示器开始显示动画
[self.indicatorView startAnimating];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
{ //页面加载完成之后调用

//指示器结束显示动画
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
webView.scrollView.hidden = NO;
[self.indicatorView stopAnimating];
});

//注意:JavaScript脚本字符串中不需要添加<script></script>标签
NSString *jsStr_1 = @"var p = document.getElementsByTagName('p')[0];";
NSString *jsStr_2 = @"p.innerHTML = '使用JavaScript很🐂';";
NSString *jsStr_3 = @"p.style.background = 'red';document.body.appendChild(p);";
[webView evaluateJavaScript:jsStr_1 completionHandler:nil];
[webView evaluateJavaScript:jsStr_2 completionHandler:^(id _Nullable value, NSError * _Nullable error) {
NSLog(@"value: %@",value); //打印出插入的内容:使用JavaScript很🐂
}];
[webView evaluateJavaScript:jsStr_3 completionHandler:nil];

NSString *jsStr_4 = @"var li = document.createElement('li');li.innerHTML='执行js代码,dom操作元素';li.style.background = 'gray';document.body.appendChild(li);";
[webView evaluateJavaScript:jsStr_4 completionHandler:nil];
}

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ViewController: UIViewController {
//网络加载指示器
lazy var indicatorView: UIActivityIndicatorView = {
var indicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
indicator.center = CGPoint(x: 200, y: 200)
return indicator
}()

override func viewDidLoad() {
super.viewDidLoad()

let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.scrollView.isHidden = true
webView.backgroundColor = .gray
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)

let url = Bundle.main.url(forResource:"index", withExtension:"html")!
webView.load(URLRequest(url: url))

//添加网络加载指示器
view.addSubview(indicatorView)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//代理 WKNavigationDelegate
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print(#function) //页面开始加载时调用

//指示器开始显示动画
indicatorView.startAnimating()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print(#function) //页面加载完成之后调用

//指示器结束显示动画
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25) {
webView.scrollView.isHidden = false
self.indicatorView.stopAnimating()
}

//注意:JavaScript脚本字符串中不需要添加<script></script>标签
let jsStr_1 = "var p = document.getElementsByTagName('p')[0];"
let jsStr_2 = "p.innerHTML = '使用JavaScript很🐂';"
let jsStr_3 = "p.style.background = 'red';document.body.appendChild(p);"
webView.evaluateJavaScript(jsStr_1, completionHandler: nil)
webView.evaluateJavaScript(jsStr_2) { (value, error) in
print(value ?? "") //打印出插入的内容:使用JavaScript很🐂
}
webView.evaluateJavaScript(jsStr_3, completionHandler: nil)

let jsStr_4 = "var li = document.createElement('li');li.innerHTML='执行js代码,dom操作元素';li.style.background = 'gray';document.body.appendChild(li);"
webView.evaluateJavaScript(jsStr_4, completionHandler: nil)
}
}

iOS中监听JavaScript函数调用

iOS混合HTML开发中,如何去监听HTML页面中一个JavaScript函数的调用,这是在需求中经常遇到的。比如,在一个混合页面中,需要点击HTML中的某个按钮标签,调用iPhone系统相机,而相机的触发必须调用iOS原生方法。再比如,在一个混合页面中,需要点击HTML中的某个支付按钮标签,调用iPhone系统已经安装的支付宝应用进行支付操作,来实现应用间的传参跳转,就需要调用支付宝SDKiOS原生方法。又比如,在混合开发中,需要从一个HTML页面跳转到我的原生页面,并且传送相应的参数,就需要监听JavaScript事件,调用原生控制器跳转方法。这又被很多人称为用JavaScript调用iOS原生代码,个人认为这种说法是不准确的。确切的说,iOS中监听JavaScript函数调用,并作出相应的行为,比较准确。

使用UIWebView监听JS函数调用

HTML页面中,对要触发的JavaScript方法中,使用window.location.href =实现一个页面重定向。当触发页面中JavaScript方法,即会调用window.location.href =进行页面重定向时,此时UIWebView代理方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;会监听到页面的重定向,并且可以获取请求对象,NSURLRequest对象request,从request中可以获得重定向的地址。可以对要重定向的地址进行重新定义协议,例如,'mengyueping.com://',后面可以拼接上要触发的OC/Swift方法名,'mengyueping.com://openCamera',在代理中拦截到完整协议地址,可以通过截取获得OC/Swift方法名,然后根据方法名给方法发送消息,触发方法,从而达到监听JavaScript函数调用的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--加载的本地HTML-->
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<br>
<button onclick="openCamera()">访问相册</button>
<a href="https://baidu.com">百度</a>
<input type="text" id="field">

<script>
function openCamera() {
document.getElementById('field').value = "赋值一下";
window.location.href = 'mengyueping.com://openCamera'; //自定义协议
}
</script>
</body>
</html>

OC版本

1
2
3
4
5
6
7
8
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.delegate = self;
webView.backgroundColor = [UIColor grayColor];
webView.scalesPageToFit = YES;
[self.view addSubview:webView];

NSURL *htmlUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
[webView loadRequest:[NSURLRequest requestWithURL:htmlUrl]];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//代理 UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *url = request.URL.absoluteString;
NSRange range = [url rangeOfString:@"mengyueping.com://"]; //自定义协议
NSUInteger location = range.location;

if (location != NSNotFound) {
NSString *str = [url substringFromIndex:(location + range.length)];
SEL selector = NSSelectorFromString(str);
//警告:PerformSelector may cause a leak because its selector is unknown
//[self performSelector:selector];
[self performSelector:selector withObject:nil afterDelay:0.0];
}

return YES;
}

// iOS原生方法访问相册
- (void)openCamera
{
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:picker animated:YES completion:nil];
}

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let webView = UIWebView(frame: view.bounds)
webView.delegate = self
webView.backgroundColor = .gray
webView.scalesPageToFit = true
view.addSubview(webView)

let url = Bundle.main.url(forResource:"index", withExtension:"html")!
webView.loadRequest(URLRequest(url: url))
}

// iOS原生方法访问相册
@objc fileprivate func openCamera() {
let pickerVC = UIImagePickerController()
pickerVC.sourceType = .photoLibrary
self.present(pickerVC, animated: true, completion: nil)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 代理 UIWebViewDelegate
extension ViewController: UIWebViewDelegate {
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {

let url = request.url?.absoluteString
let range = url?.range(of: "mengyueping.com://") //自定义协议
guard let _ = range else {
return true
}

let str = url?.substring(from: "mengyueping.com://".endIndex)
guard let selStr = str else {
return true
}
let selector = Selector(selStr)
perform(selector)

return true
}
}

使用WKWebView监听JS函数调用

OC版本

1
2
3
4
5
6
7
8
9
10
// app注册方法,供JS调用
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
webView.UIDelegate = self;
webView.navigationDelegate = self;
webView.backgroundColor = [UIColor grayColor];
[self.view addSubview:webView];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 代理 WKNavigationDelegate
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{ //页面重定向时调用,不是每次都调用,不准确
NSLog(@"navigation: %@",navigation);
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{//在发送请求之前,决定是否跳转,可以截获发送的请求

NSString *url = navigationAction.request.URL.absoluteString;
NSRange range = [url rangeOfString:@"mengyueping.com://"]; //自定义协议
NSUInteger location = range.location;

if (location != NSNotFound) {
NSString *str = [url substringFromIndex:(location + range.length)];
SEL selector = NSSelectorFromString(str);
//警告:PerformSelector may cause a leak because its selector is unknown
//[self performSelector:selector];
[self performSelector:selector withObject:nil afterDelay:0.0];
}

decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{// 在收到响应后,决定是否跳转,可以截获服务器的响应数据
decisionHandler(WKNavigationResponsePolicyAllow);
}

// iOS原生方法访问相册
- (void)openCamera
{
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:picker animated:YES completion:nil];
}

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.backgroundColor = .gray
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)

let url = Bundle.main.url(forResource:"index", withExtension:"html")!
webView.load(URLRequest(url: url))
}

// iOS原生方法访问相册
@objc fileprivate func openCamera() {
let pickerVC = UIImagePickerController()
pickerVC.sourceType = .photoLibrary
self.present(pickerVC, animated: true, completion: nil)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 代理 WKNavigationDelegate
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
//页面重定向时调用,不是每次都调用,不准确
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// 在发送请求之前,决定是否跳转,可以截获发送的请求
decisionHandler(.allow)

let url = navigationAction.request.url?.absoluteString
let range = url?.range(of: "mengyueping.com://") //自定义协议
guard let _ = range else {
return
}

let str = url?.substring(from: "mengyueping.com://".endIndex)
guard let selStr = str else {
return
}
let selector = Selector(selStr)
perform(selector)

}

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
// 在收到响应后,决定是否跳转,可以截获服务器的响应数据
decisionHandler(.allow)
}

func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print(#function) //页面开始加载时调用
// 不是每次都调用,只有decisionHandler(.allow)时才能调用此方法
}
}

WKWebView与JS之间的通信

iOSWebKit中,为JavaScript提供了一个发消息的通道,是通过WKUserContentController类实现的,可以通过配置类WKWebViewConfiguration配置到WKWebView对象上。并且WKUserContentController的对象可以添加一个脚本信息处理器,(addScriptMessageHandler: name:add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)),通过实现协议WKScriptMessageHandler来接收处理JS脚本发送过来信息。

WKWebViewConfiguration:是WKWebView初始化时的配置类,里面存储着初始化WKWebView的一系列属性。
WKUserContentController:为JavaScript提供了一个发送消息的通道,并且可以向页面注入JavaScript的类。可以在配置类> WKWebViewConfiguration属性中,配置此类。
WKScriptMessageHandler:一个协议,协议只有一个方法,页面执行特定JavaScript的一个回调,这个特定JavaScript格式为window.webkit.messageHandlers.<name>.postMessage(<messageBody>);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- index.html 要加载的HTML,及通信脚本 -->
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<br>
<button onclick="sendMsgToiOS()">点击给iOS原生发送消息</button>
<a href="https://baidu.com">百度</a>
</body>
</html>
<script>
function sendMsgToiOS() {

var ua = navigator.userAgent.toLowerCase();
if (!!ua.match(/\(i[^;]+;( u;)? cpu.+mac os x/)) { // iOS Mac OS

var JSCallIOS = function () {
//发送给 iOS 原生的 json 数据
var message = {
'method': 'push',
'name' : '王小锌',
'title' : '朋友圈',
'url': 'http://www.baidu.com'
};
//JS 脚本向 iOS原生传递消息
window.webkit.messageHandlers.JSMessageToIOS.postMessage(message);
}
JSCallIOS();

} else if (/android/.test(ua)) { // 安卓
window.android.finish();
}
}
</script>

OC版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 为JS提供了一个发送消息的通道,且可以向页面注入JS的类。
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"JSMessageToIOS"]; // <WKScriptMessageHandler>

// 添加一个脚本信息处理器。self遵守协议WKScriptMessageHandler
// 脚本信息处理器,可以接收JS脚本发送过来的消息。JS脚本通过`window.webkit.messageHandlers.<name>.postMessage(<messageBody>)`发送消息。
// 脚本处理器中监听的名字是js脚本里面消息发送的名字。 window.webkit.messageHandlers.JSMessageToIOS.postMessage(message);

// 配置
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController; //配置消息通道

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
webView.UIDelegate = self; //<WKUIDelegate>
webView.navigationDelegate = self; //<WKNavigationDelegate>
webView.backgroundColor = [UIColor grayColor];
[self.view addSubview:webView];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma mark - 协议 WKScriptMessageHandler
// 当JS给OC发送消息时,此回调中执行消息处理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
NSLog(@"JS传递过来的消息-message.body: %@",message.body);

//收到JS传递过来的消息回调,可以做一些原生想要做的事情。--> JS向原生OC传递消息。
//发送网络请求,页面跳转,打开相机等

[self openCamera];
}

// iOS原生方法访问相册
- (void)openCamera
{
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:picker animated:YES completion:nil];
}

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 为JS提供了一个发送消息的通道,且可以向页面注入JS的类。
let userContentController = WKUserContentController()
userContentController.add(self, name: "JSMessageToIOS")
// 添加一个脚本信息处理器。self遵守协议WKScriptMessageHandler
// 脚本信息处理器,可以接收JS脚本发送过来的消息。JS脚本通过`window.webkit.messageHandlers.<name>.postMessage(<messageBody>)`发送消息。
// 脚本处理器中监听的名字是js脚本里面消息发送的名字。 window.webkit.messageHandlers.JSMessageToIOS.postMessage(message);

// 配置
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController

let webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.backgroundColor = .gray
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)

let url = Bundle.main.url(forResource:"index", withExtension:"html")!
webView.load(URLRequest(url: url))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension ViewController: WKScriptMessageHandler {

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// 当JS给OC发送消息时,此回调中执行消息处理
print("JS传递过来的消息-message.body:\(message.body), name:\(message.name)");
//iOS和js统一好的名字:message.name

//收到JS传递过来的消息回调,可以做一些原生想要做的事情。--> JS向原生OC传递消息。
//发送网络请求,页面跳转,打开相机等

let vc = UIViewController()
vc.title = "name:\(message.name)"
vc.view = {
let v = UITextView(frame: self.view.bounds)
v.text = "body: \(message.body)"
return v
}()
let nav = UINavigationController(rootViewController: vc)
self.present(nav, animated: true, completion: nil)
}
}

iOS中利用JavaScriptCore交互JS

JavaScriptCore.frameworkiOS7以后推出的一个JSOC交互的框架,是使用OC语言对WebKitJS引擎进行的封装。之后引入Swift
使用时导入头文件#import <JavaScriptCore/JavaScriptCore.h>import JavaScriptCore。相关类:JSContextJSValueJSManagedValueJSVirtualMachineJSExport

使用JSContext运行JS代码

JSContext是运行JS代码的环境,一个JSContext是一个全局环境的实例。JSContext类似window对象。通过对象方法- (JSValue *)evaluateScript:(NSString *)script;来运行JS代码。

JSValue包含了每一个JS类型的值。通过JSValue可以将OC中的类型转换为JS中的类型,也可将JS中的类型转为OC中的类型。类型对照如下:


OC Swift type | JavaScript type
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
nil | undefined
NSNull | null
NSString String | string
NSNumber | number, boolean
NSDictionary Dictionary | Object object
NSArray Array | Array object
NSDate | Date object
NSBlock (1) | Function object (1)
id (2) | Wrapper object (2)
Class (3) | Constructor object (3)

JSValue都是通过JSContext返回或者创建的,并没有构造方法。每一个JSValue对象都要强引用关联一个JSContext。当与JSContext对象关联的所有JSValue释放后,JSContext也会释放。我们对JS的数值、对象、函数等进行操作,都是通过JSValue对象来实现的。JSValueJSOC/Swift数据和方法的桥梁,封装了JSOC/Swift中的对应的类型,以及如何通过OC/Swift方法调用JS函数的API

初始化JSContext对象及异常处理

OC/Swift异常会在运行时被Xcode捕获,JSContext中执行的JS如果出现异常,只会被JSContext捕获并存储在exception属性上,而不会向外抛出。时刻检查JSContext对象的exception是否为nil显然是不合适的,更合理的方法是,给JSContext对象设置exceptionHandler回调属性,它接受的是void(^exceptionHandler)(JSContext *context, JSValue *exceptionValue)形式的block,或((JSContext?, JSValue?) -> Swift.Void)形式的闭包。其默认值就是将传入的exceptionValue赋给传入的contextexception属性。这样JS运行发生异常的时候,在Block/Closure中可以立即知道,通过设置Block/Closure中参数context.exception属性,可以观察和记录语法、类型以及运行时错误。

OC版本

1
2
3
4
5
6
7
8
9
10
11
12
JSContext *context = [[JSContext alloc] init];
// 异常处理回调
context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"%@", exception);
context.exception = exception;
/*
此处打印js异常错误,JSContext不会主动抛出js异常。
常见异常:
ReferenceError: Can't find variable:
TypeError: undefined is not an object
*/
};

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MARK: 初始化JSContext对象及异常处理
lazy var context = JSContext()!
context.exceptionHandler = { (context, exception) in
guard let excep = exception else {
return
}
print(excep)
/*
此处打印js异常错误,JSContext不会主动抛出js异常。
常见异常:
ReferenceError: Can't find variable:
TypeError: undefined is not an object
*/
context?.exception = excep
}

执行带返回结果js脚本

如果执行的js脚本有返回结果,则- (JSValue *)evaluateScript:(NSString *)script;执行后返回的JSValue对象,包含返回的结果。可以解析为JS类型,也可以调用JSValue对象API方法,解析为OC对象。

OC版本

1
2
3
NSString *jsStr1 = @"1+2";
JSValue *value1 = [context evaluateScript:jsStr1]; // 返回的JSValue对象,存储的是js计算的返回结果。不存储定义的变量。
NSLog(@"value1 JS: %@ -> OC: %@",value1, value1.toNumber); // 3 3

Swift版本

1
2
3
4
5
// MARK: 执行带返回结果js脚本
let jsStr1 = "1+2"
let jsvalue1 = context.evaluateScript(jsStr1)
print("value1 JS: \(String(describing: jsvalue1)) -> Swift: \(String(describing: jsvalue1?.toNumber()))")
// value1 JS: Optional(3) -> Swift: Optional(3)

执行不带返回结果js脚本

如果执行的js脚本没有返回结果,则- (JSValue *)evaluateScript:(NSString *)script;执行后返回的JSValue对象,解析为JSundefined,对应的OC(null)

OC版本

1
2
3
NSString *jsStr2 = @"var a = 1; var b = 2;";
JSValue *value2 = [context evaluateScript:jsStr2];
NSLog(@"value2 JS: %@ -> OC: %@",value2, value2.toObject); // value2 JS: undefined -> OC: (null)

Swift版本

1
2
3
4
5
// MARK: 执行不带返回结果js脚本
let jsStr2 = "var a = 1; var b = 2;"
let jsvalue2 = context.evaluateScript(jsStr2)
print("value2 JS: \(String(describing: jsvalue2)) -> Swift: \(String(describing: jsvalue2?.toObject))");
//value2 JS: Optional(undefined) -> Swift: Optional((Function))

取出js脚本执行后存储在JSContext对象中的变量

JSContext对象调用方法- (JSValue *)evaluateScript:(NSString *)script;来执行js脚本时,会把JS中定义的变量函数等存储到JSContext对象中,并且可以通过key-value方法取出,取出获得的是JSValue对象。这样就可以通过取出的JSValue对象转换js脚本中定义的JS变量为OC对象来使用,或可以通过取出的JSValue对象调用js脚本中定义的JS函数。(调用JSValueAPI

OC版本

1
2
3
4
5
NSString *jsStr2 = @"var a = 1; var b = 2;";
JSValue *value2 = [context evaluateScript:jsStr2];
JSValue *jsValueA = context[@"a"]; // 运行js,定义的变量,存储在context中。
JSValue *jsValueB = context[@"b"];
NSLog(@"JS a: %@; JS a: %@", jsValueA, jsValueB); // JS a: 1; JS a: 2

Swift版本

1
2
3
4
// MARK: 取出js脚本执行后存储在JSContext对象中的变量
let jsValueA = context.objectForKeyedSubscript("a")
let jsValueB = context.objectForKeyedSubscript("b")
NSLog("JS a: \(String(describing: jsValueA)); JS b: \(String(describing: jsValueB))") // JS a: Optional(1); JS b: Optional(2)

取出存储的js变量,并修改

对于JSArrayObject类型,JSValue也可以通过下标直接取值和赋值。

OC版本

1
2
3
4
5
6
7
8
NSString *jsStr3 = @"var arr = [88, 'mengyueping', 66];";
JSValue *value3 = [context evaluateScript:jsStr3];
JSValue *jsArr = context[@"arr"];
NSLog(@"value3 JS: %@ -> OC: %@",value3, value3.toObject); // value2 JS: undefined -> OC: (null)
NSLog(@"JS Array: %@; Length: %@; jsArr[0]:%@", jsArr, jsArr[@"length"], jsArr[0]); // JS Array: 20,10,www.mengyueping.com; Length: 3; jsArr[0]:20
jsArr[0] = @"www.";
jsArr[2] = @".com";
NSLog(@"通过下标对js取值赋值:JS Array: %@; Length: %@; jsArr[0]:%@", jsArr, jsArr[@"length"], jsArr[0]); // JS Array: 20,10,www.mengyueping.com; Length: 3; jsArr[0]:20

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
// MARK: 取出存储的js变量,并修改
let jsStr3 = "var arr = [88, 'mengyueping', 66];"
let value3 = context.evaluateScript(jsStr3)
let jsArr = context.objectForKeyedSubscript("arr")
print("value3 JS: \(String(describing: value3)) -> Swift: \(String(describing: value3?.toObject))");
// value3 JS: Optional(undefined) -> Swift: Optional((Function))
print("JS Array: \(String(describing: jsArr)); Length: \(String(describing: jsArr?.objectForKeyedSubscript("length"))); jsArr[0]:\(String(describing: jsArr?.objectAtIndexedSubscript(0)))" )
// JS Array: Optional(88,mengyueping,66); Length: Optional(3); jsArr[0]:Optional(88)
jsArr?.setValue("www.", at: 0)
jsArr?.setValue(".com", at: 2)
print("通过下标对js取值赋值:JS Array: \(String(describing: jsArr)); Length: \(String(describing: jsArr?.objectForKeyedSubscript("length"))); jsArr[0]:\(String(describing: jsArr?.objectAtIndexedSubscript(0)))" )
// 通过下标对js取值赋值:JS Array: Optional(www.,mengyueping,.com); Length: Optional(3); jsArr[0]:Optional(www.)

取出存储的js集合对象,并转为OC数组对象

OC版本

1
2
3
4
5
6
7
8
9
10
11
12
NSString *jsStr3 = @"var arr = [88, 'mengyueping', 66];";
JSValue *value3 = [context evaluateScript:jsStr3];
JSValue *jsArr = context[@"arr"];
NSArray *ocArr = jsArr.toArray;
NSLog(@"js Arr -> OC Arr: %@", ocArr);
/*
js Arr -> OC Arr: (
"www.",
mengyueping,
".com"
)
*/

Swift版本

1
2
3
4
5
let swiftArr = jsArr?.toArray()
print("js Arr -> Swift Arr: \(String(describing: swiftArr))");
/*
js Arr -> Swift Arr: Optional([www., mengyueping, .com])
*/

取出存储的js集合对象,并直接使用OC对象给js对象赋值

JSValue是遵循JS的数组特性:没有下标越位,自动延展数组大小。即:集合中没有的下标,元素会自动补空。并且通过JSValue还可以获取JS对象上的属性,比如:JS数组的长度“length”。

OC版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NSString *jsStr3 = @"var arr = [88, 'mengyueping', 66];";
JSValue *value3 = [context evaluateScript:jsStr3];
JSValue *jsArr = context[@"arr"];
jsArr[8] = @8;
NSLog(@"通过下标对js取值赋值:JS Array: %@; Length: %@; jsArr[0]:%@", jsArr, jsArr[@"length"], jsArr[0]);
// 通过下标对js取值赋值:JS Array: www.,mengyueping,.com,,,,,,8; Length: 9; jsArr[0]:www.
NSLog(@"js Arr -> OC Arr: %@", jsArr.toArray);
/*
js Arr -> OC Arr: (
"www.",
mengyueping,
".com",
"<null>",
"<null>",
"<null>",
"<null>",
"<null>",
8
)
*/

Swift版本

1
2
3
4
5
6
7
8
// MARK: 取出存储的js集合对象,并直接使用Swift对象给js对象赋值
jsArr?.setValue(8, at: 8)
print("通过下标对js取值赋值:JS Array: \(String(describing: jsArr)); Length: \(String(describing: jsArr?.objectForKeyedSubscript("length"))); jsArr[0]:\(String(describing: jsArr?.objectAtIndexedSubscript(0)))" )
// 通过下标对js取值赋值:JS Array: Optional(www.,mengyueping,.com,,,,,,8); Length: Optional(9); jsArr[0]:Optional(www.)
print("js Arr -> Swift Arr: \(String(describing: jsArr?.toArray()))")
/*
js Arr -> Swift Arr: Optional([www., mengyueping, .com, <null>, <null>, <null>, <null>, <null>, 8])
*/

取出存储的js函数,并执行

使用JSValueAPI执行JS函数,且有参数的,可以传参。

OC版本

1
2
3
4
5
6
7
[context evaluateScript:@"function add(a, b){ return a + b; }"];
JSValue *addValue = context[@"add"]; // js函数
NSLog(@"Func: %@", addValue); // Func: function add(a, b){ return a + b; }

// 取出js函数,调用函数 - (JSValue *)callWithArguments:(NSArray *)arguments;
JSValue *sum = [addValue callWithArguments:@[@1,@2]];
NSLog(@"Sum: %d", sum.toInt32); // Sum: 3

Swift版本

1
2
3
4
5
6
7
// MARK: 取出存储的js函数,并执行
context.evaluateScript("function add(a, b){ return a + b; }")
let addValue = context.objectForKeyedSubscript("add") // js函数
print("Func: \(String(describing: addValue))") // Func: Optional(function add(a, b){ return a + b; })
// 取出js函数,调用函数 open func call(withArguments arguments: [Any]!) -> JSValue!
let sum = addValue?.call(withArguments:[1,2])
print("Sum: \(String(describing: sum?.toInt32()))") // Sum: Optional(3)

调用js函数的另一种简单方法

不必像上面一样先取出存储的JS函数,再执行JS函数。直接使用JSContext对象的JSValue *globalObject;属性,调用JSValue对象的- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;方法。如果定义的JS函数是全局函数,应该用JSContextglobalObject对象调用该方法。如果是某JS对象的方法,就应该用相应的JSValue对象调用。

OC版本

1
2
3
JSValue *jsValue = [context evaluateScript:@"function multiply(a, b){ return a * b; }"];
JSValue *multiplyValue = [jsValue.context.globalObject invokeMethod:@"multiply" withArguments:@[@3,@6]]; //第一种形式
NSLog(@"multiplyValue: %d",multiplyValue.toInt32); // multiplyValue: 18

或者:

1
2
3
JSValue *jsValue = [context evaluateScript:@"function multiply(a, b){ return a * b; }"];
JSValue *multiplyValue = [context.globalObject invokeMethod:@"multiply" withArguments:@[@3,@6]]; //第二种形式
NSLog(@"multiplyValue: %d",multiplyValue.toInt32); // multiplyValue: 18

Swift版本

1
2
3
4
// MARK:  调用js函数的另一种简单方法
let jsValue = context.evaluateScript("function multiply(a, b){ return a * b; }")
let multiplyValue = jsValue?.context.globalObject.invokeMethod("multiply", withArguments: [3,6])
print("multiplyValue: \(String(describing: multiplyValue?.toInt32()))"); // multiplyValue: Optional(18)
1
let multiplyValue = context.globalObject.invokeMethod("multiply", withArguments: [3,6])

把OC中Block转换成JS函数,并存储到JSContext对象

通过Block可以实现在OC/Swift中定义JS函数,并且在JS运行环境中调用该JS函数,函数执行可以成功的回到OC/SwiftBlock/Closure代码中,而且遵循JS方法的各种特点(比如:方法参数不固定)。JSContext提供了类方法来获取参数列表 (+(NSArray *)currentArguments)和当前调用该方法的对象(+ (JSValue *)currentThis)JS函数中this的输出的内容是GlobalObject,也是JSContext对象方法 -(JSValue *)globalObject;所返回的内容。因为在JS里面,所有全局变量和方法其实都是一个全局变量(GlobalObject)的属性,在浏览器中是window对象。如下,使用Block/Closure定义一个JS函数log。并在JS脚本中调用log函数,并使用OC执行此脚本。

OC版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
context[@"log"] = ^(){
NSLog(@"++++++Begin Log++++++");

NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@",jsVal);
}

JSValue *this = [JSContext currentThis];
NSLog(@"this: %@", this);

NSLog(@"---End Log------");
};
// 执行js,调用使用Block自定义的js函数
[context evaluateScript:@"log('mengyueping', [10,20], {'hello': 'world', 'number': '100'})"];
/*
++++++Begin Log++++++
mengyueping
10,20
[object Object]
this: [object GlobalObject]
---End Log------
*/

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// MARK: 把Swift中Closure转换成JS函数,并存储到JSContext对象
let block: @convention(block) () -> () = {
print("++++++Begin Log++++++")

let args = JSContext.currentArguments()

for jsVal in args! {
print(jsVal)
}

let this = JSContext.currentThis()
print("this: \(String(describing: this))")

print("---End Log------")
}
context.setObject(block, forKeyedSubscript: NSString(string: "log"))
// 执行js,调用使用Block自定义的js函数
context.evaluateScript("log('mengyueping', [10,20], {'hello': 'world', 'number': '100'})")
/*
++++++Begin Log++++++
mengyueping
10,20
[object Object]
this: Optional([object GlobalObject])
---End Log------
*/

JSContextJSValue实例使用下标的方式我们可以很容易地访问我们之前创建的context的任何值。JSContext需要一个字符串下标,而JSValue允许使用字符串或整数作为下标来得到里面的JS对象和数组。

Block可以传入JSContext作方法,但是JSValue没有toBlock方法来把JS方法变成Block/ClosureOC/Swift中使用。但是,JSValue提供了-(JSValue *)callWithArguments:(NSArray *)arguments;方法,可以反过来将函数参数传进去。

BlockJavaScriptCore中起到强大作用,它为JSOC之间的转换建立起更多的桥梁,让转换更方便。但需要注意:

  • block内部使用外部定义创建的对象,block会对其做强引用,而JSContext也会对被赋予的block做强引用,这样它们之间就形成了循环引用(Circular Reference)使得内存无法正常释放。
  • block内部使用外部定义创建的JSValue对象,也会造成循环引用,因为每个JSValue上都有JSContext的引用(@property (readonly, strong) JSContext *context;)JSContext再引用Block同样也会形成循环引用。
  • 无论是把Block传给JSContext对象,让其变成JS方法;还是把它赋值给exceptionHandler属性;在Block内都不要直接使用其外部定义的JSContext/JSValue对象,应该将其当做参数传入到Block中,或者通过JSContext的类方法+(JSContext *)currentContext;来获得。否则会造成循环引用使得内存无法被正确释放。

JSContext结合UIWebView处理HTML中事件监听

JSContext结合UIWebView,当点击JS函数时,响应OC/Swift操作。通过UIWebView的方法JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];获取JSContext对象。 Swift中是let context = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext")

WKWebView不支持通过KVC的方式创建JSContext,所以不能在WKWebView中使用JavaScriptCore
WKWebViewOC/SwiftJS交互的方式,更简洁,因此也用不到JavaScriptCore

加载的HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JavaScriptCore</title>
<script>
function showAlert(content) {
asyncAlert(content);
document.getElementById("js-iOS-js-argsValue").value = content;
}
function asyncAlert(content) {
setTimeout(function(){ alert(content); }, 1);
}
</script>
</head>
<body>
<h1>这是按钮调用</h1>
<input id='jsBtn' type="button" style="width:300px;height:50px;" value="点击Html按钮,调用OC/Swift要执行的代码,&#13;&#10;接收JS传递给OC/Swift的参数" onclick="handleJSToiOS('触发了Html中标签的点击事件,触发JS函数调用,js->OC/Swift->JS')" />
<br/>
<br/>
<textarea id ="js-iOS-js-argsValue" type="value" rows="5" cols="50">
</body>
</html>

OC版本

代码context[@"jsMethodName"] = ^(){//执行的OC代码};其中,jsMethodNameJS中触发事件的方法名字;^(){//执行的OC代码} 这个block通过JSContext对象变成名字为jsMethodNameJS方法;所以当触发Html点击事件所监听的jsMethodName方法时,就等于触发了OCBlock中的代码。

Block中的执行环境是子线程。可以更新部分UIview设置背景色、调用webView执行js。弹出原生alertViewCrash子线程操作UI的错误信息。

Block避免循环引用,因为block会持有外部变量,而JSContext也会强引用它所有的变量,self使用weakSelfblock内不要使用外部的JSContext对象、JSValue对象。如果要使用JSContext对象,可以使用[JSContext currentContext],也可以把JSContext对象、JSValue对象当做block的参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.view.backgroundColor = [UIColor whiteColor];
CGRect webFrame = CGRectMake(30, 80, self.view.bounds.size.width-60, self.view.bounds.size.height-100);
UIWebView *webView = [[UIWebView alloc] initWithFrame:webFrame];
NSURL *htmlUrl = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
// NSURL *htmlUrl = [NSURL URLWithString:@""];
NSURLRequest *request = [NSURLRequest requestWithURL:htmlUrl];

webView.scrollView.bounces = NO; //关闭webView的回弹效果
webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;//UIWebView滚动的比较慢,这里设置为正常速度

[webView loadRequest:request];
[self.view addSubview:webView];
self.webView = webView;
self.webView.delegate = self;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
__weak typeof (self) weakSelf = self;

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

context[@"handleJSToiOS"] = ^(){
NSLog(@"CurrentThread: %@",[NSThread currentThread]); //此Block是子线程

// 获取js函数传入的参数
NSArray *args = [JSContext currentArguments];
for (int i = 0; i<args.count; i++) {
NSLog(@"args[%d]: %@",i,args[i]);
}

// 使用JSContext执行JS代码,将JS传递给OC/Swift的数据,传递回JS
//方法一:
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')",args[0]];
[[JSContext currentContext] evaluateScript:jsStr];

//方法二:
[[JSContext currentContext][@"showAlert"] callWithArguments:args];

// 修改原生UI
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //回到主线程

weakSelf.view.backgroundColor = [UIColor orangeColor];

// BOM操作
// [weakSelf.webView goBack];
// [weakSelf.webView goForward];
// [weakSelf.webView reload];
});

// 播放系统音效
AudioServicesPlaySystemSound(1007); // 1007是系统声音的编号

/*
此处可以执行的任务:
获取地理位置信息、调用相机、扫一扫二维码、调用系统分享面板、更改原生控件属性样式(回到主线程)、
原生调用支付(JS把支付参数传递给OC/Swfit进行支付、OC/Swfit把支付结果反馈给JS)、
摇一摇、播放系统音效、
*/
};
}

Swift版本

代码jscontext.setObject(block, forKeyedSubscript: NSString(string: "handleJSToiOS"))其中,block是自己定义的一个@convention(block)ClosurehandleJSToiOSJS中触发事件的方法名字;这个自定义的闭包通过JSContext对象,将存储的代码块变成名字为jsMethodNameJS方法;所以当触发Html点击事件所监听的jsMethodName方法时,就等于触发了SwiftClosure中的代码。

Closure中的执行环境是子线程。可以更新部分UIview设置背景色、调用webView执行js。弹出原生alertViewCrash子线程操作UI的错误信息。

Closure避免循环引用,因为Closure会持有外部变量,而JSContext也会强引用它所有的变量,闭包中声明self为弱引用[weak self]Closure内不要使用外部的JSContext对象、JSValue对象。如果要使用JSContext对象,可以使用JSContext.current(),也可以把JSContext对象、JSValue对象当做Closure的参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ViewController: UIViewController {
deinit {
print(#function)
}
lazy var webView: UIWebView = {
let web = UIWebView(frame: CGRect(x: 30, y: 80, width: UIScreen.main.bounds.width-60, height: UIScreen.main.bounds.height-160))
web.delegate = self
return web
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(webView)
// let url = URL(string: "https://www..com")!
let url = Bundle.main.url(forResource:"index", withExtension:"html")!
webView.loadRequest(URLRequest(url: url))
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// MARK: 代理 UIWebViewDelegate
extension ViewController: UIWebViewDelegate {
func webViewDidFinishLoad(_ webView: UIWebView) {
print(#function)

let context = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext")
guard let jsContext = context else { return }
let jscontext = jsContext as! JSContext

let block: @convention(block) () -> () = { [weak self] in
print("CurrentThread: \(Thread.current)") //此Closure是子线程

// 获取js函数传入的参数
let args = JSContext.currentArguments()
guard let argments = args else { return }
var i = 0
for item in argments {
print("args[\(i)]: \(item)")
i += 1
}

// 使用JSContext执行JS代码,将JS传递给OC/Swift的数据,传递回JS
//方法一:
// JSContext.current().evaluateScript("showAlert('\(argments[0])')")
//方法二:
JSContext.current().objectForKeyedSubscript("showAlert").call(withArguments: argments)
// JSContext.current().objectForKeyedSubscript("alert").call(withArguments: argments)



// 修改原生UI
DispatchQueue.main.async { //回到主线程
self!.view.backgroundColor = UIColor.orange

// BOM操作
// self!.webView.reload()
// self!.webView.goForward()
// self!.webView.goBack()
}

// 播放系统音效
AudioServicesPlaySystemSound(1007)

/*
此处可以执行的任务:
获取地理位置信息、调用相机、扫一扫二维码、调用系统分享面板、更改原生控件属性样式(回到主线程)、
原生调用支付(JS把支付参数传递给OC/Swfit进行支付、OC/Swfit把支付结果反馈给JS)、
摇一摇、播放系统音效、
*/
}
jscontext.setObject(block, forKeyedSubscript: NSString(string: "handleJSToiOS"))
}
}

JSVirtualMachine

JSVirtualMachineJS脚本的执行提供底层资源。一个JSVirtualMachine实例,代表一个独立的JS对象空间,并为其执行提供资源。它通过加锁,保证JSVirtualMachine是线程安全的,如果要并发执行JS,那我们必须创建多个独立的JSVirtualMachine实例,在不同的实例中执行JS(有点像JS引擎,如V8)。有独立的堆空间和垃圾回收机制。处理线程相关,使用较少。

通过alloc/init就可以创建一个JSVirtualMachine对象,但是我们一般不用新建JSVirtualMachine对象,因为创建JSContext时,如果我们不提供一个自己创建的JSVirtualMachine,内部会自动创建一个JSVirtualMachine对象。JSContext对象管理JSVirtualMachine对象的生命周期。

JSVirtualMachine创建方式:
方式一,创建JSContext对象时,内部自动创建一个新的JSVirtualMachine对象。

1
2
//OC
JSContext *context = [[JSContext alloc] init];

1
2
//Swift
let context = JSContext()

方式二,自己创建一个JSVirtualMachine对象,传入的创建JSContext对象中。

1
2
3
//OC
JSVirtualMachine *jsVM = [[JSVirtualMachine alloc] init];
JSContext *context = [[JSContext alloc] initWithVirtualMachine:jsVM]; //传入的JSVirtualMachine对象不能为空

1
2
3
//Swift
let jsVM = JSVirtualMachine()
let c = JSContext(virtualMachine: jsVM)

JSVirtualMachineJavaScript的运行提供了底层资源JSContextJavaScript提供了运行环境;
JSContext的创建都是基于JSVirtualMachine
JSValue其实就是JS对象在JSVirtualMachine中的一个强引用。

=================
本文代码,我的Github仓库获取apple-stack

=================
2017.7更新

🐶 客观赏个好评吧 🐶