从 SSE 到 WebSocket实时 Web 通信的全面解析与实战

一、背景:为什么我们离不开"实时通信"?

传统 Web 是典型的 Request/Response 模式:

浏览器有需求 → 发起 HTTP 请求 → 服务端返回结果 → 连接结束。

但现在的应用越来越"活"了:

  • IM / 聊天系统:消息要秒级到达
  • 实时监控大屏:指标曲线、日志流滚动刷新
  • 在线协作:多人编辑文档、在线白板
  • 播放/交易类:弹幕、订单状态、股票价格、盘口数据......

如果还用"有事我问你"的拉模式(轮询),要么延迟高,要么 HTTP 请求堆成山,服务端和网络都顶不住。

我们真正想要的是:

建立一条长期存在 的通道,让服务器可以随时主动推送数据给客户端,甚至双方都能随时说话。

在这个诉求下,逐渐演化出:

  • 基于 HTTP 的 SSE(Server-Sent Events):单向服务器推送
  • 基于独立协议的 WebSocket:全双工双向通信

很多人下结论:"WebSocket 更高级,SSE 是过时玩意"。

其实更准确的说法是:两者定位不同,是互补,而不是替代。

二、从轮询到长连接:问题是怎么一步步暴露出来的?

2.1 短轮询(Short Polling)

最原始方案:

js 复制代码
setInterval(async () => {
  const res = await fetch('/api/messages/latest');
  const data = await res.json();
  render(data);
}, 3000);

问题:

  • 大量"空跑"请求:绝大多数时候是 "没新消息"

  • 延迟 vs 压力的矛盾:

    • 想低延迟 → 间隔变短 → QPS 暴涨
    • 想省资源 → 间隔变长 → 延迟明显

2.2 长轮询(Long Polling / Comet)

改进版:一次请求挂久一点

js 复制代码
async function longPoll() {
  try {
    const res = await fetch('/api/messages/stream');
    const data = await res.json();
    render(data);
  } finally {
    // 无论成功失败,都紧接着再发下一次
    longPoll();
  }
}

longPoll();

服务端逻辑类似:

  • 如果暂时没有消息,就挂起请求
  • 有消息 / 超时再返回

长轮询减少了一部分空响应,但核心矛盾仍在:

  • 还是要一次一次 HTTP 请求
  • 每次都走完整 HTTP 头、路由、鉴权
  • 中间代理有超时、最大连接数等限制

这时,自然会产生一个想法:

能不能建立一条真正的长连接

让服务器想发就发,而不是"一个消息对应一个 HTTP 请求/响应"?

答案就是:SSE 和 WebSocket

三、SSE:简单纯粹的服务器推送(Server-Sent Events)

3.1 定义与特点

SSE 全称 Server-Sent Events,由 HTML5 标准引入,特点:

  • 完全基于 HTTP 协议,不需要额外的协议升级
  • 连接方向是 单向:Server → Client
  • 浏览器提供原生对象 EventSource
  • 消息格式:text/event-stream 的文本流
  • 内建自动重连和 Last-Event-ID 断点续传机制
  • 实现成本低,适合简单的流式输出场景

非常适合:

  • 实时日志流、监控指标、报警流
  • 实时通知中心、站内信提醒
  • 股票/榜单/业务指标刷榜

3.2 SSE 消息格式长什么样?

Content-Type: text/event-stream,每条消息由若干行组成,以空行结束:

text 复制代码
id: 1001
event: notice
data: {"msg":"hello","level":"info"}

id: 1002
event: metrics
data: {"cpu":0.3,"mem":0.61}

: 这是注释行,浏览器不会触发事件

常用字段:

  • id:事件 ID,浏览器断线重连时会给服务器带上 Last-Event-ID
  • event:事件名,前端可以按事件类型做不同处理
  • data:真正的数据内容,可以多行(会自动拼成一行)

3.3 前端:EventSource API 详解

js 复制代码
const source = new EventSource('/sse/stream', {
  withCredentials: true, // 是否携带 Cookie
});

// 连接状态:0-connecting,1-open,2-closed
console.log(source.readyState);

// 默认事件(event: message 或未指定 event)
source.onmessage = (event) => {
  console.log('默认事件:', event.data);
  const payload = JSON.parse(event.data);
  render(payload);
};

// 自定义事件
source.addEventListener('notice', (event) => {
  console.log('notice 事件:', event.data);
});

// 打开
source.onopen = () => {
  console.log('SSE 连接已建立');
};

// 错误 & 断线(EventSource 会自动重连)
source.onerror = (err) => {
  console.error('SSE 出错或断线:', err);
  // 一般不建议这里自己重新 new EventSource
  // 否则可能和浏览器内建重连冲突
};

// 主动关闭
function closeSSE() {
  source.close();
}

几点注意:

  1. 自动重连 :底层会按一定策略重连,可以通过服务端 retry: 5000 修改重连间隔。
  2. 多事件类型 :可以根据 event: xxx 在前端用 addEventListener('xxx', ...) 分发。
  3. 跨域 :SSE 一样需要处理 CORS;如果有 Cookie 鉴权,记得 withCredentials: true

3.4 服务端:Spring MVC 阻塞版示例

真实生产不推荐用阻塞循环,这里只是演示 SSE 格式。

java 复制代码
@RestController
@RequestMapping("/sse")
public class SseController {

    @GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
    public void stream(HttpServletResponse response) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Connection", "keep-alive");
        response.setContentType("text/event-stream;charset=UTF-8");

        PrintWriter writer = response.getWriter();

        for (int i = 0; i < 10; i++) {
            String data = "{\"time\":" + System.currentTimeMillis() + ",\"value\":" + i + "}";

            writer.write("id: " + i + "\n");
            writer.write("event: metrics\n");
            writer.write("data: " + data + "\n\n");
            writer.flush();

            try {
                Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }
        }
    }
}

3.5 服务端:Spring WebFlux / Reactor 版(推荐)

使用 WebFlux 可以更自然地返回一个无限流

java 复制代码
@RestController
@RequestMapping("/sse")
public class SseReactiveController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> stream() {
        return Flux.interval(Duration.ofSeconds(1))
                   .map(seq -> ServerSentEvent.<String>builder()
                       .id(String.valueOf(seq))
                       .event("metrics")
                       .data("{\"time\":" + System.currentTimeMillis() + ",\"value\":" + seq + "}")
                       .build());
    }
}
  • MediaType.TEXT_EVENT_STREAM_VALUE 就是 text/event-stream
  • Flux.interval 只是示例,真实场景通常是从 MQ / 内存通道 / 监控系统订阅数据源。

3.6 Last-Event-ID 与断点续传

当服务器为每条消息设置 id 字段时:

text 复制代码
id: 42
event: metrics
data: {...}

浏览器断线重连时,会在请求头带上:

http 复制代码
Last-Event-ID: 42

服务器可以据此:

  • 从 ID 之后的消息继续推送(如果你有持久化)
  • 或者记录用户上次消费位置,做类"消费位点"管理

这是很多人用 SSE 做"轻量日志/监控消费"的一个重要能力。

3.7 SSE 的优点整理

  • 实现简单:HTTP + 文本流,框架 out-of-box
  • 穿透性好:大部分代理/防火墙对 HTTP 长连接都较友好
  • 自动重连:浏览器内建,不需要写复杂逻辑
  • 天然适配流式场景:日志流、指标流、通知流

3.8 SSE 的局限性

  • 单向:只能 Server → Client,Client → Server 仍需 HTTP 请求
  • 不支持原生二进制:需要自己 base64 或其它编码
  • 一些老旧环境兼容性略差(老 IE 等)
  • 受限于反向代理 / 网关对 HTTP 长连接的配置(超时、并发连接数)

一句话概括:

如果你的需求是 "后端不停往前端推数据,前端很少主动发实时消息",SSE 是非常优雅的选择。

四、WebSocket:为实时交互而生的全双工协议

4.1 协议与握手

WebSocket 依托 HTTP 做握手 ,然后升级为独立的 ws:// / wss:// 协议。

典型握手请求(浏览器→服务器):

http 复制代码
GET /ws/chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Origin: https://example.com

服务器响应:

http 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

之后,两端就不再用 HTTP 报文,而是使用 WebSocket 帧进行通信。

特点:

  • 通道是 全双工 的:任意时刻 Client/Server 都可以主动发送数据帧
  • 支持文本帧、二进制帧、Ping/Pong、Close 等类型
  • 一次握手、多次通信,头部开销几乎可以忽略

这使它成为 IM、游戏、协作等强交互场景的首选。

4.2 浏览器端 WebSocket API 详解

js 复制代码
const ws = new WebSocket('wss://example.com/ws/chat?token=xxx');

// 连接打开
ws.onopen = () => {
  console.log('WebSocket 打开');
  // 连接建好后可以立刻发认证/加入房间消息
  ws.send(JSON.stringify({
    type: 'auth',
    token: 'xxx',
  }));
};

// 收到消息
ws.onmessage = (event) => {
  // 可能是文本,也可能是二进制(取决于服务端)
  const msg = JSON.parse(event.data);
  handleMessage(msg);
};

// 连接关闭
ws.onclose = (event) => {
  console.log('WebSocket 关闭', event.code, event.reason);
  // 可在这里发起重连逻辑
};

// 错误
ws.onerror = (error) => {
  console.error('WebSocket 错误: ', error);
};

// 封装一个简单发送函数
function sendMessage(payload) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(payload));
  } else {
    console.warn('连接未就绪,消息被丢弃', payload);
  }
}

几点实战注意:

  • readyState

    • 0:CONNECTING
    • 1:OPEN
    • 2:CLOSING
    • 3:CLOSED
  • 可以通过 URL 参数、Header 或 Sec-WebSocket-Protocol 携带鉴权信息 / 版本号。

  • 若网络不稳定,要自己实现 重连策略(指数退避、最大重试次数等)。

4.3 Spring Boot + STOMP 方案(适合业务系统)

配置:

java 复制代码
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS(); // 可选:提供降级(长轮询/iframe 等)
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue"); // 订阅前缀
        registry.setApplicationDestinationPrefixes("/app"); // 客户端发送消息的目的前缀
    }
}

消息处理:

java 复制代码
@Controller
public class ChatController {

    @MessageMapping("/chat.send")     // 客户端发到 /app/chat.send
    @SendTo("/topic/room")           // 广播到订阅 /topic/room 的所有客户端
    public ChatMessage send(ChatMessage message) {
        message.setTimestamp(System.currentTimeMillis());
        return message;
    }
}

@Data
public class ChatMessage {
    private String from;
    private String content;
    private long timestamp;
}

前端可以用 @stomp/stompjs / sockjs-client 等库进行连接订阅。

4.4 Netty 手写 WebSocket(适合你做 IM 网关)

简化版示例(仅演示关键 Handler):

java 复制代码
public class WebSocketServer {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup boss = new NioEventLoopGroup(1);
        EventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss, worker)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(new HttpServerCodec());
                     p.addLast(new HttpObjectAggregator(65536));
                     p.addLast(new WebSocketServerProtocolHandler("/ws", null, true));
                     p.addLast(new SimpleChannelInboundHandler<TextWebSocketFrame>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
                             String text = msg.text();
                             // TODO: 解 JSON、路由、广播、持久化...
                             ctx.writeAndFlush(new TextWebSocketFrame("echo: " + text));
                         }
                     });
                 }
             });

            Channel ch = b.bind(8080).sync().channel();
            ch.closeFuture().sync();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

在真正的 IM / 网关场景,你会在此基础上:

  • 增加连接认证(token / cookie)
  • 建立 UserId → Channel 的映射
  • 实现房间/群组的 ChannelGroup 管理
  • 接入 MQ / RPC 与逻辑层、存储层交互

4.5 WebSocket 的优点总结

  • 全双工:前端/后端都能随时主动发消息
  • 支持二进制:适合音视频片段、protobuf、压缩格式
  • 高频通信下头部开销极小
  • 生态丰富:STOMP、Socket.io、自研协议都可以构建在其之上

4.6 WebSocket 的挑战

  • 协议/部署复杂度高于 SSE

  • 对代理 / 网关的配置要求更严(必须支持 Upgrade、keepalive 等)

  • 需要自己设计:

    • 心跳 / 断线检测
    • 重连策略
    • 订阅模型 / 房间管理
    • 鉴权与授权(哪些用户能收哪些消息)

一句话概括:

如果你的项目是 IM、游戏、协同编辑、多终端实时协作,WebSocket 几乎是标配。

五、SSE vs WebSocket:多维度对比

5.1 快速对比表

维度 SSE WebSocket
协议 HTTP + text/event-stream 独立协议(ws/wss,HTTP 101 升级)
通信方向 单向(Server → Client) 全双工(Client ↔ Server)
数据类型 文本(UTF-8) 文本 + 二进制
浏览器支持 现代浏览器支持,IE 较差 现代浏览器支持良好
API EventSource WebSocket
重连机制 内建自动重连 + Last-Event-ID 需自行实现重连逻辑
代理穿透性 很好(HTTP 长连接) 依赖代理/防火墙是否支持 WebSocket
典型场景 日志/监控/通知/行情 IM/游戏/协作/复杂实时业务
实现复杂度 服务端较简单 服务端要考虑连接管理、心跳、路由等
消息可靠性 需自建 ack/offset 机制(可用 id) 同样需自建 ack/重试机制

5.2 性能与资源

两者都是长连接,本质压力主要来自:

  • 并发连接数(File Descriptor / 线程 / EventLoop)
  • 消息编解码 & 广播逻辑
  • 下游服务瓶颈(数据库、MQ 等)

一般规律:

  • 服务端推 → 客户端收的场景,SSE 足够且实现简单;

  • 双向频繁通信、高并发场景,WebSocket 更合适;

  • 真正的性能瓶颈往往不在协议,而在:

    • 架构设计(接入层 / 逻辑层 / 存储层)
    • 消息路由 & 广播算法
    • 有无合理限流、背压机制

5.3 网络环境与企业内网

  • SSE 更像普通 HTTP 流,企业内代理、出口防火墙大多能容忍;
  • WebSocket 要求代理支持 Upgrade,部分老设备/保守配置场景会"直接掐掉",这时要么配置调整,要么降级到 SockJS / 长轮询。

在 ToB SaaS 场景(尤其连接各类甲方内网)时,这点非常关键。

六、真实项目中的选型:用什么、怎么用?

6.1 决策思路

可以简单套一个"选型决策树":

  1. 前端是否需要频繁主动发实时消息?

    • 是 → 优先 WebSocket
    • 否 → 看下一条
  2. 消息是否只需要从后端推向前端?

    • 是 & 数据以文本为主 → 优先 SSE
    • 否 → WebSocket
  3. 目标用户的网络环境是否对 WebSocket 不够友好?

    • 是 → 十分建议以 SSE / 长轮询 为基础,再按需引入 WebSocket
    • 否 → WebSocket + SSE 混用是不错的架构

6.2 场景示例

1)IM / 聊天 / 游戏大厅

  • 客户端:不停发聊天消息、操作指令
  • 服务端:推送群聊消息、在线列表、系统通知

👉 WebSocket 是主力,甚至你会做出一个"IM 接入层(Access)"作为独立服务。

2)监控大屏 / 日志中心

  • 日志/监控系统往前端持续推流
  • 前端只是查看,很少主动互动

👉 SSE 非常适合。配合 Kafka / Flink / TSDB 做后端数据源会很自然。

3)SaaS 管理后台 + 实时通知

  • 90% 页面只是 CRUD / 报表 / 查询
  • 偶尔会弹"有新工单"、"有新审批"、"有系统告警"

👉 两种实战方案:

  • 早期:基于 SSE 做通知/告警推送,简单好维护;
  • 后期:当你引入 WebSocket(比如在线客服、协同编辑)后,可以考虑统一抽一个"实时通道层",通过一个 WebSocket 上 multiplex 多种消息类型。

七、统一消息协议 & 前端封装:减少未来返工

无论 SSE 还是 WebSocket,你都可以用同一套业务消息协议,后面切换/混用时几乎不改业务层。

7.1 建议的消息 Envelope

json 复制代码
{
  "type": "chat_message",   // 消息类型
  "version": 1,             // 协议版本
  "traceId": "xxx",         // 可选:链路追踪
  "payload": {              // 业务内容
    "from": "alice",
    "to": "room-1",
    "content": "hello"
  }
}

好处:

  • 避免"满世界 if/else 判断结构不同的 JSON"
  • 方便做灰度 / 协议升级(version 字段非常关键)
  • 可以在边车 / 网关层基于 type 做路由

7.2 前端统一封装:以 WebSocket 为例

js 复制代码
class WSClient {
  constructor(url, { autoReconnect = true, maxRetry = 10 } = {}) {
    this.url = url;
    this.autoReconnect = autoReconnect;
    this.maxRetry = maxRetry;
    this.retryCount = 0;
    this.handlers = {};
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('ws open');
      this.retryCount = 0;
      this.emit('open');
    };

    this.ws.onmessage = (evt) => {
      try {
        const msg = JSON.parse(evt.data);
        this.emit(msg.type, msg);
      } catch (e) {
        console.error('parse msg error', e, evt.data);
      }
    };

    this.ws.onclose = () => {
      this.emit('close');
      if (this.autoReconnect && this.retryCount < this.maxRetry) {
        const delay = Math.min(30000, 1000 * Math.pow(2, this.retryCount));
        this.retryCount++;
        setTimeout(() => this.connect(), delay);
      }
    };

    this.ws.onerror = (err) => {
      this.emit('error', err);
    };
  }

  send(type, payload) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, payload }));
    }
  }

  on(type, handler) {
    (this.handlers[type] = this.handlers[type] || []).push(handler);
  }

  emit(type, ...args) {
    (this.handlers[type] || []).forEach((h) => h(...args));
  }

  close() {
    this.autoReconnect = false;
    this.ws.close();
  }
}

对于 SSE,也可以封一个 SSEClient,统一 on(type, handler) 风格,后端只是把 event: type 对上就行。

八、与 HTTP/2、gRPC、GraphQL Subscriptions 的关系(简要延伸)

现实中,你可能还会遇到这些名词:

  • HTTP/2 server push:设计上有"推送"能力,但在浏览器中已基本被废弃(Chrome 也移除了支持)。
  • gRPC streaming:基于 HTTP/2 的流式 RPC,更适合服务间通信,不是浏览器直连方案(除非 gRPC-Web)。
  • GraphQL Subscriptions:常见实现底层多用 WebSocket 或 SSE,自身是"语义层"的协议。

可以简单记住:

浏览器层面,"实时"主要还是 SSE + WebSocket,两者是基础。

更高层方案(GraphQL Subscriptions、SignalR、Socket.io)往往只是封装 & 协议之上的抽象

九、安全、运维与监控:上线后才是"真正的开始"

9.1 鉴权与授权

无论是 SSE 还是 WebSocket:

  • 推荐用 短期 Token(JWT / 自研),而不是单纯依赖 Cookie Session;
  • 对每个连接建立时进行认证,认证失败直接关闭;
  • 对消息还要做授权校验(用户是否有权限订阅某房间/某数据流)。

WebSocket 鉴权策略举例:

  1. 握手 URL 携带 ?token=xxx
  2. 服务器解析并验证 token,绑定 userId;
  3. 建立 userId → Channel 的映射;
  4. 业务层发送消息时,通过 userId 找到对应 Channel 进行推送。

9.2 连接数与限流

  • 每个连接都占用 FD + 内存(缓冲区):

    • 要设置连接数上限(按机器规格和业务模型);
    • 对单 IP / 单用户的连接数做限流。
  • 对消息发送:

    • 限制每秒消息数;
    • 对恶意/异常客户端进行断连或黑名单处理。

9.3 日志与监控指标

建议至少监控:

  • 当前在线连接数(按节点、按用户类型)
  • 每秒发送/接收消息数
  • 鉴权失败次数、握手失败次数
  • 断线重连频率(可用来判断网络质量/代理问题)

对于 SSE:

  • 统计每条 SSE 连接的平均持续时间;
  • 统计 Last-Event-ID 使用情况(是否频繁断线重连)。

十、常见坑与踩坑经验

10.1 SSE 被代理缓存 / 超时

  • 忘了加 Cache-Control: no-cache,结果浏览器直接从缓存读,根本不走后端;
  • 反向代理(Nginx、网关)默认有 proxy_read_timeout,时间一到就断连接 → 要配置足够长,或者前端/后端定期发心跳。

10.2 WebSocket 在 Nginx / 云原生环境下"莫名其妙断"

  • 没有正确设置:

    • proxy_set_header Upgrade $http_upgrade;
    • proxy_set_header Connection "upgrade";
  • 上游服务重启需要优雅处理断线 & 重连;

  • Kubernetes / Service Mesh 中可能有额外的超时、连接数限制,需要逐层排查。

10.3 浏览器多 Tab 的处理

  • 一个用户开 10 个 Tab,每个 Tab 开一个 WebSocket/SSE,后端就会有 10 条连接;

  • 对于某些类型的通知流,可以:

    • 只允许一个"主 Tab"持有连接,其他 Tab 通过 BroadcastChannel / localStorage 事件接收消息;
    • 或者服务端按 userId 合并推送,前端自己决定如何展示。

10.4 重连风暴

  • 服务端重启、网络抖动时,成千上万客户端同时重连;
  • 需要前端采用 指数退避 + 随机抖动 的重连策略,避免连接雪崩。

十一、总结:正确使用"工具箱",而不是迷信某个"银弹"

最后再强调一次本文想传达的观点:

  1. SSE 和 WebSocket 是两个定位不同的工具

    • SSE:简单、轻量、单向流,非常适合日志/监控/通知。
    • WebSocket:强大、通用、双向,适合 IM/游戏/协同等高交互场景。
  2. 选型时可以问自己三个问题:

    • 前端是否频繁主动发实时消息?
    • 是否需要双向通信 & 二进制?
    • 目标网络环境是否对 WebSocket 友好?
  3. 真正的难点不在"选 SSE 还是 WebSocket",而在:

    • 连接网关 / Access 层 的设计与扩展性
    • 心跳 / 重连 / 限流 / 监控 等"工程细节"
    • 与业务系统(IM、监控、告警、工单等)的解耦和演进
  4. 在一个成熟系统里,同时存在 SSE 和 WebSocket 是非常正常的

    • 例如:

      • WebSocket 用于 IM、协同编辑;
      • SSE 用于运维大屏、实时监控、系统通知。
相关推荐
打不了嗝 ᥬ᭄2 小时前
【Linux】多路转接 Select , Poll和Epoll
linux·网络·c++·网络协议·http
大熊猫侯佩2 小时前
Swift 6.2 列传(第三篇):字符串插值的 “补位神技”
前端·swift·apple
大熊猫侯佩2 小时前
Swift 6.2 列传(第二篇):标识符的 “破界神通”
前端·swift·apple
一颗宁檬不酸2 小时前
Java Web 踩坑实录:JSTL 标签库 URI 解析失败(HTTP 500 错误)完美解决
java·开发语言·前端
Nan_Shu_6142 小时前
熟悉RuoYi-Vue-Plus-前端 (2)
前端·javascript·vue.js
酒尘&2 小时前
JavaScript官网Promise篇
开发语言·前端·javascript·前端框架·交互
chuxinweihui3 小时前
⽹络层IP协议
服务器·网络·网络协议·tcp/ip
tangdou3690986553 小时前
AI真好玩系列-Three.js手势控制游戏开发教程 | Interactive Game Development with Three.js Hand Con
前端·人工智能·ai编程
七夜zippoe3 小时前
基于ReAct框架的智能体构建实战 - 从原理到企业级应用
前端·javascript·react.js·llm·agent·react