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

news2025/4/8 0:24:16

用命令模式设计一个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>

最后运行截图:

image-20250216224947804

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

image-20250216225112588

代码目录结构

image-20250216224328451

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2301438.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue 3最新组件解析与实践指南:提升开发效率的利器

目录 引言 一、Vue 3核心组件特性解析 1. Composition API与组件逻辑复用 2. 内置组件与生命周期优化 3. 新一代UI组件库推荐 二、高级组件开发技巧 1. 插件化架构设计 2. 跨层级组件通信 三、性能优化实战 1. 惰性计算与缓存策略 2. 虚拟滚动与列表优化 3. Tree S…

计算机网络(涵盖OSI,TCP/IP,交换机,路由器,局域网)

一、网络通信基础 &#xff08;一&#xff09;网络通信的概念 网络通信是指终端设备之间通过计算机网络进行的信息传递与交流。它类似于现实生活中的物品传递过程&#xff1a;数据&#xff08;物品&#xff09;被封装成报文&#xff08;包裹&#xff09;&#xff0c;通过网络…

JVM-Java程序的运行环境

Java Virtual Machine Java程序的运行环境 JVM组成 程序计数器 线程私有的&#xff0c;内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。 Java堆 线程共享的区域: 主要用来保存对象实例, 数组等, 当堆中没有内存空间可分配给实例也无法再扩展时, 则抛出OutOfMe…

什么是网关,网关的作用是什么?网络安全零基础入门到精通实战教程!

1. 什么是网关 网关又称网间连接器、协议转换器&#xff0c;也就是网段(局域网、广域网)关卡&#xff0c;不同网段中的主机不能直接通信&#xff0c;需要通过关卡才能进行互访&#xff0c;比如IP地址为192.168.31.9(子网掩码&#xff1a;255.255.255.0)和192.168.7.13(子网掩码…

《千恋万花》无广版手游安卓苹果免费下载直装版

自取https://pan.xunlei.com/s/VOJS77k8NDrVawqcOerQln2lA1?pwdn6k8 《千恋万花》&#xff1a;柚子社的和风恋爱杰作 《千恋万花》&#xff08;Senren * Banka&#xff09;是由日本知名美少女游戏品牌柚子社&#xff08;Yuzusoft&#xff09;于2016年推出的一款和风恋爱题材…

javaEE-14.spring MVC练习

目录 1.加法计算器 需求分析: 前端页面代码: 后端代码实现功能: 调整前端页面代码: 进行测试: 2.用户登录 需求分析: 定义接口: 1.登录数据校验接口: 2.查询登录用户接口: 前端代码: 后端代码: 调整前端代码: 测试/查错因 后端: 前端: lombok工具 1.引入依赖…

rabbitmq五种模式的实现——springboot

rabbitmq五种模式的实现——springboot 基础知识和javase的实现形式可以看我之前的博客 代码地址&#xff1a;https://github.com/9lucifer/rabbitmq4j-learning 一、进行集成 &#xff08;一&#xff09;Spring Boot 集成 RabbitMQ 概述 Spring Boot 提供了对 RabbitMQ 的自…

23. AI-大语言模型-DeepSeek赋能开发-Spring AI集成

文章目录 前言一、Spring AI 集成 DeepSeek1. 开发AI程序2. DeepSeek 大模型3. 集成 DeepSeek 大模型1. 接入前准备2. 引入依赖3. 工程配置4. 调用示例5. 小结 4. 集成第三方平台&#xff08;已集成 DeepSeek 大模型&#xff09;1. 接入前准备2. POM依赖3. 工程配置4. 调用示例…

Educational Codeforces Round 174 (Rated for Div. 2)(ABCD)

A. Was there an Array? 翻译&#xff1a; 对于整数数组 ​&#xff0c;我们将其相等特征定义为数组 &#xff0c;其中&#xff0c;如果数组 a 的第 i 个元素等于其两个相邻元素&#xff0c;则 &#xff1b;如果数组 a 的第 i 个元素不等于其至少一个相邻元素&#xff0c;则 …

如何在本机上模拟IP地址

如何在本机上模拟IP地址 前言 在某些开发或测试场景中&#xff0c;我们可能需要在本机上模拟一个指定的 IP 地址&#xff0c;并让局域网内的其他设备能够通过该 IP 访问本机提供的服务&#xff08;如 Web 服务&#xff09;。 本文将详细介绍如何在 Windows 和 macOS 系统上实…

【嵌入式Linux应用开发基础】进程间通信(1):管道

目录 一、管道的基本概念 二、管道的工作原理 三、管道的类型 3.1. 匿名管道&#xff08;Anonymous Pipe&#xff09; 3.2. 命名管道&#xff08;Named Pipe&#xff0c;FIFO&#xff09; 四、管道的读写规则 4.1. 匿名管道的读写规则 4.2. 命名管道的读写规则 五、管…

【DeepSeek】Mac m1电脑部署DeepSeek

一、电脑配置 个人电脑配置 二、安装ollama 简介&#xff1a;Ollama 是一个强大的开源框架&#xff0c;是一个为本地运行大型语言模型而设计的工具&#xff0c;它帮助用户快速在本地运行大模型&#xff0c;通过简单的安装指令&#xff0c;可以让用户执行一条命令就在本地运…

DHCP详解,网络安全零基础入门到精通实战教程!

一、DHCP简介 DHCP(Dynamic Host Configuration Protocol),动态主机配置协议&#xff0c;是一个应用层协议。当我们将客户主机ip地址设置为动态获取方式时&#xff0c;DHCP服务器就会根据DHCP协议给客户端分配IP&#xff0c;使得客户机能够利用这个IP上网。 DHCP前身是BOOTP&am…

【Prometheus】prometheus结合pushgateway实现脚本运行状态监控

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

立创实战派ESP32-S3烧录小智AI指南

小智 AI 聊天机器人-开源项目介绍 本项目是一个开源项目&#xff0c;主要用于教学目的。我们希望通过这个项目&#xff0c;能够帮助更多人入门 AI 硬件开发&#xff0c;了解如何将当下飞速发展的大语言模型应用到实际的硬件设备中。无论你是对 AI 感兴趣的学生&#xff0c;还是…

深度学习的集装箱箱号OCR识别技术,识别率99.9%

集装箱箱号OCR识别技术是一项结合计算机视觉和规则校验的复杂任务&#xff0c;以下是其关键要点及实现思路的总结&#xff1a; 1、集装箱号结构&#xff1a;11位字符&#xff0c;格式为公司代码(3字母)和序列号(6数字)以及校验码(1数字)和尺寸/类型代码(可选)&#xff0c;例如…

如何在Windows下使用Ollama本地部署DeepSeek R1

参考链接&#xff1a; 通过Ollama本地部署DeepSeek R1以及简单使用的教程&#xff08;超详细&#xff09; 【DeepSeek应用】DeepSeek R1 本地部署&#xff08;OllamaDockerOpenWebUI&#xff09; 如何将 Chatbox 连接到远程 Ollama 服务&#xff1a;逐步指南 首先需要安装oll…

【分布式理论12】事务协调者高可用:分布式选举算法

文章目录 一、分布式系统中事务协调的问题二、分布式选举算法1. Bully算法2. Raft算法3. ZAB算法 三、小结与比较 一、分布式系统中事务协调的问题 在分布式系统中&#xff0c;常常有多个节点&#xff08;应用&#xff09;共同处理不同的事务和资源。前文 【分布式理论9】分布式…

postgres源码学习之简单sql查询

postgres源码学习之sql查询 sql查询的主流程读取sql解析sql重写sql获得执行计划执行查询操作结果返回 sql查询的主流程 参考postgres的处理流程 由上一节&#xff0c;我们可以看到&#xff0c;当有新的连接通过权限认证之后&#xff0c;将进入等待接收sql语句&#xff0c;并执…

C#项目05-猜数字多线程

本项目利用多线程&#xff0c;通过点击按钮猜数字&#xff0c; 知识点 线程 基本概念 进程:一组资源&#xff0c;构成一个正在运行的程序&#xff0c;这些资源包括地址空间、文件句柄以及程序启动需要的其他东西的载体。 线程:体现一个程序的真实执行情况&#xff0c; 线…