用命令模式设计一个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传输数据的协议规范,如下所示:

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控制台输出的信息

代码目录结构

相关推荐
Hilaku7 分钟前
说实话,React的开发体验,已经被Vue甩开几条街了
前端·javascript·vue.js
星语卿7 分钟前
Js事件循环
javascript
vocal7 分钟前
【我的安卓第一课】Android 多线程与异步通信机制(1)
android
datagear7 分钟前
如何在DataGear 5.4.1 中快速制作HTTP数据源服务端分页的数据表格看板
javascript·数据可视化
顾林海9 分钟前
ViewModel 销毁时机详解
android·面试·android jetpack
namehu15 分钟前
“什么?视频又双叒叕不能播了!”—— 移动端视频兼容性填坑指南
javascript·html
多啦C梦a17 分钟前
React Hooks 编程:`useState` 和 `useEffect`,再不懂就OUT了!
前端·javascript
yvvvy32 分钟前
# React Hooks 全面解析:从 useState 到 useEffect,掌握状态与副作用管理
javascript
jqq66638 分钟前
Vue3脚手架实现(七、渲染eslint配置)
前端·javascript·vue.js
Mintopia40 分钟前
BVH:光线追踪里的空间管家
前端·javascript·计算机图形学