用命令模式设计一个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传输数据的协议规范,如下所示:
json
{
"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
回调
kotlin
interface JSInterface {
fun callback(webView: WebView, params: String, successFunction: String, failFunction: String?)
}
让一个抽象类BaseJavaScriptHandler
来实现这个接口
kotlin
abstract class BaseJavaScriptHandler : JSInterface {
override fun callback(
webView: WebView,
params: String,
successFunction: String,
failFunction: String?
) {
}
}
三、全局注册映射不同方法对应处理类
接着不同的方法,都通过继承这个BaseJavaScriptHandler
来处理各自方法的回调。比如login
方法对应的处理器LoginHandler
那么前端就只需要传一个login
参数过来,就可以交给LoginHandler
这个类去处理,这样Android
的业务代码就可以和架构代码解耦了。
kotlin
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
方法的实现就好说了,如下所示:
kotlin
/**
* @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()然后在里面传不同的方法名参数过来即可。
javascript
function login() {
// Call the Android login method
JSBridge.callNativeMethod('login', JSON.stringify({account: username, password: password}), 'onLoginSuccess', 'onLoginFail');
}
六、所有代码
下面放出所有代码
HandlerManager.kt
kotlin
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
ko
import android.webkit.WebView
interface JSInterface {
fun callback(webView: WebView, params: String, successFunction: String, failFunction: String?)
}
BaseJavaScriptHandler.kt
kotlin
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
kotlin
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
kotlin
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
kotlin
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
kotlin
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
kotlin
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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/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
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
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
控制台输出的信息

代码目录结构
