《成为大前端》系列 4.6 统一两端 JS 接口和WebView异步调用

767 阅读4分钟

统一两端的 JS 调用接口

最佳实践

前面讲了两种方式返回结果给 JS,使用哪种决定于你的业务。我个人推荐在生成环境中使用 evaluateJavascript, 因为它可以解决异步调用问题,比如拍个照然后把拍的照片数据返回 JS ,当然异步调用也有异步的麻烦,不过接下来的章节我们会轻易解决。

统一接口

在继续之后的章节前,我们做一个优化,统一我们的 JS 调用接口,方便之后的学习。

前面我们两个端调用 Native 的代码如下:

// android
window.androidBridge.callNative(JSON.stringify({ name: 'mingo' })
// iOS
window.webkit.messageHandlers.iOSBridge.postMessage({ name: 'mingo' })

现在提出一个函数:

function callNative(data) {
  // 我们要求data一定是一个json对象,在这里转成string
  let stringData = JSON.stringify(data);

  // 利用判断,决定系统,调用不同的接口
  if (window.androidBridge) {
    window.androidBridge.callNative(stringData);
  } else {
    window.webkit.messageHandlers.iOSBridge.postMessage(stringData);
  }
}

那么无论在哪个端,JS 都可以使用 callNative ,方便许多

callNative({
    name: "mingo"
});

iOS 端改动

func userContentController(_ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage)
{
    // JSON.stringif以后这里已经是String了
    let body = message.body as! String
    // 打印一下
    print("WebView callNative ok. body is \(body)")
    // 在收到js调用后运行以下代码
    webView.evaluateJavaScript("onNativeResult('Native callback ok.')") { _, _ in }
}

异步调用问题之 JSONP 原理

前面讲到推荐在生成环境中使用 evaluateJavascript,但是随后也引入了异步调用,异步调用存在一个普遍的问题:调用和返回结果对应

我们从 JSONP 讲解一下异步调用遇到的这个问题来源和解决思路

假设我们要调用一个 JSONP 服务:http://jsonp.com/query/data callback 参数是cb,那么调用这个 服务的方式如下:

http://jsonp.com/query/data?cb=jsonpCallback

代码实现:

function jsonpCallback(data) {
  console.log(data);
}

function jsonp() {
  var script = document.createElement("script");
  script.src = "http://jsonp.com/query/data?cb=jsonpCallback";
  document.body.appendChild(script);
}

jsonp();

服务端将输出:

window.jsonpCallback({ ... })

但是这里有个问题,当我们同时多次调用 jsonp 时,在 jsonpCallback 里我们无法知道对应的结果 是哪次调用的,因此我们改造一下:

var callbackId = 0

function jsonp(callback) {

    var callbackId = "jsonpCallback_" + (callbackId ++)

    window[callbackId] = function(data) {
        var script = document.getElementById(callbackId)
        script && document.body.removeChild(script)
        delete window[callbackId]
        callback(data)
    }

    var script = document.createElement('script')
    script.id = callbackId
    script.src = "http://jsonp.com/query/data?cb=" + callbackId
    document.body.appendChild(script)
}


jsonp(function(data) {
    console.log(data)
})

jsonp(function(data) {
    console.log(data)
})

... 100// 2020年了,我们还是用箭头函数吧
jsonp((data) => console.log(data))

服务端每次输出:

window.jsonpCallback_0({ ... })
window.jsonpCallback_1({ ... })
...
window.jsonpCallback_100({ ... })

这样每次请求调用和服务端结果返回都有一个一一对应关系,而这就是一个最基本且完整的 JSONP 了。

接下去的章节将讲解,在 WebView 中多次异步调用与结果返回如何做到一一对应,和 JSONP 的原理是一致的。

WebView 异步调用(Android)

前面利用 JSONP 原理讲解一下异步调用遇到的调用与结果需要一一对应的问题和解决思路,现在 我们解决 WebView 下,JS 调用 Native 得到返回结果的一一对应。

JS 端

将 JS 调用 Native 改为类似 jsonp 的方式,但稍微有些许不同,看下面代码:

var currentCallbackId = 0;

function callNative(data, callback) {
  // 生成一个唯一callbackId
  var callbackId = "nativeCallback_" + currentCallbackId++;

  // 给window添加callback
  window[callbackId] = result => {
    delete window[callbackId];
    callback(result);
  };

  var stringData = JSON.stringify(data);
  if (window.androidBridge) {
    // android端传递两个参数
    window.androidBridge.callNative(callbackId, stringData);
  } else {
    // iOS不支持多参数,我们传递json对象
    window.webkit.messageHandlers.iOSBridge.postMessage({
      callbackId: callbackId,
      data: stringData
    });
  }
}

function onClickButton() {
  callNative(
    {
      name: "mingo"
    },
    result => {
      var logEl = document.getElementById("log");
      logEl.innerText += result + "\n";
    }
  );
}

Native 端

@JavascriptInterface
fun callNative(callbackId: String, arg: String) {
    Log.e("WebView", "callNative ok. args is $arg")
    webView.post {
        webView.evaluateJavascript("window.$callbackId('Native callback ok.')", null)
    }
}

运行看效果和之前一样,说明通了。

WebView 异步调用(iOS)

前面利用 JSONP 原理讲解了一下异步调用时遇到的调用与结果需要一一对应的问题与解决思路,现在 我们用同样的思路解决 WebView 下,JS 调用 Native 返回结果一一对应问题。

JS 端

将 JS 调用 Native 改为类似 jsonp 的方式,但稍微有些许不同,看下面代码:

var currentCallbackId = 0;

function callNative(data, callback) {
  // 生成一个唯一callbackId
  var callbackId = "nativeCallback_" + currentCallbackId++;

  // 给window添加callback
  window[callbackId] = result => {
    delete window[callbackId];
    callback(result);
  };

  var stringData = JSON.stringify(data);
  if (window.androidBridge) {
    // android端传递两个参数
    window.androidBridge.callNative(callbackId, stringData);
  } else {
    // iOS不支持多参数,我们传递json对象
    window.webkit.messageHandlers.iOSBridge.postMessage({
      callbackId: callbackId,
      data: stringData
    });
  }
}

function onClickButton() {
  callNative(
    {
      name: "mingo"
    },
    result => {
      var logEl = document.getElementById("log");
      logEl.innerText += result + "\n";
    }
  );
}

Native 端

func userContentController(_ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage)
{
    let body = message.body as! [String: Any]
    print("WebView callNative ok. body is \(body)")
    let callbackId = body["callbackId"] as! String
    webView.evaluateJavaScript("window.\(callbackId)('Native callback ok.')") { _, _ in }
}

运行看效果和之前一样,说明通了。

其实这里原理很简单,一旦知道了就可以在很多架构上使用这个原理,比如 RPC、node 进程间调用,暂且不表。