h5和app的相互调用

6,982 阅读6分钟

背景

由于 h5 的开发成本较低(开发速度快),且支持多端共用(忽略浏览器兼容性),所以经常有企业的原生应用中会嵌入 webview 来达到 h5 替代原生页面的开发模式,我们称为混合开发,其应用称为 Hybrid App。既然如此,就或多或少会涉及 h5 页面和 app 进行通信(交互)的问题,比如 h5 页面调用 app 的相机方法,分享功能等。

以此背景,我们来聊聊 h5 和 app 通信(交互)的方法

说明:由于本人没有接触过 app 的原生开发,所以 app 部分代码使用伪代码实现

1. app 调用 h5 代码

由于 app 是 webview 的宿主,可以直接访问 h5,所以 app 调用 h5 比较简单。其原理是通过访问 JS 的全局对象来访问方法。

首先在 h5 中定义方法

window.interface = {
    sayName(name) {
        return `hi ${name}`;
    }
}

然后在 app 中直接调用就行

webview.evaluateJavascript('window.interface.sayName("xiao ming")', (msg) => {
    // hi xiao ming
});

异步处理

如果 app 调用 h5 的异步代码,如何获取异步结果呢?

首先在 h5 中定义异步方法,在异步方法里面要接收来自 app 的函数体

window.interface = {
    asyncSayName(name, code) {
        setTimeout(() => {
            const fn = new Function(code);

            fn(name);
        }, 1000)
    }
}

然后在 app 中直接调用

webview.evaluateJavascript('window.interface.asyncSayName("xiao ming", code)');

主要区别在于第二个参数 code,如果我们将 code 设置为 h5 调用 app 的方法(具体如何写可以参考 h5 调用 app 的方法),不就能实现 app 调用 h5,再通过 h5 调用 app 获得异步结果了?有点绕吧!

再三思索下,感觉通过 new Function 来实现回调可能会有安全风险,所以更好的实现方式还是通过事件注册分发机制,在 h5 中异步处理完成后主动调用 app 方法来实现(如传递事件 ID 对应的 app 方法),具体如何实现大家可以思考思考。

2. h5 调用 app 方法(h5 -> app)

h5 调用 app 的方法需要多一个步骤,app 先往 JS 环境注入方法

依然是伪代码实现

webview.addJavascriptInterface((delay) => {
    // 延迟唤起相机
}, 'openCamera');

现在我们就可以在 h5 中通过调用 window.openCamera 来执行 app 方法唤起相机了

window.openCamera(1000);

异步处理

我们实现了 h5 调用 app 方法,那又怎么获得异步结果呢?比如,我们在唤起相机后,如何获得相机的拍摄结果呢?

同样可以通过执行回调函数

// 在注册的方法中添加 callback
webview.addJavascriptInterface((delay, callback) => {
    // 延迟唤起相机

    // 在照相后调用callback
    webview.evaluateJavascript('window[callback]("拍摄结果base64")');
}, 'openCamera');

好了,现在可以在 h5 中调用 app 并处理异步结果了

// 首先定义回调函数
window.handleCameraResult = {
    // 拍摄结果处理
}

window.openCamera(1000,  'handleCameraResult');

有个问题是,如果多次调用 app 方法,每次需要对结果对不同的处理呢?定义多个方法?个人认为更好的方式还是利用事件注册分发机制来实现比较好,可以和 app 同学好好约定下。

3. app 中的具体实现

虽然上面使用了伪代码实现 app 代码,但是还是有些不够的,因为如果遇到比较“暖心”的面试官,他大概率还是会问问你 IOS 和 Android 的区别的。好吧,我就是被问过。

既然如此,那就抱着应试的心态了解了解好了

app 调用 h5

在 Android 中,通过 webview.evaluateJavascript 来执行 h5 方法

webview.evaluateJavascript('window.sdk.double(10)', new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String s) {
    // 20
  }
});

在 IOS 中也差不多,通过 webview stringByEvaluatingJavaScriptFromString 来执行 h5 方法

NSString *func = @"window.sdk.double(10)";
NSString *str = [webview stringByEvaluatingJavaScriptFromString:func]; // 20

在 app 中注入 h5 方法

在 Android 中,通过 webview.addJavascriptInterface 接口注入

webview.addJavascriptInterface(new Object() {
  @JavascriptInterface
  public int double(value) {
    return value * 2;
  }
  
  @JavascriptInterface
  public int triple(value) {
    return value * 3;
  }
}, "appSdk");

在 IOS 中有些区别,通过 @"documentView.webView.mainFrame.javaScriptContext" 暴露的接口注入

@interface AppSdk : NSObject
{}
- (int) double:(int)value;
- (int) triple:(int)value;
@end

@implementation AppSdk
- (int) double:(int)value {
  return value * 2;
}
- (int) triple:(int)value {
  return value * 3;
}
@end

JSContext *context=[webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
   
AppSdk *appSdk = [AppSdk new];

context[@"appSdk"] = appSdk;

4. h5 调用 app 的其它方式

前面我们通过 app 往 h5 注入方法的方式实现了 h5 调用 app 方法。其实还有其它的实现方式。

在 webview 中我们发起的请求可以被 app 拦截,还有 console,alert,prompt 也可以被 app 拦截。有这个基础,我们就可以在 app 中实现拦截方法,通过 app 和 h5 的协约来调用 app 方法。

可以看看拦截请求的相关代码

IOS

- (BOOL)webview:(UIWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  // 判断如果 url 是 sdk:// 打头的就拦截掉
  // 然后从 url sdk://action?params 中取出 action 与params

  NSString *urlStr = request.URL.absoluteString;
  
  if ([urlStr hasPrefix:@"sdk://"]) {
    
    // 比如 action = double, params = value=10
    NSString *func = @"window.bridge.getDouble(20)";
    [webview stringByEvaluatingJavaScriptFromString:func];

    return NO;
  }

  return YES;
}

Android

webview.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // 判断如果 url 是 sdk:// 打头的就拦截掉
        // 然后从 url sdk://action?params 中取出 action 与params 
        
        Uri uri = Uri.parse(url);                                 
        if ( uri.getScheme().equals("sdk")) {

            // 比如 action = double, params = value=10
            webview.evaluateJavascript('window.bridge.getDouble(20)');

            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
});

通过拦截请求来调用 app 方法的大致步骤为

  1. 约定自定义协议:sdk://action?params

  2. h5 发起协议调用 app:location.href = sdk://opencamera?1000

  3. app 拦截请求,根据参数 1000 执行 opencamera 函数

  4. 如果需要执行回调函数的话,根据协议约定传函数名或者默认回调函数都行,app 直接调用就行

页面多次调用 location.href 会存在覆盖的情况,可以利用 iframe 来发起请求

5. 新版本 IOS 的实现

大概就是在 IOS8 之后,苹果推出了 WKWebView,该 WKWebView 通过 WKScriptMessageHandler 代理方法可以实现接收接收脚步消息(个人感觉应该是在 webview 实现了 postMessage 相关协议)。

所以首先在 IOS 中配置不同的 MessageHandler 方法,此处省略 xxx

然后在 h5 中通过 postMessage 来调用

window.webkit.messageHandlers.applyRecordVoice.postMessage(null);

在 IOS 中,这种方法应该是比较主流的 H5 调用 app 的方式了

总结

app 和 h5 的通信其实很简单,主要原理是 app 可以调用 h5 方法且可以注入 h5 方法,新版 IOS 的话比较特别,通过 WKScriptMessageHandler 实现。难点可能在于在知道明白通信的方法后,如何更好的封装通信方法,解耦业务逻辑,且同时注重安全性和数据传输的可靠性(网上有文章讨论 WKScriptMessageHandler 的性能及承载量,有兴趣可以了解)

参考


欢迎到前端自习群一起学习~516913974