用命令模式设计一个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.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
<!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控制台输出的信息

代码目录结构

