JS和原生

516 阅读3分钟

1 通信原理

除了JavaScriptCore,JS调用原生只能异步处理返回值

1.1 URL拦截(JS调用原生) + 解析JS(原生调用JS)

1.1.1 基于生命周期的拦截

基于URL协议,JS发送请求,在WebView生命周期方法中拦截到如myURLProtocol://。 这种方法对于UIWebView和WKWebView通用。

  • 1 拦截:针对JS调用原生。 如果JS需要返回值,需要在参数里加上回调函数(或根据协议,约定好怎么回调),然后通过原生调用JS的方式,把返回值传回去。
//WKWebView
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    NSURL *URL = navigationAction.request.URL;
    NSString *scheme = [URL scheme];
    if ([scheme isEqualToString:@"myURLProtocol"]) {
        [self handleCustomAction:URL];    //自己解析协议
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

//UIWebView
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *URL = request.URL;
    NSString *scheme = [URL scheme];
    if ([scheme isEqualToString:@"myURLProtocol"]) {
        [self handleCustomAction:URL];    //自己解析协议
        return NO;
    }
    return YES;
}
  • 2 解析:针对原生调用JS
//WKWebView
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@----%@",result, error);
    }];

//UIWebView
[_webView stringByEvaluatingJavaScriptFromString:jsStr];    //需要自己另外处理回调

iOS下JS与OC互相调用(一)--UIWebView 拦截URL iOS下JS与OC互相调用(二)--WKWebView 拦截URL

###1.1.2 基于NSURLProtocol的拦截

  1. 继承NSURLProtocol,并注册子类。
  2. 重写+ (BOOL)canInitWithRequest:(NSURLRequest *)request等方法
  3. 对于WK,没有官方支持,下文2方案也不完善。

iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求

iOS WKWebView (NSURLProtocol)拦截js、css,图片资源

1.2 基于JavaScriptCore,只适用UIWebView

见第2节

1.3 基于MessageHandle,只适用WKWebView

见第3节

2 基于JavaScriptCore

2.1 JavaScriptCore的构成

JS的引擎,原生和JS互相访问

最早是WebKit中解释执行JS,作用相当于Chrome的V8

  1. JSVirtualMachine 每个虚拟机对应原生的一个线程。 每个虚拟机有自己的GC,多个虚拟机之间的内存无法共享。
  2. JSContext 代表JS的上下文环境(浏览器中的一个tab?) 可以从UIWebView中获取,也可以从本地JS文件中读取。
  3. JSValue 原生调用JS的关键, 通过Context获取到JSValue,可以代表js的一个变量、函数
  4. JSExport协议 JS调用原生的关键,原生通过JSExport协议给JS提供接口。

2.2 实例

2.2.1 原生访问JS的变量,通过context拿到变量i

JSContext *context = [[JSContext alloc] init];
// 解析执行 JavaScript 脚本
[context evaluateScript:@"var i = 4 + 8"];
// 转换 i 变量为原生对象
NSNumber *number = [context[@"i"] toNumber];
NSLog(@"var i is %@, number is %@",context[@"i"], number); 

2.2.2 原生访问JS的函数,将JSValue映射到context中的一个函数。

// 解析执行 JavaScript 脚本
[context evaluateScript:@"function addition(x, y) { return x + y}"];
// 获得 addition 函数
JSValue *addition = context[@"addition"];
// 传入参数执行 addition 函数
JSValue *resultValue = [addition callWithArguments:@[@(4), @(8)]];
// 将 addition 函数执行的结果转成原生 NSNumber 来使用。
NSLog(@"function is %@; reslutValue is %@",addition, [resultValue toNumber]);

2.2.3 JS访问原生,通过JSExport协议

@protocol YYYJSExports <JSExport>
-(void)record:(NSString *)msg;
@end

@interface YYYUIWebViewJSBridge()<YYYJSExports>
@property (nonatomic, strong) JSContext *jsContext;
@end

@implementation YYYUIWebViewJSBridge

+ (void)registWebView:(UIWebView *_Nonnull)webView withId:(NSString *_Nonnull)webViewId {
    if(!webView || !webViewId) return;
    
    SHWUIWebViewJSBridge *bridge = [SHWUIWebViewJSBridge new];
    bridge.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    bridge.jsContext[@"shw_analytics"] = bridge;
    [bridgeContainer setObject:webView forKey:webViewId];
}

+ (void)deRegistWebViewWithId:(NSString *_Nonnull)webViewId {
    if(!webViewId) return;
    
    [bridgeContainer removeObjectForKey:webViewId];
}

- (void)record:(NSString *)msg {
    SHWEvent *event = [SHWEvent initEventWithId:_SW_ANALYTICS_EVENTID_WEBVIEW args:@{__SW_KEY_WebView: msg}];
    [SHWMsgQueue logEvent:event];
}

3 基于MessageHandler

MessageHandler,作用类似于JSExport,为JS调用原生提供接口。 但原生调用JS,不能像JSContext那样直接调用,而是通过通过evaluateJS。

iOS下JS与OC互相调用(三)--MessageHandler

3.1 原生调用JS

    NSString *jsStr2 = @"window.ctuapp_share_img";
    [self.webView evaluateJavaScript:jsStr2 completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@----%@",result, error);
    }];

3.2 JS调用原生

//viewWillAppear:
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"ScanAction"];

//viewWillDisappear:
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"ScanAction"];

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
// message.body -- Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.
    NSLog(@"body:%@",message.body);
    if ([message.name isEqualToString:@"ScanAction"]) {
        NSLog(@"扫一扫");
    } else if ([message.name isEqualToString:@"Location"]) {
        [self getLocation];
    } else if ([message.name isEqualToString:@"Share"]) {
        [self shareWithParams:message.body];
    }
}