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

最后运行截图:

image-20250216224947804

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

image-20250216225112588

代码目录结构

image-20250216224328451

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3月营销日历:开启春日盛宴,绽放生活魅力

关键营销节点∶惊蛰、女生节、妇女节、 植树节、315消费者权益日、春分 营销关键词 养生、女生魅力、感恩女性、环保、品质 01.重点关注品类 春季服饰&#xff1a;如轻薄外套、春装等&#xff0c;适合惊蛰后的市场需求&#xff1b; 美妆护肤&#xff1a;妇女节期间&#xf…

GPT-SoVITS更新V3 win整合包

GPT-SoVITS 是由社区开发者联合打造的开源语音生成框架&#xff0c;其创新性地融合了GPT语言模型与SoVITS&#xff08;Singing Voice Inference and Timbre Synthesis&#xff09;语音合成技术&#xff0c;实现了仅需5秒语音样本即可生成高保真目标音色的突破。该项目凭借其开箱…

AI芯片:科技变革的核心驱动力

近年来&#xff0c;人工智能&#xff08;AI&#xff09;的飞速发展对众多行业产生了深远影响&#xff0c;芯片领域也不例外。AI在芯片设计、制造及应用等方面带来了革新性的改变&#xff0c;成为推动芯片行业发展的关键力量。 AI助力芯片设计效率飞升 传统芯片设计极为复杂&am…

【phpstudy】关于实现两个不同版本的mysql并存。

1.首先是先安装好两个版本的mysql mysql5.7用默认的就行 2.更改mysql8.0的配置&#xff0c;如图 3.找到mysql8.0的路径&#xff0c;看着个里面就可以知道了 4.进入后&#xff0c;可以把data里面的数据情况&#xff0c;就是把data文件夹里的东西删除&#xff08;我是先备份好了一…

Coze扣子新功能详解

今晚(2025-01-24)扣子再次进行更新 主要更新内容&#xff1a; 搭建小程序和 H5 用户界面时&#xff0c;支持使用音频组件播放音频内容 数据库操作体验提升 界面优化&#xff1a;对数据库详情界面进行了重新设计&#xff0c;并将工作流运行数据库的测试数据位置从原工作流底…

Pytorch深度学习教程_3_初识pytorch

欢迎来到《PyTorch深度学习教程》系列的第三篇&#xff01;在前面的两篇中&#xff0c;我们已经介绍了Python及numpy的基本使用。今天&#xff0c;我们将深入探索PyTorch的核心功能&#xff0c;帮助你更好地理解和使用这个强大的深度学习框架。 欢迎订阅专栏&#xff1a; 深度…

第4章 信息系统架构(二)

4.2 系统架构 信息系统架构是一种体系结构&#xff0c;它反映了一个组织信息系统的各个组成部分之间的关系&#xff0c;以及信息系统与相关业务、信息系统与相关技术之间的关系。 4.2.1 架构定义 对于大规模的复杂系统来说&#xff0c;对总体的系统结构设计比起对计算算法和…

剑指 Offer II 024. 反转链表

comments: true edit_url: https://github.com/doocs/leetcode/edit/main/lcof2/%E5%89%91%E6%8C%87%20Offer%20II%20024.%20%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/README.md 剑指 Offer II 024. 反转链表 题目描述 给定单链表的头节点 head &#xff0c;请反转链表&#xff…

Python----数据结构(单链表:节点,是否为空,长度,遍历,添加,删除,查找)

一、链表 链表是一种线性数据结构&#xff0c;由一系列按特定顺序排列的节点组成&#xff0c;这些节点通过指针相互连接。每个节点包含两部分&#xff1a;元素和指向下一个节点的指针。其中&#xff0c;最简单的形式是单向链表&#xff0c;每个节点含有一个信息域和一个指针域&…

Java开发实习面试笔试题(含答案)

在广州一家中大公司面试&#xff08;BOSS标注是1000-9999人&#xff0c;薪资2-3k&#xff09;&#xff0c;招聘上写着Java开发&#xff0c;基本没有标注前端要求&#xff0c;但是到场知道是前后端分离人不分离。开始先让你做笔试&#xff08;12道问答4道SQL题&#xff09;&…

Docker:3、在VSCode上安装并运行python程序或JavaScript程序

1.VSCode上安装并运行python程序&#xff1a; 1.1.安装Docker插件 1.2.新建自动化脚本DockerFile FROM python:3.-slim-buster WORKDIR /app COPY .. RUN pip3 install -r requirements.txt CMD ["python3", "app.py"]COPY <本地路径><目标…

MOS管炸了,PWM“死区”时间得了解一下

从字面上来看“死区”的意思就是&#xff1a;如果处于这个区&#xff0c;那就会出现“损坏”的现象&#xff0c;直白点&#xff0c;就是“禁区”&#xff01; 实际应用中&#xff0c;比如大功率设备的电机&#xff0c;还有变频器等驱动电路&#xff0c;多部分都是采用MOS管和IG…

idea-代码补全快捷键

文章目录 前言idea-代码补全快捷键1. 基本补全2. 类型匹配补全3. 后缀补全4. 代码补全 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会太差&#xff0c;…

保姆级教程:利用Ollama与Open-WebUI本地部署 DeedSeek-R1大模型

1. 安装Ollama 根据自己的系统下载Ollama&#xff0c;我的是Linux&#xff0c;所以我使用如下命令进行下载安装&#xff1a; curl -fsSL https://ollama.com/install.sh | sh2. 安装Open-WebUI 使用 Docker 的方式部署 open-webui &#xff0c;使用gpu的话按照如下命令进行 …

win10 系统 自定义Ollama安装路径 及模型下载位置

win10 系统 自定义Ollama安装路径 及模型下载位置 由于Ollama的exe安装软件双击安装的时候默认是在C盘&#xff0c;以及后续的模型数据下载也在C盘&#xff0c;导致会占用C盘空间&#xff0c;所以这里单独写了一个自定义安装Ollama安装目录的教程。 Ollama官网地址&#xff1…

以教代学——费曼学习法

本文是思维导图&#xff0c;算是费曼学习法精髓我的个人总结&#xff0c;如果条件允许的话可以去看看费曼自传性质的书&#xff0c;书名见文末。 《别闹了&#xff0c;费曼先生》&#xff1a;英文书名为《Surely Youre Joking, Mr. Feynman!》&#xff0c;这是一本自传性质的书…

JWT 令牌

目录 一、JWT 1、什么是JWT 2、JWT的组成 3、JJWT签发与验证token 1、创建token 2、解析token 3、设置过期时间 4、自定义claims 前言&#xff1a; 在现代Web应用和微服务架构中&#xff0c;用户身份验证和信息安全传输是核心问题。JSON Web Token&#xff08;J…

工控网络安全介绍 工控网络安全知识题目

31.PDR模型与访问控制的主要区别(A) A、PDR把对象看作一个整体 B、PDR作为系统保护的第一道防线 C、PDR采用定性评估与定量评估相结合 D、PDR的关键因素是人 32.信息安全中PDR模型的关键因素是(A) A、人 B、技术 C、模型 D、客体 33.计算机网络最早出现在哪个年代(B) A、20世…

快速定位并优化CPU 与 JVM 内存性能瓶颈

1. CPU 性能优化实战 CPU&#xff08;Central Processing Unit&#xff09;是计算机系统的运算和控制核心&#xff0c;是信息处理、程序运行的最终执行单元&#xff0c;相当于系统的“大脑”。当 CPU 过于繁忙&#xff0c;就像“人脑”并发处理过多的事情&#xff0c;会降低做…

Kimi K1.5 与 DeepSeek R1:AI 模型的深度对比

文章目录 一、背景介绍二、核心功能对比三、K1.5 使用方法&#xff1a;四、总结 随着人工智能技术的飞速发展&#xff0c;大型语言模型在各个领域都展现出了巨大的潜力。Kimi K1.5 和 DeepSeek R1 作为当前备受关注的两款先进 AI 模型&#xff0c;各自拥有独特的功能和优势。本…