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 UIKit
class 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方案。