主页 > 手机  > 

用命令模式设计一个JSBridge用于JavaScript与Android交互通信

用命令模式设计一个JSBridge用于JavaScript与Android交互通信
用命令模式设计一个JSBridge用于JavaScript与Android交互通信

在开发APP的过程中,通常会遇到Android需要与H5页面互相传递数据的情况,而Android与H5交互的容器就是WebView。

因此要想设计一个高可用的 J S B r i d g e JSBridge JSBridge,不妨可以参考下述示例:

一、传输协议规范

设计一套用于 A n d r o i d Android Android端与 J a v a S c r i p t JavaScript JavaScript传输数据的协议规范,如下所示:

{ "code": "1000001", "msg": "调用成功", "content": { "model": "NOH-AL00", "brand": "HUAWEI" } }

其中

code 字段用来表示调用的状态码msg 字段用来表示调用信息content 字段用来传输数据

既然是要设计到Android与JavaScript两个交互,就必然会涉及

Android端传输数据给JavaScript

一般是通过 w e b V i e w . e v a l u a t e J a v a s c r i p t ( j a v a S c r i p t C o d e , n u l l ) webView.evaluateJavascript(javaScriptCode, null) webView.evaluateJavascript(javaScriptCode,null)

JavaScript端传输数据给Android

J S B r i d g e . c a l l N a t i v e M e t h o d ( ) JSBridge.callNativeMethod() JSBridge.callNativeMethod()

其中要求Android端会有个统一入口,方法名叫做callNativeMethod ,然后会暴露一个JavaScript的入口webView.addJavascriptInterface(JSBridge(this, webView), “JSBridge”)

二、Android端接口

设计一个JSInterface接口,来执行Javascript调用Android回调

interface JSInterface { fun callback(webView: WebView, params: String, successFunction: String, failFunction: String?) }

让一个抽象类BaseJavaScriptHandler来实现这个接口

abstract class BaseJavaScriptHandler : JSInterface { override fun callback( webView: WebView, params: String, successFunction: String, failFunction: String? ) { } } 三、全局注册映射不同方法对应处理类

接着不同的方法,都通过继承这个BaseJavaScriptHandler来处理各自方法的回调。比如login方法对应的处理器LoginHandler

那么前端就只需要传一个login参数过来,就可以交给LoginHandler这个类去处理,这样Android的业务代码就可以和架构代码解耦了。

class LoginHandler : BaseJavaScriptHandler() { companion object { const val KEY_ACCOUNT = "account" const val KEY_PASSWORD = "password" } override fun callback( webView: WebView, params: String, successFunction: String, failFunction: String? ) { login(webView, params, successFunction, failFunction) } private fun login(webView: WebView, params: String, successFunction: String, failFunction: String?) { } }

那么接下来如何让不同的方法都映射到不同的类名里的callback方法里去呢?

答案:通过map保存对应的方法名映射到类名的关系

然后对外暴露getJavaScriptHandler方法,来获取对应的Handler实例对象来运行callback接口

object HandlerManager { const val TAG = "HandlerManager" private val map = HashMap<String, Class<out BaseJavaScriptHandler>>() fun registerJavaScriptHandler() { register(JSBridgeConstants.METHOD_NAME_LOGIN, LoginHandler::class.java) register(JSBridgeConstants.METHOD_NAME_SHOW_TOAST, ShowToastHandler::class.java) } fun getJavaScriptHandler(methodName: String) : Class<out BaseJavaScriptHandler>? { return if (map.containsKey(methodName)) { map[methodName] } else { NoSuchMethodHandler::class.java } } private fun register(methodName: String, classObject: Class<out BaseJavaScriptHandler>) { map[methodName] = classObject } } 四、统一分发不同方法执行

由于通常前端 J a v a S c r i p t JavaScript JavaScript与 A n d r o i d Android Android交互会有多个不同的方法调用,因此我们需要设计一个统一全局调用的收口地方,然后不同的方法通过不同的参数来区分即可。

在Android端加上一个@JavascriptInterface注解,用于收敛一个与js交互的入口。

这样设计的好处是:

可以统一埋点统计Javascript调用Android代码的次数收敛一个入口,找代码方便,代码简洁解耦清晰 class JSBridge(private val context: Context, private val webView: WebView) { /** * @param method 前端调用Native端的方法名 * @param params 前端透传来的参数 * @param successFunction 执行成功后回调给前端的方法名 * @param failFunction 执行失败后回调给前端的方法名 */ @JavascriptInterface fun callNativeMethod(method: String, params: String, successFunction: String, failFunction: String) { } }

然后里面的实现可以通过用method方法名来解耦开来业务代码,不同的method方法对应用不同methodHandler类去解决单个方法需要执行的逻辑,这样就解耦开来了。

这样一来callNativeMethod方法的实现就好说了,如下所示:

/** * @param method 前端调用Native端的方法名 * @param params 前端透传来的参数 * @param successFunction 执行成功后回调给前端的方法名 * @param failFunction 执行失败后回调给前端的方法名 */ @JavascriptInterface fun callNativeMethod(method: String, params: String, successFunction: String, failFunction: String) { val javaScriptHandler = HandlerManager.getJavaScriptHandler(method) // 如果找到对应的 handler,则执行处理 javaScriptHandler?.let { handler -> // 生成对应handler的实例对象 val handlerInstance = handler.newInstance() // 触发对应handler的回调 handlerInstance.callback(webView, params, successFunction, failFunction) } ?: run { // 如果没有找到对应的 handler,可以打印日志或显示提示 Toast.makeText(context, "未找到对应的处理方法: $method", Toast.LENGTH_SHORT).show() } }

只需要在实例化全局WebView的时候,去暴露Javascript接口实例对象即可,如下所示

// 全局注册 HandlerManager.registerJavaScriptHandler() val webView: WebView = findViewById(R.id.web_container) webView.settings.javaScriptEnabled = true webView.webViewClient = WebViewClient() webView.webChromeClient = WebChromeClient() // Add JSBridge interface webView.addJavascriptInterface(JSBridge(this, webView), "JSBridge") webView.loadUrl("file:///android_asset/index.html")) 五、前端调用

这样前端调用Android端的方法就很简单了,通过 J S B r i d g e . c a l l N a t i v e M e t h o d ( ) JSBridge.callNativeMethod() JSBridge.callNativeMethod()然后在里面传不同的方法名参数过来即可。

function login() { // Call the Android login method JSBridge.callNativeMethod('login', JSON.stringify({account: username, password: password}), 'onLoginSuccess', 'onLoginFail'); } 六、所有代码

下面放出所有代码

HandlerManager.kt

import kotlin.collections.HashMap object HandlerManager { const val TAG = "HandlerManager" private val map = HashMap<String, Class<out BaseJavaScriptHandler>>() fun registerJavaScriptHandler() { register(JSBridgeConstants.METHOD_NAME_LOGIN, LoginHandler::class.java) register(JSBridgeConstants.METHOD_NAME_SHOW_TOAST, ShowToastHandler::class.java) } fun getJavaScriptHandler(methodName: String) : Class<out BaseJavaScriptHandler>? { return if (map.containsKey(methodName)) { map[methodName] } else { NoSuchMethodHandler::class.java } } private fun register(methodName: String, classObject: Class<out BaseJavaScriptHandler>) { map[methodName] = classObject } }

JSInterface.kt

import android.webkit.WebView interface JSInterface { fun callback(webView: WebView, params: String, successFunction: String, failFunction: String?) }

BaseJavaScriptHandler.kt

import android.os.Build import android.util.Log import android.webkit.WebView import org.json.JSONObject abstract class BaseJavaScriptHandler : JSInterface { companion object { const val TAG = "BaseJavaScriptHandler" } override fun callback( webView: WebView, params: String, successFunction: String, failFunction: String? ) { } fun callbackToJavaScript(webView: WebView, callbackMethod: String?, callbackParams: String?) { if (callbackMethod == null) { return } var javaScriptCode = if (callbackParams != null) { "$callbackMethod($callbackParams)" } else { "$callbackMethod()" } Log.i(TAG, "===> javaScriptCode is $javaScriptCode") MainThreadUtils.runOnMainThread(runnable = Runnable { webView.evaluateJavascript(javaScriptCode, null) }) } fun getCallbackParams(code: String?, msg: String?, content: String?) : String { val params = JSONObject().apply { code?.let { put(JSBridgeConstants.KEY_CODE, code) } msg?.let { put(JSBridgeConstants.KEY_MSG, msg) } if (content == null) { put(JSBridgeConstants.KEY_CONTENT, getExtraParams().toString()) } else { put(JSBridgeConstants.KEY_CONTENT, content) } } return params.toString() } fun getExtraParams(): JSONObject { val jsonObject = JSONObject().apply { put(JSBridgeConstants.KEY_BRAND, Build.BRAND) put(JSBridgeConstants.KEY_MODEL, Build.MODEL) } return jsonObject } }

LoginHandler.kt

package com.check.webviewapplication import android.webkit.WebView import android.widget.Toast import org.json.JSONObject class LoginHandler : BaseJavaScriptHandler() { companion object { const val KEY_ACCOUNT = "account" const val KEY_PASSWORD = "password" } override fun callback( webView: WebView, params: String, successFunction: String, failFunction: String? ) { login(webView, params, successFunction, failFunction) } private fun login(webView: WebView, params: String, successFunction: String, failFunction: String?) { val paramsObject = JSONObject(params) val account: String = paramsObject.opt(KEY_ACCOUNT) as? String ?: "" val password: String = paramsObject.get(KEY_PASSWORD) as? String ?: "" val isSuccess = checkValid(account, password) if (isSuccess) { showToast(webView, "登录成功") val callbackParams = getCallbackParams( JSBridgeConstants.CODE_SUCCESS, JSBridgeConstants.MSG_SUCCESS, getExtraParams().toString() ) callbackToJavaScript(webView, successFunction, callbackParams) } else { showToast(webView, "登录失败") val callbackParams = getCallbackParams( JSBridgeConstants.CODE_FAILURE, JSBridgeConstants.MSG_FAILURE, getExtraParams().toString() ) callbackToJavaScript(webView, failFunction, callbackParams) } } private fun checkValid(account: String, password: String) : Boolean { // 模拟账号检验流程,假设只有账号是123,密码是456的才可以检验通过 return "123" == account && "456" == password } private fun showToast(webView: WebView, msg: String) { webView.context?.let { Toast.makeText(webView.context, msg, Toast.LENGTH_SHORT).show() } } }

ShowToastHandler.kt

import android.webkit.WebView import android.widget.Toast class ShowToastHandler : BaseJavaScriptHandler() { override fun callback( webView: WebView, params: String, successFunction: String, failFunction: String? ) { webView.context?.let { Toast.makeText(webView.context, JSBridgeConstants.METHOD_NAME_SHOW_TOAST, Toast.LENGTH_SHORT).show() } val callbackParams = getCallbackParams(JSBridgeConstants.CODE_SUCCESS, JSBridgeConstants.MSG_SUCCESS, null) callbackToJavaScript(webView, successFunction, callbackParams) } }

JSBridgeConstants.kt

class JSBridgeConstants { companion object { const val METHOD_NAME_LOGIN = "login" const val METHOD_NAME_SHOW_TOAST = "showToast" const val MSG_SUCCESS = "此方法执行成功" const val MSG_FAILURE = "此方法执行失败" const val CODE_SUCCESS = "1" const val CODE_FAILURE = "0" const val KEY_CODE = "code" const val KEY_MSG = "msg" const val KEY_CONTENT = "content" const val VALUE_SUCCESS = "1" const val VALUE_FAILURE = "0" const val KEY_MODEL = "model" const val KEY_BRAND = "brand" } }

JSBridge.kt

import android.content.Context import android.webkit.JavascriptInterface import android.webkit.WebView import android.widget.Toast class JSBridge(private val context: Context, private val webView: WebView) { /** * @param method 前端调用Native端的方法名 * @param params 前端透传来的参数 * @param successFunction 执行成功后回调给前端的方法名 * @param failFunction 执行失败后回调给前端的方法名 */ @JavascriptInterface fun callNativeMethod(method: String, params: String, successFunction: String, failFunction: String) { val javaScriptHandler = HandlerManager.getJavaScriptHandler(method) // 如果找到对应的 handler,则执行处理 javaScriptHandler?.let { handler -> val handlerInstance = handler.newInstance() handlerInstance.callback(webView, params, successFunction, failFunction) } ?: run { // 如果没有找到对应的 handler,可以打印日志或显示提示 Toast.makeText(context, "未找到对应的处理方法: $method", Toast.LENGTH_SHORT).show() } } }

BaseWebView.kt

import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Toast class BaseWebView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : WebView(context, attrs, defStyleAttr) { init { setupWebView() } // 提供一份默认的webViewClient,同时提供自由注入业务的webViewClient private var webViewClient: WebViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { super.onPageStarted(view, url, favicon) // Handle page start Toast.makeText(context, "Page started: $url", Toast.LENGTH_SHORT).show() } override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) // Handle page finish Toast.makeText(context, "Page finished: $url", Toast.LENGTH_SHORT).show() } override fun onReceivedError( view: WebView?, request: WebResourceRequest?, error: WebResourceError? ) { super.onReceivedError(view, request, error) // Handle error Toast.makeText(context, "Error: ${error?.description}", Toast.LENGTH_SHORT).show() } } @SuppressLint("SetJavaScriptEnabled") private fun setupWebView() { // Enable JavaScript settings.javaScriptEnabled = true // Enable DOM storage settings.domStorageEnabled = true // Set a WebViewClient to handle page navigation webViewClient = getWebViewClient() // Set a WebChromeClient to handle JavaScript dialogs, favicons, titles, and the progress webChromeClient = WebChromeClient() // Enable zoom controls settings.setSupportZoom(true) settings.builtInZoomControls = true settings.displayZoomControls = false // Enable caching settings.cacheMode = WebSettings.LOAD_DEFAULT } // Load a URL override fun loadUrl(url: String) { super.loadUrl(url) } // Load a URL with additional headers override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) { super.loadUrl(url, additionalHttpHeaders) } // Lifecycle methods override fun onResume() { } override fun onPause() { } fun onDestroy() { // Clean up WebView clearHistory() freeMemory() destroy() } override fun setWebViewClient(client: WebViewClient) { this.webViewClient = client } override fun getWebViewClient() : WebViewClient { return webViewClient } }

MainThreadUtils.kt

import android.os.Handler import android.os.Looper object MainThreadUtils { private val mainHandler = Handler(Looper.getMainLooper()) /** * 判断当前是否在主线程 */ fun isMainThread(): Boolean { return Looper.getMainLooper().thread === Thread.currentThread() } /** * 在主线程执行代码块 * @param runnable 需要执行的代码块 */ fun runOnMainThread(runnable: Runnable) { if (isMainThread()) { runnable.run() } else { mainHandler.post(runnable) } } /** * 在主线程执行代码块(使用 lambda 表达式) * @param block 需要执行的代码块 */ fun runOnMainThread(block: () -> Unit) { if (isMainThread()) { block.invoke() } else { mainHandler.post { block.invoke() } } } /** * 延迟在主线程执行代码块 * @param delayMillis 延迟时间(毫秒) * @param block 需要执行的代码块 */ fun runOnMainThreadDelayed(delayMillis: Long, block: () -> Unit) { mainHandler.postDelayed({ block.invoke() }, delayMillis) } }

MainActivity.kt

import android.annotation.SuppressLint import android.os.Bundle import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 全局注册 HandlerManager.registerJavaScriptHandler() val webView: WebView = findViewById(R.id.web_container) webView.settings.javaScriptEnabled = true webView.webViewClient = WebViewClient() webView.webChromeClient = WebChromeClient() // Add JSBridge interface webView.addJavascriptInterface(JSBridge(this, webView), "JSBridge") // Load the local HTML file webView.loadUrl("file:///android_asset/login.html") } }

activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android /apk/res/android" xmlns:app="http://schemas.android /apk/res-auto" xmlns:tools="http://schemas.android /tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <WebView android:id="@+id/web_container" android:layout_width="match_parent" android:layout_height="600dp" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>

index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #e9ecef; } .login-container { background-color: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); width: 320px; text-align: center; } .login-container input, .login-container button { display: block; width: 100%; margin-bottom: 15px; padding: 12px; border-radius: 5px; font-size: 16px; box-sizing: border-box; } .login-container input { border: 1px solid #ddd; } .login-container button { background-color: #007BFF; color: white; border: none; cursor: pointer; transition: background-color 0.3s; } .login-container button:hover { background-color: #0056b3; } .message { margin-top: 15px; font-size: 14px; color: green; } .error { color: red; } </style> </head> <body> <div class="login-container"> <input type="text" id="username" placeholder="Username"> <input type="password" id="password" placeholder="Password"> <button onclick="login()">Login</button> <button onclick="showToast()">ShowToast</button> <div id="message" class="message"></div> </div> <script> function login() { var username = document.getElementById('username').value; var password = document.getElementById('password').value; // Call the Android login method JSBridge.callNativeMethod('login', JSON.stringify({account: username, password: password}), 'onLoginSuccess', 'onLoginFail'); } function showToast() { JSBridge.callNativeMethod('showToast', '', '', ''); } function onLoginSuccess(response) { console.log("Raw response:", response); var messageDiv = document.getElementById('message'); try { // 先将 response 转换为 JSON 字符串 const jsonString = JSON.stringify(response); console.log("JSON string:", jsonString); // 然后解析为对象 const params = JSON.parse(jsonString); console.log("Parsed params:", params); if (params.content) { const content = JSON.parse(params.content); console.log("Parsed content:", content); messageDiv.textContent = `Login successful! Brand: ${content.brand}, Model: ${content.model}`; } else { messageDiv.textContent = "Login successful! " + params.msg; } } catch (e) { console.error("Error parsing response:", e); messageDiv.textContent = "Login failed: " + e.message; } messageDiv.classList.remove('error'); } function onLoginFail(response) { var messageDiv = document.getElementById('message'); messageDiv.textContent = "Login failed!" + response; messageDiv.classList.add('error'); } </script> </body> </html>

login.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; justify-content: center; align-items: flex-end; height: 100vh; margin: 0; background-color: #e9ecef; } .login-container { background-color: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); width: 320px; text-align: center; margin-bottom: 20px; } .login-container input, .login-container button { display: block; width: 100%; margin-bottom: 15px; padding: 12px; border-radius: 5px; font-size: 16px; box-sizing: border-box; } .login-container input { border: 1px solid #ddd; } .login-container button { background-color: #007BFF; color: white; border: none; cursor: pointer; transition: background-color 0.3s; } .login-container button:hover { background-color: #0056b3; } .message { margin-top: 15px; font-size: 14px; color: green; } .error { color: red; } </style> </head> <body> <div class="login-container"> <input type="text" id="username" placeholder="Username"> <input type="password" id="password" placeholder="Password"> <button onclick="login()">Login</button> <button onclick="showToast()">ShowToast</button> <div id="message" class="message"></div> </div> <script> function login() { var username = document.getElementById('username').value; var password = document.getElementById('password').value; // Call the Android login method JSBridge.callNativeMethod('login', JSON.stringify({account: username, password: password}), 'onLoginSuccess', 'onLoginFail'); } function showToast() { JSBridge.callNativeMethod('showToast', '', '', ''); } function onLoginSuccess(response) { console.log("Raw response:", response); var messageDiv = document.getElementById('message'); try { // 先将 response 转换为 JSON 字符串 const jsonString = JSON.stringify(response); console.log("JSON string:", jsonString); // 然后解析为对象 const params = JSON.parse(jsonString); console.log("Parsed params:", params); if (params.content) { const content = JSON.parse(params.content); console.log("Parsed content:", content); messageDiv.textContent = `Login successful! Brand: ${content.brand}, Model: ${content.model}`; } else { messageDiv.textContent = "Login successful! " + params.msg; } } catch (e) { console.error("Error parsing response:", e); messageDiv.textContent = "Login failed: " + e.message; } messageDiv.classList.remove('error'); } function onLoginFail(response) { var messageDiv = document.getElementById('message'); messageDiv.textContent = "Login failed!" + response; messageDiv.classList.add('error'); } </script> </body> </html>

最后运行截图:

用chrome://inspect/#devices还可以查看对应的JavaScript控制台输出的信息

代码目录结构

标签:

用命令模式设计一个JSBridge用于JavaScript与Android交互通信由讯客互联手机栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“用命令模式设计一个JSBridge用于JavaScript与Android交互通信