JSBridge三种实现

2,024 阅读5分钟

JSBridge实现原理

1. 背景

在介绍JSBridge实现原理时,先简单介绍下什么是hybrid APP,在传统的Native开发模式中,每次对功能的迭代更新都意味着一次提审发版本,这导致在需要频繁更新的场景下会有极大限制,这时候采用hybrid APP的开发模式就可以很好的解决这个问题,可以将一些频繁变动更新的内容采用h5进行开发,通过webview引入到Native中。

在这种开发模式下,难免会遇到需要进行交互的场景,例如h5需要实现点击按钮跳转到端内的某个页面或者触发某个原生逻辑,例如在h5中有一个功能需要在原生APP中实现认证才可以使用,那么此时在h5中点击后需要跳转到端内的认证页,怎么解决这个问题呢,这就是JSBridge做的事情了。

2. JSBridge实现原理

JSBridge可以理解为一个双向通信的桥梁,可以支持h5访问原生的摄像头、相册、地理位置的原生功能,支持Native去执行js的功能。交互实现原理分为以下两个场景:

2.1 Native调用js

在Native中都提供了WebView去引入H5网页,并且都提供了evaluateJavaScript方法去直接执行js代码,并且可以获取返回值进行回调,一般情况下,前端和客户端同学会功能维护一份交互文档,里面记录了js侧提供给Native侧调用的方法,例如在客户端处理完认证信息后调用js侧提供的刷新用户信息方法刷新状态。

首先在js侧初始化调用函数:

例如js侧统一提供一个交互入口window.nativeEventCallback(json),可以自定义JSON的格式,例如event、data、version、pkg等字段,在nativeEventCallback方法内部根据协商好的事件及数据做相应的交互处理即可。

/**
 * 初始化Native回调事件
 * @description 该函数需要在初始化页面时候使用
 * @throw 如返回数据为空, 会抛出错误
 */
export const initNativeEventCallback = () => {
  // console.log("initNativeEventCallback", initNativeEventCallback)
​
  window.nativeEventCallback = (json) => {
    console.log("native_json", json)
​
    if (json) {
      let messages = json
      const { event, data, version, pkg } = messages
      // TODO 根据以上字段做对应事情
      
    } else {
      throw "messages is not define."
    }
  }
}

📢注意:该交互代码需要再页面初始化时调用,否则Native无法正常交互。

iOS(Swift):

import WebKit
import UIKitclass WebViewController: UIViewController {
    var webView: WKWebView!
​
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 初始化 WebView
        webView = WKWebView(frame: self.view.bounds)
        self.view.addSubview(webView)
​
        if let url = URL(string: "https://www.example.com") {
            let request = URLRequest(url: url)
            webView.load(request)
        }
    }
    
    // 调用 JavaScript 中的 window.nativeEventCallback(json) 方法
    func callJSFunction() {
        let jsonObject: [String: Any] = [
            "event": "sampleEvent",
            "data": ["key": "value"],
            "version": "1.0",
            "pkg": "com.example"
        ]
        
        // 将 JSON 转换为字符串格式
        let jsonData = try! JSONSerialization.data(withJSONObject: jsonObject)
        let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
        
        // 调用 js 方法
        let jsCode = "window.nativeEventCallback((jsonString));"
        webView.evaluateJavaScript(jsCode) { result, error in
            if let error = error {
                print("JavaScript execution error: (error.localizedDescription)")
            } else {
                print("JavaScript executed successfully")
            }
        }
    }
}

Android(Kotlin):

import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject
​
class WebViewActivity : AppCompatActivity() {
    private lateinit var webView: WebView
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        webView = WebView(this)
        setContentView(webView)
​
        webView.settings.javaScriptEnabled = true
        webView.webViewClient = WebViewClient()
        webView.loadUrl("https://www.example.com")
    }
​
    // 调用 JavaScript 中的 window.nativeEventCallback(json) 方法
    private fun callJSFunction() {
        val jsonObject = JSONObject().apply {
            put("event", "sampleEvent")
            put("data", JSONObject(mapOf("key" to "value")))
            put("version", "1.0")
            put("pkg", "com.example")
        }
        
        val jsCode = "window.nativeEventCallback($jsonObject);"
        
        // 使用 evaluateJavascript 方法调用 js
        webView.evaluateJavascript(jsCode) { result ->
            println("JavaScript execution result: $result")
        }
    }
}

Flutter:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
​
class WebViewExample extends StatefulWidget {
  @override
  _WebViewExampleState createState() => _WebViewExampleState();
}
​
class _WebViewExampleState extends State<WebViewExample> {
  late WebViewController _controller;
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter WebView'),
        actions: [
          IconButton(
            icon: Icon(Icons.send),
            onPressed: _sendMessageToWebView,
          ),
        ],
      ),
      body: WebView(
        initialUrl: 'https://www.example.com',
        javascriptMode: JavascriptMode.unrestricted,
        onWebViewCreated: (WebViewController controller) {
          _controller = controller;
        },
      ),
    );
  }
​
  // 调用 JavaScript 中的 window.nativeEventCallback(json) 方法
  void _sendMessageToWebView() {
    // 构造包含 event、data、version 和 pkg 的 JSON 对象
    final jsonObject = {
      "event": "sampleEvent",
      "data": {"key": "value"},
      "version": "1.0",
      "pkg": "com.example"
    };
​
    // 将 JSON 转换为字符串格式
    final jsonString = jsonEncode(jsonObject);
​
    // 调用 js 方法
    final jsCode = "window.nativeEventCallback($jsonString);";
    _controller.runJavascript(jsCode);
  }
}

2.2 js与Native交互

2.2.1 URL Scheme方案

URL Scheme是一种应用程序间通信常用的方案。 URL Scheme格式:<protocol>://<domain>/<path>?<query> ,例如weixin://dl/business/?t= *TICKET*

在早期的交互方案中,通常使用拦截URL Scheme的方式实现js与Native的交互,实现如下:

步骤一:在Native中,iOS 使用 shouldStartLoadWithRequest,Android 使用 shouldOverrideUrlLoading 拦截并解析 URL,根据协商好的交互逻辑进行处理。

Android:

import android.os.Bundle;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
​
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        WebView webView = new WebView(this);
        setContentView(webView);
        
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                String url = request.getUrl().toString();
                if (url.startsWith("myapp://")) {
                    // 解析自定义 URL scheme
                    System.out.println("Intercepted URL: " + url);
                    // 在此解析参数并执行相应逻辑
                    return true; // 拦截请求,防止加载
                }
                return false; // 允许请求加载
            }
        });
​
        webView.loadUrl("https://www.example.com");
    }
}

iOS:

import WebKit
​
class ViewController: UIViewController, WKNavigationDelegate {
    var webView: WKWebView!
​
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView = WKWebView(frame: view.bounds)
        webView.navigationDelegate = self
        view.addSubview(webView)
        
        if let url = URL(string: "https://www.example.com") {
            webView.load(URLRequest(url: url))
        }
    }
​
    // 拦截并解析 URL 请求
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = navigationAction.request.url, url.scheme == "myapp" {
            // 解析自定义 URL scheme
            print("Intercepted URL:", url)
            if url.host == "action" {
                // 在此解析参数并执行相应逻辑
            }
            decisionHandler(.cancel) // 拦截请求,防止加载
            return
        }
        decisionHandler(.allow) // 允许请求加载
    }
}

步骤二:在js中通过href或者AJAX去访问该URL Scheme,即可完成交互

// a标签
<a href="myapp://action?event=sampleEvent&data=value">点击</a>
// location
window.location.href = "myapp://action?event=sampleEvent&data=value";
// ajax
$ajax.get("myapp://action?event=sampleEvent&data=value")
​
// 或者使用iframe
// 使用iframe 封装 JS-bridge
​
  const sdk = {
    invoke(url, data = {}, onSuccess, onError) {
      const iframe = document.createElement('iframe')
      iframe.style.visibility = 'hidden' // 隐藏iframe
      document.body.appendChild(iframe)
      iframe.onload = () => {
        const content = iframe1.contentWindow.document.body.innerHTML
        onSuccess(JSON.parse(content))
        iframe.remove()
      }
      iframe.onerror = () => {
        onError()
        iframe.remove()
      }
      iframe.src = `myapp://${url}?data=${JSON.stringify(data)}`
    },
    fn1(data, onSuccess, onError) {
      this.invoke('api/fn1', data, onSuccess, onError)
    },
    fn2(data, onSuccess, onError) {
      this.invoke('api/fn2', data, onSuccess, onError)
    },
    fn3(data, onSuccess, onError) {
      this.invoke('api/fn3', data, onSuccess, onError)
    },
  }
优点

简单、兼容性高;适用于较小数据量的交互。

缺点

数据传输容量有限;每次调用都刷新 URL,有性能影响。

2.2.2 向webview注入js接口方式

在高版本的设备和 WebView中,提供了向webview注入js接口的能力,js调用注入的js即可调用原生逻辑:

  • iOS:WKWebView 中的 WKScriptMessageHandler
import WebKit
import UIKit
​
class ViewController: UIViewController, WKScriptMessageHandler {
    var webView: WKWebView!
​
    override func viewDidLoad() {
        super.viewDidLoad()
​
        let config = WKWebViewConfiguration()
        config.userContentController.add(self, name: "nativeHandler") // 注册 js 消息的处理器
​
        webView = WKWebView(frame: self.view.bounds, configuration: config)
        self.view.addSubview(webView)
​
        if let url = URL(string: "https://www.example.com") {
            webView.load(URLRequest(url: url))
        }
    }
​
    // 处理来自 js 的消息
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "nativeHandler" {
            if let body = message.body as? [String: Any] {
                print("Received message from js:", body)
                // 处理消息内容,如 event, data 等
            }
        }
    }
}

js侧调用:

window.webkit.messageHandlers.nativeHandler.postMessage({ event: 'sampleEvent', data: 'someData' });
  • Android:使用addJavascriptInterface
import android.content.Context;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
​
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
​
        WebView webView = new WebView(this);
        setContentView(webView);
​
        // 启用 JavaScript
        webView.getSettings().setJavaScriptEnabled(true);
​
        // 添加 JavaScript 接口
        webView.addJavascriptInterface(new JSBridgeInterface(this), "jsBridge");
​
        // 加载网页
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("https://www.example.com");
    }
​
    public class JSBridgeInterface {
        private Context context;
​
        public JSBridgeInterface(Context context) {
            this.context = context;
        }
​
        @JavascriptInterface
        public void sendMessage(String json) {
            // 处理来自 JavaScript 的消息
            System.out.println("Received message from js: " + json);
            // 在此解析 JSON 并执行相应逻辑
            Toast.makeText(context, "Message from js: " + json, Toast.LENGTH_SHORT).show();
        }
    }
}

js侧调用:

window.jsBridge.sendMessage({ event: 'sampleEvent', data: 'someData' });

通过以上代码可以发现,在Android和iOS的实现中,js侧调用的方式不一致,所以一般需要做兼容处理,例如实现一个调用Native系统分享功能:

/**
 * 调用系统功能分享文本
 * @version 1.0.0
 * @param data {content: "xxxxxx", title:"yyyy"}
 * @param data.eventId 该事件动作唯一ID
 */
export const shareText = ({ content, title, eventId = "" }) => {
  const data = JSON.stringify({ content, title, eventId })
​
  return new Promise((resolve) => {
    eventBus.$on("shareText", resolve)
​
    try {
      window.webkit.messageHandlers.shareText.postMessage(data)
    } catch (err) { }
​
    try {
      window.JSBridgeService.shareText(data)
    } catch (err) { }
  })
}

📢注意:需要先与客户端协商对应的交互逻辑,然后在前端侧定义好对应的api

优点:

传输数据量大,可以满足复杂场景

缺点
  • iOS 的 WKScriptMessageHandler:自 iOS 8 起可用。
  • Android 的 addJavascriptInterface:自 Android 4.1 (API 16) 起可用,但在 Android 4.2 及以上版本中使用时需遵循一定的安全要求。

低版本的系统或者webview不支持该方式。

2.2.3 通过WebChromeClient的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt()方法,对消息message进行拦截

这种方案其实跟URL Scheme方案类似,都是通过拦截H5的一些事件进行响应来实现交互。

3. 总结

如果对兼容性要求不高的话,可以使用js接口注入的方案实现js与Native的交互,如果对兼容性有要求的话,则需要降级为URL Scheme方案。