当前位置: 首页 > news >正文

Android WebSocket工具类:重连、心跳、消息队列一站式解决方案

  1. 依赖库
    使用 OkHttp 的WebSocket支持。

在 build.gradle 中添加依赖:

implementation 'com.squareup.okhttp3:okhttp:4.9.3'
  1. WebSocket工具类实现
import okhttp3.*;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;public class WebSocketManager {private static final String TAG = "WebSocketManager";private OkHttpClient client;private WebSocket webSocket;private String wsUrl;private AtomicInteger reconnectCount = new AtomicInteger(0);private boolean isConnecting = false;private boolean isConnected = false;private Handler mainHandler = new Handler(Looper.getMainLooper());private Runnable heartbeatRunnable;private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>(); // 线程安全的消息队列private ExecutorService executorService = Executors.newSingleThreadExecutor(); // 线程池private int maxReconnectCount = 5; // 最大重连次数private long reconnectInterval = 5000; // 重连间隔时间(毫秒)private long heartbeatInterval = 30000; // 心跳间隔时间(毫秒)private long heartbeatTimeout = 60000; // 心跳超时时间(毫秒)private WebSocketCallback callback;private WebSocketListener webSocketListener = new WebSocketListener() {@Overridepublic void onOpen(WebSocket webSocket, Response response) {Log.d(TAG, "WebSocket connected");isConnecting = false;isConnected = true;reconnectCount.set(0); // 重置重连次数startHeartbeat(); // 开始心跳sendQueuedMessages(); // 发送缓存的消息if (callback != null) {mainHandler.post(() -> callback.onConnected());}}@Overridepublic void onMessage(WebSocket webSocket, String text) {Log.d(TAG, "Received message: " + text);if (callback != null) {mainHandler.post(() -> callback.onMessage(text));}}@Overridepublic void onClosed(WebSocket webSocket, int code, String reason) {Log.d(TAG, "WebSocket closed: " + reason);isConnecting = false;isConnected = false;stopHeartbeat(); // 停止心跳if (callback != null) {mainHandler.post(() -> callback.onDisconnected());}}@Overridepublic void onFailure(WebSocket webSocket, Throwable t, Response response) {Log.e(TAG, "WebSocket failed: " + t.getMessage());isConnecting = false;isConnected = false;stopHeartbeat(); // 停止心跳if (callback != null) {mainHandler.post(() -> callback.onError(t));}reconnect(); // 尝试重连}};// 私有构造函数,使用Builder模式创建实例private WebSocketManager(Builder builder) {this.wsUrl = builder.wsUrl;this.maxReconnectCount = builder.maxReconnectCount;this.reconnectInterval = builder.reconnectInterval;this.heartbeatInterval = builder.heartbeatInterval;this.heartbeatTimeout = builder.heartbeatTimeout;client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).writeTimeout(10, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();}// 连接WebSocketpublic void connect() {if (!isConnecting && !isConnected) {isConnecting = true;executorService.submit(() -> {Request request = new Request.Builder().url(wsUrl).build();webSocket = client.newWebSocket(request, webSocketListener);});}}// 断开连接public void disconnect() {if (webSocket != null) {executorService.submit(() -> webSocket.close(1000, "Normal closure"));}releaseResources();}// 发送消息public void sendMessage(String message) {if (webSocket != null && isConnected) {executorService.submit(() -> {boolean sent = webSocket.send(message);if (!sent) {// 发送失败,将消息加入队列messageQueue.offer(message);}});} else {// 网络未连接时,将消息加入队列messageQueue.offer(message);}}// 重连机制private void reconnect() {if (reconnectCount.get() < maxReconnectCount) {reconnectCount.incrementAndGet();mainHandler.postDelayed(() -> {Log.d(TAG, "Reconnecting... Attempt: " + reconnectCount.get());connect();}, reconnectInterval);} else {Log.e(TAG, "Max reconnection attempts reached");}}// 发送缓存的消息private void sendQueuedMessages() {executorService.submit(() -> {while (!messageQueue.isEmpty()) {String message = messageQueue.poll();if (message != null) {boolean sent = webSocket.send(message);if (!sent) {// 发送失败,将消息重新加入队列messageQueue.offer(message);break;}}}});}// 开始心跳private void startHeartbeat() {heartbeatRunnable = new Runnable() {@Overridepublic void run() {if (isConnected) {webSocket.send("Heartbeat"); // 发送心跳消息mainHandler.postDelayed(this, heartbeatInterval);}}};mainHandler.post(heartbeatRunnable);}// 停止心跳private void stopHeartbeat() {mainHandler.removeCallbacks(heartbeatRunnable);}// 释放资源private void releaseResources() {executorService.shutdown();mainHandler.removeCallbacksAndMessages(null);}// 设置回调public void setWebSocketCallback(WebSocketCallback callback) {this.callback = callback;}// Builder模式public static class Builder {private String wsUrl;private int maxReconnectCount = 5;private long reconnectInterval = 5000;private long heartbeatInterval = 30000;private long heartbeatTimeout = 60000;public Builder(String wsUrl) {this.wsUrl = wsUrl;}public Builder setMaxReconnectCount(int maxReconnectCount) {this.maxReconnectCount = maxReconnectCount;return this;}public Builder setReconnectInterval(long reconnectInterval) {this.reconnectInterval = reconnectInterval;return this;}public Builder setHeartbeatInterval(long heartbeatInterval) {this.heartbeatInterval = heartbeatInterval;return this;}public Builder setHeartbeatTimeout(long heartbeatTimeout) {this.heartbeatTimeout = heartbeatTimeout;return this;}public WebSocketManager build() {return new WebSocketManager(this);}}// 回调接口public interface WebSocketCallback {void onMessage(String message);void onConnected();void onDisconnected();void onError(Throwable t);}
}
  1. 功能说明
    3.1 连接状态管理
    增加 isConnecting 状态,区分连接中和已连接状态。

3.2 消息重发机制
在发送失败时,将消息重新加入队列,等待重连后发送。

3.3 动态心跳机制
支持动态调整心跳间隔和超时时间。

3.4 资源释放
在断开连接时,释放线程池和Handler资源,避免内存泄漏。

3.5 日志分级
通过 Log.d 和 Log.e 区分调试日志和错误日志。

  1. 使用示例
    4.1 初始化并连接WebSocket
WebSocketManager webSocketManager = new WebSocketManager.Builder("ws://your-websocket-url").setMaxReconnectCount(10).setReconnectInterval(3000).setHeartbeatInterval(60000).setHeartbeatTimeout(120000).build();webSocketManager.setWebSocketCallback(new WebSocketManager.WebSocketCallback() {@Overridepublic void onMessage(String message) {Log.d(TAG, "Received: " + message);}@Overridepublic void onConnected() {Log.d(TAG, "Connected");}@Overridepublic void onDisconnected() {Log.d(TAG, "Disconnected");}@Overridepublic void onError(Throwable t) {Log.e(TAG, "Error: " + t.getMessage());}
});webSocketManager.connect();

4.2 发送消息

webSocketManager.sendMessage("Hello, WebSocket!");

4.3 断开连接

webSocketManager.disconnect();

4.4 处理生命周期

@Override
protected void onDestroy() {super.onDestroy();webSocketManager.disconnect();
}

在Activity中使用WebSocket工具类非常简单。我们需要确保WebSocket的生命周期与Activity的生命周期绑定,避免内存泄漏和资源浪费。以下是完整的示例代码,展示如何在Activity中初始化、使用和释放WebSocket工具类。

在Activity中使用WebSocket工具类

  1. Activity代码示例
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";private WebSocketManager webSocketManager;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化WebSocketManagerwebSocketManager = new WebSocketManager.Builder("ws://your-websocket-url").setMaxReconnectCount(10).setReconnectInterval(3000).setHeartbeatInterval(60000).setHeartbeatTimeout(120000).build();// 设置WebSocket回调webSocketManager.setWebSocketCallback(new WebSocketManager.WebSocketCallback() {@Overridepublic void onMessage(String message) {Log.d(TAG, "Received: " + message);// 在这里处理接收到的消息}@Overridepublic void onConnected() {Log.d(TAG, "Connected");// 在这里处理连接成功事件}@Overridepublic void onDisconnected() {Log.d(TAG, "Disconnected");// 在这里处理连接断开事件}@Overridepublic void onError(Throwable t) {Log.e(TAG, "Error: " + t.getMessage());// 在这里处理错误事件}});// 连接WebSocketwebSocketManager.connect();// 发送消息webSocketManager.sendMessage("Hello, WebSocket!");}@Overrideprotected void onDestroy() {super.onDestroy();// 断开WebSocket连接并释放资源if (webSocketManager != null) {webSocketManager.disconnect();}}
}
  1. 代码说明
    2.1 初始化WebSocketManager
    在 onCreate 方法中,使用 Builder 模式初始化 WebSocketManager,并设置最大重连次数、重连间隔、心跳间隔和超时时间。
webSocketManager = new WebSocketManager.Builder("ws://your-websocket-url").setMaxReconnectCount(10).setReconnectInterval(3000).setHeartbeatInterval(60000).setHeartbeatTimeout(120000).build();

2.2 设置回调
通过 setWebSocketCallback 方法设置回调接口,处理WebSocket的连接、消息、断开和错误事件。

webSocketManager.setWebSocketCallback(new WebSocketManager.WebSocketCallback() {@Overridepublic void onMessage(String message) {Log.d(TAG, "Received: " + message);// 在这里处理接收到的消息}@Overridepublic void onConnected() {Log.d(TAG, "Connected");// 在这里处理连接成功事件}@Overridepublic void onDisconnected() {Log.d(TAG, "Disconnected");// 在这里处理连接断开事件}@Overridepublic void onError(Throwable t) {Log.e(TAG, "Error: " + t.getMessage());// 在这里处理错误事件}
});

2.3 连接WebSocket
在 onCreate 方法中调用 connect() 方法,启动WebSocket连接。

webSocketManager.connect();

2.4 发送消息
通过 sendMessage 方法发送消息。

webSocketManager.sendMessage("Hello, WebSocket!");

2.5 释放资源
在 onDestroy 方法中调用 disconnect() 方法,断开WebSocket连接并释放资源。

@Override
protected void onDestroy() {super.onDestroy();if (webSocketManager != null) {webSocketManager.disconnect();}
}
  1. 注意事项
    3.1 生命周期绑定
    确保WebSocket的生命周期与Activity的生命周期绑定,避免内存泄漏。

在 onDestroy 中释放资源。

3.2 线程安全
WebSocket的回调方法(如 onMessage)运行在后台线程,如果需要更新UI,请使用 Handler 或 runOnUiThread。

3.3 网络权限
在 AndroidManifest.xml 中添加网络权限:

<!--    网络权限--><uses-permission android:name="android.permission.INTERNET"/>
<!--    wifi权限--><uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/><uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/><uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

3.4 SSL/TLS支持
如果WebSocket服务器使用 wss 协议(即基于SSL/TLS的安全连接),OkHttp 会自动处理SSL/TLS握手,无需额外配置。

  1. 扩展功能
    4.1 动态调整心跳间隔
    可以根据服务器响应动态调整心跳间隔。例如,在收到服务器的心跳响应后,延长心跳间隔。
@Override
public void onMessage(String message) {if ("HeartbeatResponse".equals(message)) {// 动态调整心跳间隔webSocketManager.setHeartbeatInterval(120000);}
}

4.2 消息重发机制
在发送失败时,将消息加入队列,等待重连后重新发送。

webSocketManager.sendMessage("Hello, WebSocket!");

4.3 日志分级
通过 Log.d 和 Log.e 区分调试日志和错误日志,方便调试和生产环境使用。

  1. 总结
    在Activity中使用优化后的WebSocket工具类非常简单。通过生命周期绑定、回调接口和资源释放机制,可以确保WebSocket的高效运行和资源管理。以下是完整的流程:

初始化:在 onCreate 中初始化 WebSocketManager。

设置回调:处理连接、消息、断开和错误事件。

连接WebSocket:调用 connect() 方法。

发送消息:通过 sendMessage 方法发送消息。

释放资源:在 onDestroy 中调用 disconnect() 方法。

相关文章:

Android WebSocket工具类:重连、心跳、消息队列一站式解决方案

依赖库 使用 OkHttp 的WebSocket支持。 在 build.gradle 中添加依赖&#xff1a; implementation com.squareup.okhttp3:okhttp:4.9.3WebSocket工具类实现 import okhttp3.*; import android.os.Handler; import android.os.Looper; import android.util.Log;import java.ut…...

认识vue2脚手架

1.认识脚手架结构 使用VSCode将vue项目打开&#xff1a; package.json&#xff1a;包的说明书&#xff08;包的名字&#xff0c;包的版本&#xff0c;依赖哪些库&#xff09;。该文件里有webpack的短命令&#xff1a; serve&#xff08;启动内置服务器&#xff09; build命令…...

【STM32】STM32系列产品以及新手入门的STM32F103

&#x1f4e2; STM32F103xC/D/E 系列是一款高性能、低功耗的 32 位 MCU&#xff0c;适用于工业、汽车、消费电子等领域&#xff1b;基于 ARM Cortex-M3&#xff0c;主频最高 72MHz&#xff0c;支持 512KB Flash、64KB SRAM&#xff0c;适合复杂嵌入式应用&#xff0c;提供丰富的…...

<建模软件安装教程1>Blender4.2系列

Blender4.2安装教程 0注意&#xff1a;Windows环境下安装 第一步&#xff0c;百度网盘提取安装包。百度网盘链接&#xff1a;通过网盘分享的文件&#xff1a;blender.zip 链接: https://pan.baidu.com/s/1OG0jMMtN0qWDSQ6z_rE-9w 提取码: 0309 --来自百度网盘超级会员v3的分…...

CentOS Docker 安装指南

CentOS Docker 安装指南 引言 Docker 是一个开源的应用容器引擎&#xff0c;它允许开发者打包他们的应用以及应用的依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。Docker 容器是完全使用沙箱机制&#xff0c;相互之…...

分布式ID生成方案:数据库号段、Redis与第三方开源实现

分布式ID生成方案&#xff1a;数据库号段、Redis与第三方开源实现 引言 在分布式系统中&#xff0c;全局唯一ID生成是核心基础能力之一。本文针对三种主流分布式ID生成方案&#xff08;数据库号段模式、Redis方案、第三方开源框架&#xff09;进行解析&#xff0c;从实现原理…...

tcc编译器教程2 编译lua解释器

本文主要介绍了使用tcc编译器编译lua解释器源码。 1 介绍 lua是一门编程语言,开源且源码很容易编译,我平时用来测试C语言编程环境时经常使用。一般能编译成功就说明编程环境设置正常。下面用之前设置好的tcc编程环境进行测试。 2 获取源码 我一般有保留多个版本的lua源码进…...

利用 requestrepo 工具验证 XML外部实体注入漏洞

1. 前言 在数字化浪潮席卷的当下&#xff0c;网络安全的重要性愈发凸显。应用程序在便捷生活与工作的同时&#xff0c;也可能暗藏安全风险。XXE&#xff08;XML外部实体&#xff09;漏洞作为其中的典型代表&#xff0c;攻击者一旦利用它&#xff0c;便能窃取敏感信息、掌控服务…...

在 Maven 中使用 <scope> 元素:全面指南

目录 前言 在 Maven 中&#xff0c; 元素用于定义依赖项的作用范围&#xff0c;即依赖项在项目生命周期中的使用方式。正确使用 可以帮助我们优化项目的构建过程&#xff0c;减少不必要的依赖冲突&#xff0c;并提高构建效率。本文将详细介绍 的使用步骤、常见作用范围、代码…...

uni_app实现下拉刷新

1. 在页面配置中启用下拉刷新 首先&#xff0c;你需要在页面的 pages.json 文件中启用下拉刷新功能。 {"pages": [{"path": "pages/index/index","style": {"navigationBarTitleText": "首页","enablePull…...

PCIe协议之RCB、MPS、MRRS详解

✨前言&#xff1a; PCIe总线的存储器写请求、存储器读完成等TLP中含有数据负载&#xff0c;即Data Payload。Data Payload的长度和MPS&#xff08;Max Payload Size&#xff09;、MRRS&#xff08;Max Read Request Size&#xff09;和RCB&#xff08;Read Completion Bounda…...

达梦数据库在Linux,信创云 安装,备份,还原

&#xff08;一&#xff09;系统环境检查 1操作系统&#xff1a;确认使用的是国产麒麟操作系统&#xff0c;检查系统版本是否兼容达梦数据库 V8。可以通过以下命令查看系统版本&#xff1a; cat /etc/os-release 2硬件资源&#xff1a;确保服务器具备足够的硬件资源&#xff0…...

使用Dockerfile打包java项目生成镜像部署到Linux_java项目打docker镜像的dockerfile

比起容器、镜像来说&#xff0c;Dockerfile 非常普通&#xff0c;它就是一个纯文本&#xff0c;里面记录了一系列的构建指令&#xff0c;比如选择基础镜像、拷贝文件、运行脚本等等&#xff0c;每个指令都会生成一个 Layer&#xff0c;而 Docker 顺序执行这个文件里的所有步骤&…...

爬虫案例九js逆向爬取CBA中国篮球网

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、CBA网站分析二、代码 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 爬取CBA中国篮球网 提示&#xff1a;以下是本篇文章正文内容…...

【DeepSeek】Ubuntu快速部署DeepSeek(Ollama方式)

文章目录 人人都该学习的DeepSeekDeepSeek不同版本功能差异DeepSeek与硬件直接的关系DeepSeek系统兼容性部署方式选择部署步骤&#xff08;Ollama方式&#xff09;1.选定适合的deepseek版本2.环境准备3.安装Ollama4.部署deepseek5.测试使用 人人都该学习的DeepSeek DeepSeek 作…...

C++后端服务器开发技术栈有哪些?有哪些资源或开源库拿来用?

一、 C后台服务器开发是一个涉及多方面技术选择的复杂领域&#xff0c;特别是在高性能、高并发的场景下。以下是C后台服务器开发的一种常见技术路线&#xff0c;涵盖了从基础到高级的技术栈。 1. 基础技术栈 C标准库 C11/C14/C17/C20&#xff1a;使用现代C特性&#xff0c;如…...

基于SpringBoot的餐厅点餐管理系统设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…...

服务端和客户端通信(TCP)

服务端 using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks;namespace TeachTcpServer {class Program{static void Main(string[] args){#region 知识点一 …...

Java 大视界 -- Java 大数据在智能体育赛事运动员表现分析与训练优化中的应用(122)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…...

基于Spring Boot的多级缓存架构实现

基于Spring Boot的多级缓存架构实现 以下是一个基于Spring Boot的多级缓存架构实现示例 多级缓存架构实现方案 1. 依赖配置&#xff08;pom.xml&#xff09; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-star…...

OpenLayers 可视化之热力图

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 热力图&#xff08;Heatmap&#xff09;又叫热点图&#xff0c;是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

连锁超市冷库节能解决方案:如何实现超市降本增效

在连锁超市冷库运营中&#xff0c;高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术&#xff0c;实现年省电费15%-60%&#xff0c;且不改动原有装备、安装快捷、…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

【C++进阶篇】智能指针

C内存管理终极指南&#xff1a;智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...

高抗扰度汽车光耦合器的特性

晶台光电推出的125℃光耦合器系列产品&#xff08;包括KL357NU、KL3H7U和KL817U&#xff09;&#xff0c;专为高温环境下的汽车应用设计&#xff0c;具备以下核心优势和技术特点&#xff1a; 一、技术特性分析 高温稳定性 采用先进的LED技术和优化的IC设计&#xff0c;确保在…...

ubuntu中安装conda的后遗症

缘由: 在编译rk3588的sdk时&#xff0c;遇到编译buildroot失败&#xff0c;提示如下&#xff1a; 提示缺失expect&#xff0c;但是实测相关工具是在的&#xff0c;如下显示&#xff1a; 然后查找借助各个ai工具&#xff0c;重新安装相关的工具&#xff0c;依然无解。 解决&am…...

JavaScript 标签加载

目录 JavaScript 标签加载script 标签的 async 和 defer 属性&#xff0c;分别代表什么&#xff0c;有什么区别1. 普通 script 标签2. async 属性3. defer 属性4. type"module"5. 各种加载方式的对比6. 使用建议 JavaScript 标签加载 script 标签的 async 和 defer …...